mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-05-20 07:26:58 -04:00
refactor: refactor UI using jotai
This commit is contained in:
56
gpgui/src/atoms/gateway.ts
Normal file
56
gpgui/src/atoms/gateway.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { atom } from "jotai";
|
||||
import gatewayService from "../services/gatewayService";
|
||||
import vpnService from "../services/vpnService";
|
||||
import { notifyErrorAtom } from "./notification";
|
||||
import { isProcessingAtom, statusAtom } from "./status";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
if (!get(isProcessingAtom)) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const disconnectVpnAtom = atom(null, async (get, set) => {
|
||||
try {
|
||||
set(statusAtom, "disconnecting");
|
||||
await vpnService.disconnect();
|
||||
set(statusAtom, "disconnected");
|
||||
} catch (err) {
|
||||
set(statusAtom, "disconnected");
|
||||
set(notifyErrorAtom, "Failed to disconnect from VPN");
|
||||
}
|
||||
});
|
36
gpgui/src/atoms/notification.ts
Normal file
36
gpgui/src/atoms/notification.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { AlertColor } from "@mui/material";
|
||||
import { atom } from "jotai";
|
||||
|
||||
export type Severity = AlertColor;
|
||||
|
||||
const notificationVisibleAtom = atom(false);
|
||||
export const notificationConfigAtom = atom({
|
||||
title: "",
|
||||
message: "",
|
||||
severity: "info" as Severity,
|
||||
});
|
||||
|
||||
export const closeNotificationAtom = atom(
|
||||
(get) => get(notificationVisibleAtom),
|
||||
(_get, set) => {
|
||||
set(notificationVisibleAtom, false);
|
||||
}
|
||||
);
|
||||
|
||||
export const notifyErrorAtom = atom(null, (_get, set, err: unknown) => {
|
||||
let msg: string;
|
||||
if (err instanceof Error) {
|
||||
msg = err.message;
|
||||
} else if (typeof err === "string") {
|
||||
msg = err;
|
||||
} else {
|
||||
msg = "Unknown error";
|
||||
}
|
||||
|
||||
set(notificationVisibleAtom, true);
|
||||
set(notificationConfigAtom, {
|
||||
title: "Error",
|
||||
message: msg,
|
||||
severity: "error",
|
||||
});
|
||||
});
|
225
gpgui/src/atoms/portal.ts
Normal file
225
gpgui/src/atoms/portal.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { atom } from "jotai";
|
||||
import { focusAtom } from "jotai-optics";
|
||||
import authService, { AuthData } from "../services/authService";
|
||||
import portalService, {
|
||||
PasswordPrelogin,
|
||||
Prelogin,
|
||||
SamlPrelogin,
|
||||
} from "../services/portalService";
|
||||
import { gatewayLoginAtom } from "./gateway";
|
||||
import { notifyErrorAtom } from "./notification";
|
||||
import { isProcessingAtom, statusAtom } from "./status";
|
||||
|
||||
type GatewayData = {
|
||||
name: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
type Credential = {
|
||||
user: string;
|
||||
passwd: string;
|
||||
userAuthCookie: string;
|
||||
prelogonUserAuthCookie: string;
|
||||
};
|
||||
|
||||
type AppData = {
|
||||
portal: string;
|
||||
gateways: GatewayData[];
|
||||
selectedGateway: string;
|
||||
credentials: Record<string, Credential>;
|
||||
};
|
||||
|
||||
const appAtom = atom<AppData>({
|
||||
portal: "",
|
||||
gateways: [],
|
||||
selectedGateway: "",
|
||||
credentials: {},
|
||||
});
|
||||
|
||||
export const portalAtom = focusAtom(appAtom, (optic) => optic.prop("portal"));
|
||||
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(portalAtom);
|
||||
if (!portal) {
|
||||
set(notifyErrorAtom, "Portal is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
set(statusAtom, "prelogin");
|
||||
const prelogin = await portalService.prelogin(portal);
|
||||
if (!get(isProcessingAtom)) {
|
||||
console.info("Request cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
};
|
||||
|
||||
export const passwordPreloginAtom = atom<PasswordPrelogin>({
|
||||
isSamlAuth: false,
|
||||
region: "",
|
||||
authMessage: "",
|
||||
labelUsername: "",
|
||||
labelPassword: "",
|
||||
});
|
||||
|
||||
export const cancelConnectPortalAtom = atom(null, (_get, set) => {
|
||||
set(statusAtom, "disconnected");
|
||||
});
|
||||
|
||||
export const usernameAtom = atom("");
|
||||
export const passwordAtom = atom("");
|
||||
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(portalAtom);
|
||||
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");
|
||||
authData = await authService.samlLogin(samlAuthMethod, samlRequest);
|
||||
} catch (err) {
|
||||
throw new Error("SAML login failed");
|
||||
}
|
||||
|
||||
if (!authData) {
|
||||
// User closed the SAML login window, cancel the login
|
||||
set(cancelConnectPortalAtom);
|
||||
return;
|
||||
}
|
||||
|
||||
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(portalAtom);
|
||||
const prelogin = await portalService.prelogin(portal);
|
||||
if (prelogin.isSamlAuth) {
|
||||
await authService.emitAuthRequest({
|
||||
samlBinding: prelogin.samlAuthMethod,
|
||||
samlRequest: prelogin.samlRequest,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type PortalCredential =
|
||||
| {
|
||||
user: string;
|
||||
passwd: string;
|
||||
}
|
||||
| {
|
||||
user: string;
|
||||
"prelogin-cookie": string | null;
|
||||
"portal-userauthcookie": string | null;
|
||||
};
|
||||
|
||||
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 portal = get(portalAtom);
|
||||
let portalConfig;
|
||||
try {
|
||||
portalConfig = await portalService.fetchConfig(portal, credential);
|
||||
// Ensure the password auth window is closed
|
||||
set(passwordAuthVisibleAtom, false);
|
||||
} finally {
|
||||
set(portalConfigLoadingAtom, false);
|
||||
}
|
||||
|
||||
if (!get(isProcessingAtom)) {
|
||||
console.info("Request cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
const { gateways, userAuthCookie, prelogonUserAuthCookie } = portalConfig;
|
||||
console.info("portalConfig", portalConfig);
|
||||
if (!gateways.length) {
|
||||
throw new Error("No gateway found");
|
||||
}
|
||||
|
||||
const { region } = prelogin;
|
||||
const { address } = portalService.preferredGateway(gateways, region);
|
||||
await set(gatewayLoginAtom, address, {
|
||||
user: credential.user,
|
||||
userAuthCookie,
|
||||
prelogonUserAuthCookie,
|
||||
});
|
||||
}
|
||||
);
|
44
gpgui/src/atoms/status.ts
Normal file
44
gpgui/src/atoms/status.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { atom } from "jotai";
|
||||
import vpnService from "../services/vpnService";
|
||||
|
||||
export type Status =
|
||||
| "disconnected"
|
||||
| "prelogin"
|
||||
| "authenticating-saml"
|
||||
| "authenticating-password"
|
||||
| "portal-config"
|
||||
| "gateway-login"
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "disconnecting"
|
||||
| "error";
|
||||
|
||||
export const statusAtom = atom<Status>("disconnected");
|
||||
statusAtom.onMount = (setAtom) => {
|
||||
return vpnService.onStatusChanged((status) => {
|
||||
status === "connected" && setAtom("connected");
|
||||
});
|
||||
};
|
||||
|
||||
const statusTextMap: Record<Status, String> = {
|
||||
disconnected: "Not Connected",
|
||||
prelogin: "Portal pre-logging in...",
|
||||
"authenticating-saml": "Authenticating...",
|
||||
"authenticating-password": "Authenticating...",
|
||||
"portal-config": "Retrieving portal config...",
|
||||
"gateway-login": "Logging in to gateway...",
|
||||
connecting: "Connecting...",
|
||||
connected: "Connected",
|
||||
disconnecting: "Disconnecting...",
|
||||
error: "Error",
|
||||
};
|
||||
|
||||
export const statusTextAtom = atom((get) => {
|
||||
const status = get(statusAtom);
|
||||
return statusTextMap[status];
|
||||
});
|
||||
|
||||
export const isProcessingAtom = atom((get) => {
|
||||
const status = get(statusAtom);
|
||||
return status !== "disconnected" && status !== "connected";
|
||||
});
|
Reference in New Issue
Block a user