import { IdpRequests } from './classes.idprequests.js'; import * as plugins from './plugins.js'; export class IdpClient { // INSTANCE PRIVATE private helpers = { async extractDataFromJwtString(jwtString: string): Promise { return plugins.webjwt.getDataFromJwtString(jwtString); }, }; // INSTANCE PUBLIC public appData: plugins.lointReception.data.IApp; public rolesReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1); public organizationsReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1); public receptionTrUrl: string; constructor(receptionBaseUrlArg: string, appDataArg?: plugins.lointReception.data.IApp) { this.receptionTrUrl = receptionBaseUrlArg if (this.receptionTrUrl.endsWith('/')) { this.receptionTrUrl = this.receptionTrUrl.slice(0, -1); } if (!this.receptionTrUrl.endsWith('/typedrequest')) { this.receptionTrUrl = `${this.receptionTrUrl}/typedrequest`; } console.log(`reception client connecting to ${this.receptionTrUrl}`); if (!appDataArg) { appDataArg = { id: '', // TODO appUrl: `https://${window.location.host}/`, description: null, logoUrl: null, name: null, }; } this.appData = appDataArg; } public requests = new IdpRequests(this); /** * app data can be transferred when redirecting to the sso domain using query params * this message retrieves the app data when on the sso domain */ public async getAppDataOnSsoDomain() { if (!window.location.href.startsWith('https://sso.workspace.global/')) { console.error('You are trying to access SSO appData on a non sso domain.'); return null; } const appDataString = plugins.smarturl.Smarturl.createFromUrl(window.location.href).searchParams .appdata; if (!appDataString) { console.error('no appdata query arg detected'); return null; } const appData = plugins.smartjson.parseBase64(appDataString); return appData; } public async setJwt(jwtStringArg: string) { await this.storeJwt(jwtStringArg); } /** * a typedsocket for going reactive */ public typedsocket: plugins.typedsocket.TypedSocket; /** * a typed router to go reactive */ public typedrouter = new plugins.typedrequest.TypedRouter(); public statusObservable = new plugins.smartrx.rxjs.Subject(); public ssoStore = new plugins.webstore.WebStore({ storeName: 'wgsso', dbName: 'wgsso', }); public async storeJwt(jwtString: string) { await this.ssoStore.set('wgJwt', jwtString); } public async getJwt(): Promise { return await this.ssoStore.get('wgJwt'); } public async getJwtData(): Promise { return this.helpers.extractDataFromJwtString(await this.getJwt()); } public async deleteJwt() { await this.ssoStore.delete('wgJwt'); console.log('removed jwt'); } /** * performs jwt housekeeping * only call if jwt is present * @returns */ public async performJwtHousekeeping() { let jwt = await this.getJwt(); if (!jwt) { return null; } const extractedJwt = await this.helpers.extractDataFromJwtString(jwt); if (extractedJwt.data.refreshFrom < Date.now() && Date.now() < extractedJwt.data.validUntil) { jwt = await this.refreshJwt(); } else if (Date.now() > extractedJwt.data.validUntil) { this.deleteJwt(); } return jwt; } public async refreshJwt(refreshTokenArg?: string): Promise { let extractedJwt: plugins.lointReception.data.IJwt; if (!refreshTokenArg) { extractedJwt = await this.helpers.extractDataFromJwtString(await this.getJwt()); } const refreshJwtReq = new plugins.typedrequest.TypedRequest( `${this.receptionTrUrl}/typedrequest`, 'refreshJwt' ); const response = await refreshJwtReq.fire({ refreshToken: refreshTokenArg || extractedJwt.data.refreshToken, }); if (response.jwt) { await this.storeJwt(response.jwt); } else { await this.deleteJwt(); } this.statusObservable.next(response.status); return await this.getJwt(); } /** * can be used to switch between pages */ public async getTransferToken(appDataArg?: plugins.lointReception.data.IApp): Promise { const jwt = await this.performJwtHousekeeping(); const extractedJwt = await this.helpers.extractDataFromJwtString(jwt); const getTransferToken = new plugins.typedrequest.TypedRequest( `${this.receptionTrUrl}/typedrequest`, 'exchangeRefreshTokenAndTransferToken' ); const response = await getTransferToken.fire({ refreshToken: extractedJwt.data.refreshToken, appData: appDataArg || this.appData, }); return response.transferToken; } /** * gets a transfer token and switches to a location */ public async getTransferTokenAndSwitchToLocation(newLocationArg: string): Promise { const transferToken = await this.getTransferToken(); if (!transferToken) { alert('failed to get transfer token!'); } const urlInstance = plugins.smarturl.Smarturl.createFromUrl(newLocationArg, { searchParams: { transfertoken: transferToken, }, }); const transferUrl = urlInstance.toString(); window.location.href = transferUrl; return; } /** * processes a transfer token */ public async processTransferToken(): Promise { const href = window.location.href; const url = plugins.smarturl.Smarturl.createFromUrl(href); const transferToken = url.searchParams['transfertoken']; if (transferToken) { const getTransferToken = new plugins.typedrequest.TypedRequest( `${this.receptionTrUrl}/typedrequest`, 'exchangeRefreshTokenAndTransferToken' ); const response = await getTransferToken.fire({ transferToken, appData: this.appData, }); if (response.refreshToken) { await this.refreshJwt(response.refreshToken); } else { globalThis.alert?.('transfer token invalid'); return false; } return true; } else { return false; } } // Login Status stuff public async checkJwtPresent() { const jwt = await this.performJwtHousekeeping(); if (jwt) { return true; } else { return false; } } /** * forces the current user to login * @param requireLoginArg * @returns */ public async determineLoginStatus(requireLoginArg: boolean = false): Promise { const jwtPresent = await this.checkJwtPresent(); if (jwtPresent) { const jwt = await this.performJwtHousekeeping(); return !!jwt; } else { const transferTokenResult = await this.processTransferToken(); if (transferTokenResult) { // we are in the clear return true; } else { if (requireLoginArg) { const urlInstance = plugins.smarturl.Smarturl.createFromUrl( 'https://sso.workspace.global/', { searchParams: { appdata: plugins.smartjson.stringifyBase64(this.appData), action: 'login', }, } ); if (!globalThis.location.href.startsWith('https://sso.workspace.global/')) { globalThis.location.href = urlInstance.toString(); } } return false; } } } /** * logs out the current user */ public async logout() { const urlInstance = plugins.smarturl.Smarturl.createFromUrl('https://sso.workspace.global/', { searchParams: { appdata: plugins.smartjson.stringifyBase64(this.appData), action: 'logout', }, }); if (!globalThis.location.href.startsWith('https://sso.workspace.global/')) { // we are somewhere in an app await this.deleteJwt(); globalThis.location.href = urlInstance.toString(); } else { // we are in the sso page await this.enableTypedSocket(); console.log(`logging out against ${this.receptionTrUrl}`) const logoutTr = this.typedsocket.createTypedRequest( 'logout' ); await logoutTr.fire({ refreshToken: (await this.getJwtData()).data.refreshToken, }); await this.deleteJwt(); const appData = await this.getAppDataOnSsoDomain(); if (appData) { console.log(`redirecting to app after logout: ${appData.appUrl}`); window.location.href = appData.appUrl; } else { console.error('no appData provided. Not redirecting after logout.'); } } } public typedsocketDeferred = plugins.smartpromise.defer(); public async enableTypedSocket() { if (this.typedsocketDeferred.claimed) { return this.typedsocketDeferred.promise; } this.typedsocketDeferred.claim(); this.typedsocket = await plugins.typedsocket.TypedSocket.createClient( this.typedrouter, `${this.receptionTrUrl}/` ); this.typedsocketDeferred.resolve(this.typedsocket); return this.typedsocketDeferred.promise; } public async stop() { await this.typedsocket?.stop(); } // ================================== // Organization and Settings stuff // ================================== public async createOrganization( orgNameArg: string, orgSlugArg: string, modeArg: 'checkAvailability' | 'manifest' ) { await this.typedsocketDeferred.promise; const validateOrg = this.typedsocket.createTypedRequest( 'createOrganization' ); const response = await validateOrg.fire({ jwt: await this.getJwt(), action: modeArg, organizationName: orgNameArg, organizationSlug: orgSlugArg, userId: (await this.getJwtData()).id, }); return response; } /** * gets the current OrganizationRoles */ public async getRolesAndOrganizations() { await this.typedsocketDeferred.promise; const rolesAndOrganizationsForUserId = this.typedsocket.createTypedRequest( 'getRolesAndOrganizationsForUserId' ); const response = await rolesAndOrganizationsForUserId.fire({ jwt: await this.getJwt(), userId: (await this.getJwtData()).id, }); return response; } /** * updates the PaddleCheckoutId for an organization. */ public async updatePaddleCheckoutId(orgIdArg: string, checkoutIdArg: string) { await this.typedsocketDeferred.promise; const updateBillingPlan = this.typedsocket.createTypedRequest( 'updatePaymentMethod' ); const response = await updateBillingPlan.fire({ jwtString: await this.getJwt(), orgId: orgIdArg, paddle: { checkoutId: checkoutIdArg, }, }); return response; } }