@etsoo/materialui
Version:
TypeScript Material-UI Implementation
326 lines (274 loc) • 8.3 kB
text/typescript
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);
}
}