feat: add the settings window

This commit is contained in:
Kevin Yue
2023-07-09 10:06:44 +08:00
parent 963b7d5407
commit bf96a88e21
45 changed files with 1470 additions and 641 deletions

View File

@@ -0,0 +1,82 @@
import { atom } from "jotai";
import authService from "../services/authService";
import portalService, { Prelogin } from "../services/portalService";
import { loginPortalAtom } from "./loginPortal";
import { notifyErrorAtom } from "./notification";
import { launchPasswordLoginAtom } from "./passwordLogin";
import { currentPortalDataAtom, portalAddressAtom } from "./portal";
import { launchSamlLoginAtom, retrySamlLoginAtom } from "./samlLogin";
import { isProcessingAtom, statusAtom } from "./status";
/**
* Connect to the portal, workflow:
* 1. Portal prelogin to get the prelogin data
* 2. Try to login with the cached credential
* 3. If login failed, launch the SAML login window or the password login window based on the prelogin data
*/
export const connectPortalAtom = atom(
null,
async (get, set, action?: "retry-auth") => {
// Retry the SAML authentication
if (action === "retry-auth") {
set(retrySamlLoginAtom);
return;
}
const portal = get(portalAddressAtom);
if (!portal) {
set(notifyErrorAtom, "Portal is empty");
return;
}
try {
set(statusAtom, "prelogin");
const prelogin = await portalService.prelogin(portal);
const isProcessing = get(isProcessingAtom);
if (!isProcessing) {
console.info("Request cancelled");
return;
}
try {
// If the portal is cached, use the cached credential
await set(loginWithCachedCredentialAtom, prelogin);
} catch {
// Otherwise, login with SAML or the password
if (prelogin.isSamlAuth) {
await set(launchSamlLoginAtom, prelogin);
} else {
set(launchPasswordLoginAtom, prelogin);
}
}
} catch (err) {
set(cancelConnectPortalAtom);
set(notifyErrorAtom, err);
}
}
);
connectPortalAtom.onMount = (dispatch) => {
return authService.onAuthError(() => {
dispatch("retry-auth");
});
};
export const cancelConnectPortalAtom = atom(null, (_get, set) => {
set(statusAtom, "disconnected");
});
/**
* Read the cached credential from the current portal data and login with it
*/
const loginWithCachedCredentialAtom = atom(
null,
async (get, set, prelogin: Prelogin) => {
const { cachedCredential } = get(currentPortalDataAtom);
if (!cachedCredential) {
throw new Error("No cached credential");
}
await set(loginPortalAtom, cachedCredential, prelogin);
}
);

View File

@@ -1,65 +1,48 @@
import { atom } from "jotai";
import gatewayService from "../services/gatewayService";
import vpnService from "../services/vpnService";
import { notifyErrorAtom } from "./notification";
import { isProcessingAtom, statusAtom } from "./status";
import { connectPortalAtom } from "./connectPortal";
import {
GatewayData,
currentPortalDataAtom,
updatePortalDataAtom,
} from "./portal";
import { statusAtom } from "./status";
import { disconnectVpnAtom } from "./vpn";
type GatewayCredential = {
user: string;
passwd?: string;
userAuthCookie: string;
prelogonUserAuthCookie: string;
};
export const gatewayLoginAtom = atom(
null,
async (get, set, gateway: string, credential: GatewayCredential) => {
set(statusAtom, "gateway-login");
let token: string;
try {
token = await gatewayService.login(gateway, credential);
} catch (err) {
throw new Error("Failed to login to gateway");
}
const isProcessing = get(isProcessingAtom);
if (!isProcessing) {
console.info("Request cancelled");
return;
}
await set(connectVpnAtom, gateway, token);
}
);
const connectVpnAtom = atom(
null,
async (_get, set, vpnAddress: string, token: string) => {
try {
set(statusAtom, "connecting");
await vpnService.connect(vpnAddress, token);
set(statusAtom, "connected");
} catch (err) {
throw new Error("Failed to connect to VPN");
}
}
);
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const disconnectVpnAtom = atom(null, async (get, set) => {
try {
set(statusAtom, "disconnecting");
await vpnService.disconnect();
// Sleep a short time, so that the client can receive the service's disconnected event.
await sleep(100);
} catch (err) {
set(statusAtom, "disconnected");
set(notifyErrorAtom, "Failed to disconnect from VPN");
}
export const portalGatewaysAtom = atom<GatewayData[]>((get) => {
const { gateways } = get(currentPortalDataAtom);
return gateways;
});
export const selectedGatewayAtom = atom(
(get) => get(currentPortalDataAtom).selectedGateway,
async (get, set, update: string) => {
const portalData = get(currentPortalDataAtom);
await set(updatePortalDataAtom, { ...portalData, selectedGateway: update });
}
);
export const gatewaySwitcherVisibleAtom = atom(false);
export const openGatewaySwitcherAtom = atom(null, (get, set) => {
export const openGatewaySwitcherAtom = atom(null, (_get, set) => {
set(gatewaySwitcherVisibleAtom, true);
});
const switchingAtom = atom(false);
export const switchGatewayAtom = atom(
(get) => get(switchingAtom),
async (get, set, gateway: GatewayData) => {
const status = await get(statusAtom);
// Update the selected gateway first
await set(selectedGatewayAtom, gateway.name);
if (status === "connected") {
try {
set(switchingAtom, true);
await set(disconnectVpnAtom);
await set(connectPortalAtom);
} finally {
set(switchingAtom, false);
}
}
}
);

View File

@@ -0,0 +1,35 @@
import { atom } from "jotai";
import gatewayService from "../services/gatewayService";
import { isProcessingAtom, statusAtom } from "./status";
import { connectVpnAtom } from "./vpn";
type GatewayCredential = {
user: string;
passwd?: string;
userAuthCookie: string;
prelogonUserAuthCookie: string;
};
/**
* Login to a gateway to get the token, and then connect to VPN with the token
*/
export const loginGatewayAtom = atom(
null,
async (get, set, gateway: string, credential: GatewayCredential) => {
set(statusAtom, "gateway-login");
let token: string;
try {
token = await gatewayService.login(gateway, credential);
} catch (err) {
throw new Error("Failed to login to gateway");
}
const isProcessing = get(isProcessingAtom);
if (!isProcessing) {
console.info("Request cancelled");
return;
}
await set(connectVpnAtom, gateway, token);
}
);

View File

@@ -0,0 +1,100 @@
import { atom } from "jotai";
import portalService, {
PortalConfig,
PortalCredential,
Prelogin,
} from "../services/portalService";
import { selectedGatewayAtom } from "./gateway";
import { loginGatewayAtom } from "./loginGateway";
import { portalAddressAtom, updatePortalDataAtom } from "./portal";
import { isProcessingAtom, statusAtom } from "./status";
// Indicates whether the portal config is being fetched
// This is mainly used to show the loading indicator in the password login form
const portalConfigLoadingAtom = atom(false);
/**
* Workflow:
*
* 1. Fetch portal config
* 2. Save the portal config to the external storage
* 3. Login the gateway, which will retrieve the token and pass it
* to the background service to connect the VPN
*/
export const loginPortalAtom = atom(
(get) => get(portalConfigLoadingAtom),
async (
get,
set,
credential: PortalCredential,
prelogin: Prelogin,
configFetched?: () => void
) => {
set(statusAtom, "portal-config");
const portalAddress = get(portalAddressAtom);
if (!portalAddress) {
throw new Error("Portal is empty");
}
set(portalConfigLoadingAtom, true);
let portalConfig: PortalConfig;
try {
portalConfig = await portalService.fetchConfig(portalAddress, credential);
configFetched?.();
} finally {
set(portalConfigLoadingAtom, false);
}
const isProcessing = get(isProcessingAtom);
if (!isProcessing) {
console.info("Request cancelled");
return;
}
const { gateways, userAuthCookie, prelogonUserAuthCookie } = portalConfig;
if (!gateways.length) {
throw new Error("No gateway found");
}
if (userAuthCookie === "empty" || prelogonUserAuthCookie === "empty") {
throw new Error("Failed to login, please try again");
}
// Here, we have got the portal config successfully, refresh the cached portal data
const previousSelectedGateway = get(selectedGatewayAtom);
const selectedGateway = gateways.find(
({ name }) => name === previousSelectedGateway
);
// Update the portal data to persist it
await set(updatePortalDataAtom, {
address: portalAddress,
gateways: gateways.map(({ name, address }) => ({ name, address })),
cachedCredential: {
user: credential.user,
passwd: credential.passwd,
"portal-userauthcookie": userAuthCookie,
"portal-prelogonuserauthcookie": prelogonUserAuthCookie,
},
selectedGateway: selectedGateway?.name,
});
// Choose the best gateway
const { region } = prelogin;
const { name, address } = portalService.chooseGateway(gateways, {
region,
preferredGateway: previousSelectedGateway,
});
// Log in to the gateway
await set(loginGatewayAtom, address, {
user: credential.user,
userAuthCookie,
prelogonUserAuthCookie,
});
// Update the selected gateway after a successful login
await set(selectedGatewayAtom, name);
}
);

View File

@@ -1,17 +1,25 @@
import { exit } from "@tauri-apps/api/process";
import { atom } from "jotai";
import { RESET } from "jotai/utils";
import { disconnectVpnAtom } from "./gateway";
import { appDataStorageAtom, portalAddressAtom } from "./portal";
import settingsService, { TabValue } from "../services/settingsService";
import { passwordAtom, usernameAtom } from "./passwordLogin";
import { appDataAtom, portalAddressAtom } from "./portal";
import { statusAtom } from "./status";
import { disconnectVpnAtom } from "./vpn";
export const openSettingsAtom = atom(null, (_get, _set, update?: TabValue) => {
settingsService.openSettings({ tab: update });
});
export const resetAtom = atom(null, (_get, set) => {
set(appDataStorageAtom, RESET);
set(appDataAtom, RESET);
set(portalAddressAtom, "");
set(usernameAtom, "");
set(passwordAtom, "");
});
export const quitAtom = atom(null, async (get, set) => {
const status = get(statusAtom);
const status = await get(statusAtom);
if (status === "connected") {
await set(disconnectVpnAtom);

View File

@@ -1,5 +1,6 @@
import { AlertColor } from "@mui/material";
import { atom } from "jotai";
import ErrorWithTitle from "../utils/ErrorWithTitle";
export type Severity = AlertColor;
@@ -37,9 +38,11 @@ export const notifyErrorAtom = atom(
msg = "Unknown error";
}
const title = err instanceof ErrorWithTitle ? err.title : "Error";
set(notificationVisibleAtom, true);
set(notificationConfigAtom, {
title: "Error",
title,
message: msg,
severity: "error",
duration: duration <= 0 ? undefined : duration,

View File

@@ -0,0 +1,74 @@
import { atom } from "jotai";
import { atomWithDefault } from "jotai/utils";
import { PasswordPrelogin } from "../services/portalService";
import { loginPortalAtom } from "./loginPortal";
import { notifyErrorAtom } from "./notification";
import { currentPortalDataAtom, portalAddressAtom } from "./portal";
import { statusAtom } from "./status";
const loginFormVisibleAtom = atom(false);
export const passwordPreloginAtom = atom<PasswordPrelogin>({
isSamlAuth: false,
region: "",
authMessage: "",
labelUsername: "",
labelPassword: "",
});
export const launchPasswordLoginAtom = atom(
null,
(_get, set, prelogin: PasswordPrelogin) => {
set(loginFormVisibleAtom, true);
set(passwordPreloginAtom, prelogin);
set(statusAtom, "authenticating-password");
}
);
// Use the cached credential to login
export const usernameAtom = atomWithDefault((get) => {
return get(currentPortalDataAtom).cachedCredential?.user ?? "";
});
export const passwordAtom = atomWithDefault((get) => {
return get(currentPortalDataAtom).cachedCredential?.passwd ?? "";
});
export const cancelPasswordAuthAtom = atom(
(get) => get(loginFormVisibleAtom),
(_get, set) => {
set(loginFormVisibleAtom, false);
set(statusAtom, "disconnected");
}
);
export const passwordLoginAtom = atom(
(get) => get(loginPortalAtom),
async (get, set) => {
const portal = get(portalAddressAtom);
const username = get(usernameAtom);
const password = get(passwordAtom);
if (!portal) {
set(notifyErrorAtom, "Portal is empty");
return;
}
if (!username) {
set(notifyErrorAtom, "Username is empty");
return;
}
try {
const credential = { user: username, passwd: password };
const prelogin = get(passwordPreloginAtom);
await set(loginPortalAtom, credential, prelogin, () => {
// Hide the login form after portal login success
set(loginFormVisibleAtom, false);
});
} catch (err) {
set(statusAtom, "disconnected");
set(notifyErrorAtom, err);
}
}
);

View File

@@ -1,16 +1,8 @@
import { atom } from "jotai";
import { withImmer } from "jotai-immer";
import { atomWithDefault, atomWithStorage } from "jotai/utils";
import authService, { AuthData } from "../services/authService";
import portalService, {
PasswordPrelogin,
PortalCredential,
Prelogin,
SamlPrelogin,
} from "../services/portalService";
import { disconnectVpnAtom, gatewayLoginAtom } from "./gateway";
import { notifyErrorAtom } from "./notification";
import { isProcessingAtom, statusAtom } from "./status";
import { atomWithDefault } from "jotai/utils";
import { PortalCredential } from "../services/portalService";
import { atomWithTauriStorage } from "../services/storeService";
import { unwrap } from "./unwrap";
export type GatewayData = {
name: string;
@@ -32,346 +24,65 @@ type AppData = {
clearCookies: boolean;
};
type AppDataUpdate =
| {
type: "PORTAL";
payload: PortalData;
}
| {
type: "SELECTED_GATEWAY";
payload: string;
};
const defaultAppData: AppData = {
const DEFAULT_APP_DATA: AppData = {
portal: "",
portals: [],
// Whether to clear the cookies of the SAML login webview, default is true
clearCookies: true,
};
export const appDataStorageAtom = atomWithStorage<AppData>(
"APP_DATA",
defaultAppData
);
const appDataImmerAtom = withImmer(appDataStorageAtom);
const updateAppDataAtom = atom(null, (_get, set, update: AppDataUpdate) => {
const { type, payload } = update;
switch (type) {
case "PORTAL":
const { address } = payload;
set(appDataImmerAtom, (draft) => {
draft.portal = address;
const portalIndex = draft.portals.findIndex(
({ address: portalAddress }) => portalAddress === address
);
if (portalIndex === -1) {
draft.portals.push(payload);
} else {
draft.portals[portalIndex] = payload;
}
});
break;
case "SELECTED_GATEWAY":
set(appDataImmerAtom, (draft) => {
const { portal, portals } = draft;
const portalData = portals.find(({ address }) => address === portal);
if (portalData) {
portalData.selectedGateway = payload;
}
});
break;
}
});
export const portalAddressAtom = atomWithDefault(
(get) => get(appDataImmerAtom).portal
export const appDataAtom = atomWithTauriStorage("APP_DATA", DEFAULT_APP_DATA);
const unwrappedAppDataAtom = atom(
(get) => get(unwrap(appDataAtom)) || DEFAULT_APP_DATA
);
// Read the portal address from the store as the default value
export const portalAddressAtom = atomWithDefault<string>(
(get) => get(unwrappedAppDataAtom).portal
);
// The cached portal data for the current portal address
export const currentPortalDataAtom = atom<PortalData>((get) => {
const portalAddress = get(portalAddressAtom);
const { portals } = get(appDataImmerAtom);
const appData = get(unwrappedAppDataAtom);
const { portals } = appData;
const portalData = portals.find(({ address }) => address === portalAddress);
return portalData || { address: portalAddress, gateways: [] };
});
const clearCookiesAtom = atom(
(get) => get(appDataImmerAtom).clearCookies,
(_get, set, update: boolean) => {
set(appDataImmerAtom, (draft) => {
draft.clearCookies = update;
});
}
);
export const portalGatewaysAtom = atom<GatewayData[]>((get) => {
const { gateways } = get(currentPortalDataAtom);
return gateways;
});
export const selectedGatewayAtom = atom(
(get) => get(currentPortalDataAtom).selectedGateway
);
export const connectPortalAtom = atom(
(get) => get(isProcessingAtom),
async (get, set, action?: "retry-auth") => {
// Retry the SAML authentication
if (action === "retry-auth") {
set(retrySamlAuthAtom);
return;
}
const portal = get(portalAddressAtom);
if (!portal) {
set(notifyErrorAtom, "Portal is empty");
return;
}
try {
set(statusAtom, "prelogin");
const prelogin = await portalService.prelogin(portal);
const isProcessing = get(isProcessingAtom);
if (!isProcessing) {
console.info("Request cancelled");
return;
}
try {
await set(loginWithCachedCredentialAtom, prelogin);
} catch {
if (prelogin.isSamlAuth) {
await set(launchSamlAuthAtom, prelogin);
} else {
await set(launchPasswordAuthAtom, prelogin);
}
}
} catch (err) {
set(cancelConnectPortalAtom);
set(notifyErrorAtom, err);
}
}
);
connectPortalAtom.onMount = (dispatch) => {
return authService.onAuthError(() => {
dispatch("retry-auth");
});
};
const loginWithCachedCredentialAtom = atom(
export const updatePortalDataAtom = atom(
null,
async (get, set, prelogin: Prelogin) => {
const { cachedCredential } = get(currentPortalDataAtom);
if (!cachedCredential) {
throw new Error("No cached credential");
async (get, set, update: PortalData) => {
const appData = await get(appDataAtom);
const { portals } = appData;
const portalIndex = portals.findIndex(
({ address }) => address === update.address
);
if (portalIndex === -1) {
portals.push(update);
} else {
portals[portalIndex] = update;
}
await set(portalLoginAtom, cachedCredential, prelogin);
await set(appDataAtom, (appData) => ({
...appData,
portal: update.address,
portals,
}));
}
);
export const passwordPreloginAtom = atom<PasswordPrelogin>({
isSamlAuth: false,
region: "",
authMessage: "",
labelUsername: "",
labelPassword: "",
});
export const cancelConnectPortalAtom = atom(null, (_get, set) => {
set(statusAtom, "disconnected");
});
export const usernameAtom = atomWithDefault(
(get) => get(currentPortalDataAtom).cachedCredential?.user ?? ""
);
export const passwordAtom = atomWithDefault(
(get) => get(currentPortalDataAtom).cachedCredential?.passwd ?? ""
);
const passwordAuthVisibleAtom = atom(false);
const launchPasswordAuthAtom = atom(
null,
async (_get, set, prelogin: PasswordPrelogin) => {
set(passwordAuthVisibleAtom, true);
set(passwordPreloginAtom, prelogin);
set(statusAtom, "authenticating-password");
}
);
export const cancelPasswordAuthAtom = atom(
(get) => get(passwordAuthVisibleAtom),
(_get, set) => {
set(passwordAuthVisibleAtom, false);
set(cancelConnectPortalAtom);
}
);
export const passwordLoginAtom = atom(
(get) => get(portalConfigLoadingAtom),
async (get, set, username: string, password: string) => {
const portal = get(portalAddressAtom);
if (!portal) {
set(notifyErrorAtom, "Portal is empty");
return;
}
if (!username) {
set(notifyErrorAtom, "Username is empty");
return;
}
try {
const credential = { user: username, passwd: password };
const prelogin = get(passwordPreloginAtom);
await set(portalLoginAtom, credential, prelogin);
} catch (err) {
set(cancelConnectPortalAtom);
set(notifyErrorAtom, err);
}
}
);
const launchSamlAuthAtom = atom(
null,
async (get, set, prelogin: SamlPrelogin) => {
const { samlAuthMethod, samlRequest } = prelogin;
let authData: AuthData;
try {
set(statusAtom, "authenticating-saml");
const clearCookies = get(clearCookiesAtom);
authData = await authService.samlLogin(
samlAuthMethod,
samlRequest,
clearCookies
);
} catch (err) {
throw new Error("SAML login failed");
}
if (!authData) {
// User closed the SAML login window, cancel the login
set(cancelConnectPortalAtom);
return;
}
// SAML login success, update clearCookies to false to reuse the SAML session
set(clearCookiesAtom, false);
const credential = {
user: authData.username,
"prelogin-cookie": authData.prelogin_cookie,
"portal-userauthcookie": authData.portal_userauthcookie,
};
await set(portalLoginAtom, credential, prelogin);
}
);
const retrySamlAuthAtom = atom(null, async (get) => {
const portal = get(portalAddressAtom);
const prelogin = await portalService.prelogin(portal);
if (prelogin.isSamlAuth) {
await authService.emitAuthRequest({
samlBinding: prelogin.samlAuthMethod,
samlRequest: prelogin.samlRequest,
});
}
});
const portalConfigLoadingAtom = atom(false);
const portalLoginAtom = atom(
(get) => get(portalConfigLoadingAtom),
async (get, set, credential: PortalCredential, prelogin: Prelogin) => {
set(statusAtom, "portal-config");
set(portalConfigLoadingAtom, true);
const portalAddress = get(portalAddressAtom);
let portalConfig;
try {
portalConfig = await portalService.fetchConfig(portalAddress, credential);
// Ensure the password auth window is closed
set(passwordAuthVisibleAtom, false);
} finally {
set(portalConfigLoadingAtom, false);
}
const isProcessing = get(isProcessingAtom);
if (!isProcessing) {
console.info("Request cancelled");
return;
}
const { gateways, userAuthCookie, prelogonUserAuthCookie } = portalConfig;
if (!gateways.length) {
throw new Error("No gateway found");
}
if (userAuthCookie === "empty" || prelogonUserAuthCookie === "empty") {
throw new Error("Failed to login, please try again");
}
// Previous selected gateway
const previousGateway = get(selectedGatewayAtom);
// Update the app data to persist the portal data
set(updateAppDataAtom, {
type: "PORTAL",
payload: {
address: portalAddress,
gateways: gateways.map(({ name, address }) => ({
name,
address,
})),
cachedCredential: {
user: credential.user,
passwd: credential.passwd,
"portal-userauthcookie": userAuthCookie,
"portal-prelogonuserauthcookie": prelogonUserAuthCookie,
},
selectedGateway: previousGateway,
},
});
const { region } = prelogin;
const { name, address } = portalService.preferredGateway(gateways, {
region,
previousGateway,
});
await set(gatewayLoginAtom, address, {
user: credential.user,
userAuthCookie,
prelogonUserAuthCookie,
});
// Update the app data to persist the gateway data
set(updateAppDataAtom, {
type: "SELECTED_GATEWAY",
payload: name,
});
}
);
export const switchingGatewayAtom = atom(false);
export const switchToGatewayAtom = atom(
(get) => get(switchingGatewayAtom),
async (get, set, gateway: GatewayData) => {
set(updateAppDataAtom, {
type: "SELECTED_GATEWAY",
payload: gateway.name,
});
if (get(statusAtom) === "connected") {
try {
set(switchingGatewayAtom, true);
await set(disconnectVpnAtom);
await set(connectPortalAtom);
} finally {
set(switchingGatewayAtom, false);
}
}
export const clearCookiesAtom = atom(
async (get) => {
const { clearCookies } = await get(appDataAtom);
return clearCookies;
},
async (_get, set, update: boolean) => {
await set(appDataAtom, (appData) => ({
...appData,
clearCookies: update,
}));
}
);

View File

@@ -0,0 +1,59 @@
import { atom } from "jotai";
import authService, { AuthData } from "../services/authService";
import portalService, { SamlPrelogin } from "../services/portalService";
import { loginPortalAtom } from "./loginPortal";
import { clearCookiesAtom, portalAddressAtom } from "./portal";
import { statusAtom } from "./status";
import { unwrap } from "./unwrap";
export const launchSamlLoginAtom = atom(
null,
async (get, set, prelogin: SamlPrelogin) => {
const { samlAuthMethod, samlRequest } = prelogin;
let authData: AuthData;
try {
set(statusAtom, "authenticating-saml");
const clearCookies = await get(clearCookiesAtom);
authData = await authService.samlLogin(
samlAuthMethod,
samlRequest,
clearCookies
);
// update clearCookies to false to reuse the SAML session
await set(clearCookiesAtom, false);
} catch (err) {
throw new Error("SAML login failed");
}
if (!authData) {
// User closed the SAML login window, cancel the login
set(statusAtom, "disconnected");
return;
}
const credential = {
user: authData.username,
"prelogin-cookie": authData.prelogin_cookie,
"portal-userauthcookie": authData.portal_userauthcookie,
};
await set(loginPortalAtom, credential, prelogin);
}
);
export const retrySamlLoginAtom = atom(null, async (get) => {
const portal = get(portalAddressAtom);
if (!portal) {
throw new Error("Portal not found");
}
const prelogin = await portalService.prelogin(portal);
if (prelogin.isSamlAuth) {
await authService.emitAuthRequest({
samlBinding: prelogin.samlAuthMethod,
samlRequest: prelogin.samlRequest,
});
}
});

View File

@@ -0,0 +1,98 @@
import { atom } from "jotai";
import { atomWithDefault } from "jotai/utils";
import settingsService, {
ClientOS,
DEFAULT_SETTINGS_DATA,
SETTINGS_DATA,
} from "../services/settingsService";
import { atomWithTauriStorage } from "../services/storeService";
import { unwrap } from "./unwrap";
const settingsDataAtom = atomWithTauriStorage(
SETTINGS_DATA,
DEFAULT_SETTINGS_DATA
);
const unwrappedSettingsDataAtom = atom(
(get) => get(unwrap(settingsDataAtom)) || DEFAULT_SETTINGS_DATA
);
export const clientOSAtom = atomWithDefault<ClientOS>((get) => {
const { clientOS } = get(unwrappedSettingsDataAtom);
return clientOS;
});
export const osVersionAtom = atomWithDefault<string>((get) => {
const { osVersion } = get(unwrappedSettingsDataAtom);
return osVersion;
});
// The os version of the current OS, retrieved from the Rust backend
const currentOsVersionAtom = atomWithDefault(() =>
settingsService.getCurrentOsVersion()
);
// The default OS version for the selected client OS
export const defaultOsVersionAtom = atomWithDefault((get) => {
const clientOS = get(clientOSAtom);
const osVersion = get(osVersionAtom);
const currentOsVersion = get(unwrap(currentOsVersionAtom));
// The current OS version is not ready, trigger the suspense,
// to avoid the intermediate UI state
if (!currentOsVersion) {
return Promise.resolve("");
}
return settingsService.determineOsVersion(
clientOS,
osVersion,
currentOsVersion
);
});
export const clientVersionAtom = atomWithDefault<string>((get) => {
const { clientVersion } = get(unwrappedSettingsDataAtom);
return clientVersion;
});
export const userAgentAtom = atom((get) => {
const clientOS = get(clientOSAtom);
const osVersion = get(osVersionAtom);
const currentOsVersion = get(unwrap(currentOsVersionAtom)) || "";
const clientVersion = get(clientVersionAtom);
return settingsService.buildUserAgent(
clientOS,
osVersion,
currentOsVersion,
clientVersion
);
});
export const customOpenSSLAtom = atomWithDefault<boolean>((get) => {
const { customOpenSSL } = get(unwrappedSettingsDataAtom);
return customOpenSSL;
});
export const opensslConfigAtom = atomWithDefault(async () => {
return settingsService.getOpenSSLConfig();
});
export const saveSettingsAtom = atom(null, async (get, set) => {
const clientOS = get(clientOSAtom);
const osVersion = get(osVersionAtom);
const clientVersion = get(clientVersionAtom);
const customOpenSSL = get(customOpenSSLAtom);
await set(settingsDataAtom, {
clientOS,
osVersion,
clientVersion,
customOpenSSL,
});
if (customOpenSSL) {
await settingsService.updateOpenSSLConfig();
}
});

View File

@@ -1,8 +1,9 @@
import { atom } from "jotai";
import { atomWithDefault } from "jotai/utils";
import vpnService from "../services/vpnService";
import { selectedGatewayAtom, switchGatewayAtom } from "./gateway";
import { notifyErrorAtom, notifySuccessAtom } from "./notification";
import { selectedGatewayAtom, switchingGatewayAtom } from "./portal";
import { unwrap } from "./unwrap";
export type Status =
| "disconnected"
@@ -16,17 +17,22 @@ export type Status =
| "disconnecting"
| "error";
const internalIsOnlineAtom = atomWithDefault(() => vpnService.isOnline());
export const isOnlineAtom = atom(
(get) => get(internalIsOnlineAtom),
// Whether the gpservice has started
const _backgroundServiceStartedAtom = atomWithDefault<
boolean | Promise<boolean>
>(() => vpnService.isOnline());
export const backgroundServiceStartedAtom = atom(
(get) => get(_backgroundServiceStartedAtom),
async (get, set, update: boolean) => {
const isOnline = await get(internalIsOnlineAtom);
// Already online, do nothing
if (update && update === isOnline) {
const prev = await get(_backgroundServiceStartedAtom);
// Already started, do nothing
if (update && update === prev) {
return;
}
set(internalIsOnlineAtom, update);
set(_backgroundServiceStartedAtom, update);
// From stopped to started
if (update) {
set(notifySuccessAtom, "The background service is online");
} else {
@@ -34,25 +40,19 @@ export const isOnlineAtom = atom(
}
}
);
isOnlineAtom.onMount = (setAtom) => vpnService.onServiceStatusChanged(setAtom);
const internalStatusReadyAtom = atom(false);
export const statusReadyAtom = atom(
(get) => get(internalStatusReadyAtom),
(get, set, status: Status) => {
set(internalStatusReadyAtom, true);
set(statusAtom, status);
}
);
statusReadyAtom.onMount = (setAtom) => {
vpnService.status().then(setAtom);
backgroundServiceStartedAtom.onMount = (setAtom) => {
vpnService.onServiceStatusChanged(setAtom);
};
export const statusAtom = atom<Status>("disconnected");
// The current status of the vpn connection
export const statusAtom = atomWithDefault<Status | Promise<Status>>(() =>
vpnService.status()
);
statusAtom.onMount = (setAtom) => vpnService.onVpnStatusChanged(setAtom);
const statusTextMap: Record<Status, String> = {
const statusTextMap: Record<Status, string> = {
disconnected: "Not Connected",
prelogin: "Portal pre-logging in...",
"authenticating-saml": "Authenticating...",
@@ -65,9 +65,13 @@ const statusTextMap: Record<Status, String> = {
error: "Error",
};
export const statusTextAtom = atom((get) => {
const status = get(statusAtom);
const switchingGateway = get(switchingGatewayAtom);
export const statusTextAtom = atom<string>((get) => {
const status = get(unwrap(statusAtom));
const switchingGateway = get(switchGatewayAtom);
if (!status) {
return "Loading...";
}
if (status === "connected") {
const selectedGateway = get(selectedGatewayAtom);
@@ -84,11 +88,16 @@ export const statusTextAtom = atom((get) => {
return statusTextMap[status];
});
export const isProcessingAtom = atom((get) => {
const status = get(statusAtom);
const switchingGateway = get(switchingGatewayAtom);
export const isProcessingAtom = atom<boolean>((get) => {
const status = get(unwrap(statusAtom));
const switchingGateway = get(switchGatewayAtom);
return (
(status !== "disconnected" && status !== "connected") || switchingGateway
);
if (!status) {
return false;
}
if (switchingGateway) {
return true;
}
return status !== "disconnected" && status !== "connected";
});

View File

@@ -0,0 +1 @@
export { unstable_unwrap as unwrap } from "jotai/utils";

30
gpgui/src/atoms/vpn.ts Normal file
View File

@@ -0,0 +1,30 @@
import { atom } from "jotai";
import vpnService from "../services/vpnService";
import { notifyErrorAtom } from "./notification";
import { statusAtom } from "./status";
export const connectVpnAtom = atom(
null,
async (_get, set, vpnAddress: string, token: string) => {
try {
set(statusAtom, "connecting");
await vpnService.connect(vpnAddress, token);
set(statusAtom, "connected");
} catch (err) {
throw new Error("Failed to connect to VPN");
}
}
);
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const disconnectVpnAtom = atom(null, async (get, set) => {
try {
set(statusAtom, "disconnecting");
await vpnService.disconnect();
// Sleep a short time, so that the client can receive the service's disconnected event.
await sleep(100);
} catch (err) {
set(statusAtom, "disconnected");
set(notifyErrorAtom, "Failed to disconnect from VPN");
}
});