import { atom } from "jotai"; import portalService, { PortalConfig, PortalCredential, Prelogin, } from "../services/portalService"; import logger from "../utils/logger"; import { redact } from "../utils/redact"; import { selectedGatewayAtom } from "./gateway"; import { loginGatewayAtom } from "./loginGateway"; import { portalAddressAtom, updatePortalDataAtom } from "./portal"; import { isProcessingAtom, statusAtom } from "./status"; /** * The error thrown when the portal config is abnormal, can back to normal after retrying */ export class AbnormalPortalConfigError extends Error { constructor(message: string) { super(message); this.name = "AbnormalPortalConfigError"; } } // 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?.(); } catch (err) { const isProcessing = get(isProcessingAtom); if (!isProcessing) { logger.info("Operation cancelled"); return; } throw err; } finally { set(portalConfigLoadingAtom, false); } const isProcessing = get(isProcessingAtom); if (!isProcessing) { logger.info("Operation cancelled"); return; } const { gateways, userAuthCookie, prelogonUserAuthCookie } = portalConfig; if (!gateways.length) { throw new AbnormalPortalConfigError("No gateway found"); } if (userAuthCookie === "empty" || prelogonUserAuthCookie === "empty") { throw new AbnormalPortalConfigError("Empty user auth cookie"); } // Here, we have got the portal config successfully, refresh the cached portal data const previousSelectedGateway = get(selectedGatewayAtom)?.name; 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, }); logger.info(`Found the preferred gateway: ${name} (${redact(address)})`); // 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); } );