mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
feat: add the settings window
This commit is contained in:
82
gpgui/src/atoms/connectPortal.ts
Normal file
82
gpgui/src/atoms/connectPortal.ts
Normal 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);
|
||||
}
|
||||
);
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
35
gpgui/src/atoms/loginGateway.ts
Normal file
35
gpgui/src/atoms/loginGateway.ts
Normal 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);
|
||||
}
|
||||
);
|
100
gpgui/src/atoms/loginPortal.ts
Normal file
100
gpgui/src/atoms/loginPortal.ts
Normal 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);
|
||||
}
|
||||
);
|
@@ -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);
|
||||
|
@@ -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,
|
||||
|
74
gpgui/src/atoms/passwordLogin.ts
Normal file
74
gpgui/src/atoms/passwordLogin.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
@@ -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,
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
59
gpgui/src/atoms/samlLogin.ts
Normal file
59
gpgui/src/atoms/samlLogin.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
});
|
98
gpgui/src/atoms/settings.ts
Normal file
98
gpgui/src/atoms/settings.ts
Normal 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();
|
||||
}
|
||||
});
|
@@ -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";
|
||||
});
|
||||
|
1
gpgui/src/atoms/unwrap.ts
Normal file
1
gpgui/src/atoms/unwrap.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { unstable_unwrap as unwrap } from "jotai/utils";
|
30
gpgui/src/atoms/vpn.ts
Normal file
30
gpgui/src/atoms/vpn.ts
Normal 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");
|
||||
}
|
||||
});
|
Reference in New Issue
Block a user