UNPKG

@etsoo/materialui

Version:

TypeScript Material-UI Implementation

326 lines (274 loc) 8.3 kB
import { ApiRefreshTokenDto, AppLoginParams, AppTryLoginParams, AuthApi, BridgeUtils, ExternalEndpoint, IApi, IApiPayload, TokenAuthRQ } from "@etsoo/appscript"; import { IServiceApp } from "./IServiceApp"; import { IServiceAppSettings } from "./IServiceAppSettings"; import { IServiceUser, ServiceUserToken } from "./IServiceUser"; import { ReactApp } from "./ReactApp"; import { IActionResult } from "@etsoo/shared"; const coreTokenKey = "core-refresh-token"; const tryLoginKey = "tryLogin"; /** * Core Service App * Service login to core system, get the refesh token and access token * Use the acess token to the service api, get a service access token * Use the new acess token and refresh token to login */ export class ServiceApp< U extends IServiceUser = IServiceUser, S extends IServiceAppSettings = IServiceAppSettings > extends ReactApp<S, U> implements IServiceApp { /** * Core endpoint */ protected coreEndpoint: ExternalEndpoint; /** * Core system API */ readonly coreApi: IApi; /** * Core system origin */ readonly coreOrigin: string; private coreAccessToken: string | undefined; /** * Constructor * @param settings Settings * @param name Application name * @param debug Debug mode */ constructor(settings: S, name: string, debug: boolean = false) { super(settings, name, debug); // Custom core API name can be done with override this.coreName const coreEndpoint = this.settings.endpoints?.[this.coreName]; if (coreEndpoint == null) { throw new Error("Core API endpont is required."); } this.coreEndpoint = coreEndpoint; this.coreOrigin = new URL(coreEndpoint.webUrl).origin; this.coreApi = this.createApi(this.coreName, coreEndpoint); this.keepLogin = true; } /** * Get token authorization request data * @param api API, if not provided, use the core API * @returns Result */ getTokenAuthRQ(api?: IApi): TokenAuthRQ { api ??= this.coreApi; const auth = api.getAuthorization(); if (auth == null) { throw new Error("Authorization is required."); } return { accessToken: auth.token, tokenScheme: auth.scheme }; } /** * Load core system UI * @param tryLogin Try login or not */ loadCore(tryLogin: boolean = false) { if (BridgeUtils.host == null) { let url = this.coreEndpoint.webUrl; if (!tryLogin) url = url.addUrlParam(tryLoginKey, tryLogin); globalThis.location.href = url; } else { const startUrl = tryLogin ? undefined : "".addUrlParam(tryLoginKey, tryLogin); BridgeUtils.host.loadApp(this.coreName, startUrl); } } /** * Go to the login page * @param data Login parameters */ override toLoginPage(data?: AppLoginParams) { // Destruct const { removeUrl, showLoading, params } = data ?? {}; // Cache current URL this.cachedUrl = removeUrl ? undefined : globalThis.location.href; // Get the redirect URL new AuthApi(this).getLogInUrl().then((url) => { if (!url) return; // Add try login flag if (params != null) { url = url.addUrlParams(params); } this.loadUrlEx(url); }); // Make sure apply new device id for new login this.clearDeviceId(); } /** * Load URL with core origin * @param url URL */ loadUrlEx(url: string) { super.loadUrl(url, this.coreOrigin); } /** * Signout, with userLogout and toLoginPage * @param action Callback */ override signout(action?: () => void | boolean) { // Clear core token this.storage.setData(coreTokenKey, undefined); // Super call return super.signout(action); } /** * * @param user Current user * @param core Core system API token data * @param keep Keep in local storage or not * @param dispatch User state dispatch */ userLoginEx( user: U & ServiceUserToken, core?: ApiRefreshTokenDto, dispatch?: boolean ) { if (user.clientDeviceId && user.passphrase) { // Save the passphrase // Interpolated string expressions are different between TypeScript and C# for the null value const passphrase = this.decrypt( user.passphrase, `${user.uid ?? ""}-${this.settings.appId}` ); if (passphrase) { this.deviceId = user.clientDeviceId; this.updatePassphrase(passphrase); } } // User login const { refreshToken } = user; // Core system login // It's the extreme case, the core system token should not be the same with the current app user token core ??= { refreshToken, accessToken: user.token, tokenType: user.tokenScheme ?? "Bearer", expiresIn: user.seconds }; // Cache the core system data this.saveCoreToken(core); // User login and trigger the dispatch at last this.userLogin(user, refreshToken, dispatch); } /** * Save core system data * @param data Data */ protected saveCoreToken(data: ApiRefreshTokenDto) { // Hold the core system access token this.coreAccessToken = data.accessToken; // Cache the core system refresh token this.storage.setData(coreTokenKey, this.encrypt(data.refreshToken)); // Exchange tokens this.exchangeTokenAll(data); } /** * On switch organization handler * This method is called when the organization is switched successfully */ protected onSwitchOrg(): Promise<void> | void {} /** * Switch organization * @param organizationId Organization ID * @param fromOrganizationId From organization ID * @param payload Payload */ async switchOrg( organizationId: number, fromOrganizationId?: number, payload?: IApiPayload<IActionResult<U & ServiceUserToken>> ) { if (!this.coreAccessToken) { throw new Error("Core access token is required to switch organization."); } const [result, refreshToken] = await new AuthApi(this).switchOrg( { organizationId, fromOrganizationId, token: this.coreAccessToken }, payload ); if (result == null) return; if (!result.ok) { return result; } if (result.data == null) { throw new Error("Invalid switch organization result."); } let core: ApiRefreshTokenDto | undefined; if ("core" in result.data && typeof result.data.core === "string") { core = JSON.parse(result.data.core); delete result.data.core; } // Override the user data's refresh token const user = refreshToken ? { ...result.data, refreshToken } : result.data; // User login without dispatch this.userLoginEx(user, core, false); // Handle the switch organization await this.onSwitchOrg(); // Trigger the dispatch at last this.doLoginDispatch(user); return result; } protected override async refreshTokenSucceed( user: U, token: string, callback?: (result?: boolean | IActionResult) => boolean | void ): Promise<void> { // Check core system token const coreToken = this.storage.getData<string>(coreTokenKey); if (!coreToken) { callback?.({ ok: false, type: "noData", title: "Core token is blank" }); return; } const coreTokenDecrypted = this.decrypt(coreToken); if (!coreTokenDecrypted) { callback?.({ ok: false, type: "noData", title: "Core token decrypted is blank" }); return; } // Call the core system API refresh token const data = await this.apiRefreshTokenData(this.coreApi, { token: coreTokenDecrypted, appId: this.settings.appId }); if (data == null) return; // Cache the core system refresh token // Follow similar logic in userLoginEx this.saveCoreToken(data); // Call the super await super.refreshTokenSucceed(user, token, callback); } /** * Try login * @param params Login parameters */ override async tryLogin(params?: AppTryLoginParams) { // Destruct params ??= {}; let { onFailure, ...rest } = params; if (onFailure == null) { onFailure = params.onFailure = (type) => { console.log(`Try login failed: ${type}.`); this.toLoginPage(rest); }; } return await super.tryLogin(params); } }