// // Copyright © 2024 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. // import { type AccountRole, BackupStatus, Data, type Person, type PersonUuid, type PersonInfo, SocialId, Version, type WorkspaceInfoWithStatus, type WorkspaceMemberInfo, WorkspaceMode, concatLink, type WorkspaceUserOperation, WorkspaceUuid } from '@hcengineering/core' import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' import type { LoginInfo, OtpInfo, WorkspaceLoginInfo, RegionInfo, WorkspaceOperation } from './types' /** @public */ export interface AccountClient { // Static methods getProviders: () => Promise // RPC getUserWorkspaces: () => Promise selectWorkspace: ( workspaceUrl: string, kind?: 'external' | 'internal' | 'byregion', externalRegions?: string[] ) => Promise validateOtp: (email: string, code: string) => Promise loginOtp: (email: string) => Promise getLoginInfoByToken: () => Promise restorePassword: (password: string) => Promise confirm: () => Promise requestPasswordReset: (email: string) => Promise sendInvite: (email: string, role: AccountRole) => Promise resendInvite: (email: string, role: AccountRole) => Promise leaveWorkspace: (account: string) => Promise changeUsername: (first: string, last: string) => Promise changePassword: (oldPassword: string, newPassword: string) => Promise signUpJoin: ( email: string, password: string, first: string, last: string, inviteId: string ) => Promise join: (email: string, password: string, inviteId: string) => Promise createInviteLink: ( exp: number, emailMask: string, limit: number, role: AccountRole, personId?: any ) => Promise checkJoin: (inviteId: string) => Promise getWorkspaceInfo: (updateLastVisit?: boolean) => Promise getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise getRegionInfo: () => Promise createWorkspace: (name: string, region?: string) => Promise signUpOtp: (email: string, first: string, last: string) => Promise signUp: (email: string, password: string, first: string, last: string) => Promise login: (email: string, password: string) => Promise getPerson: () => Promise getPersonInfo: (account: PersonUuid) => Promise getSocialIds: () => Promise getWorkspaceMembers: () => Promise updateWorkspaceRole: (account: string, role: AccountRole) => Promise updateWorkspaceName: (name: string) => Promise deleteWorkspace: () => Promise findPerson: (socialString: string) => Promise // Service methods workerHandshake: (region: string, version: Data, operation: WorkspaceOperation) => Promise getPendingWorkspace: ( region: string, version: Data, operation: WorkspaceOperation ) => Promise updateWorkspaceInfo: ( wsUuid: string, event: string, version: Data, progress: number, message?: string ) => Promise listWorkspaces: (region?: string | null, mode?: WorkspaceMode | null) => Promise performWorkspaceOperation: ( workspaceId: string | string[], event: WorkspaceUserOperation, ...params: any ) => Promise assignWorkspace: (email: string, workspaceUuid: string, role: AccountRole) => Promise updateBackupInfo: (info: BackupStatus) => Promise updateWorkspaceRoleBySocialId: (socialKey: string, targetRole: AccountRole) => Promise setCookie: () => Promise deleteCookie: () => Promise } /** @public */ export function getClient (accountsUrl?: string, token?: string): AccountClient { if (accountsUrl === undefined) { throw new Error('Accounts url not specified') } return new AccountClientImpl(accountsUrl, token) } interface Request { method: string params: Record } class AccountClientImpl implements AccountClient { private readonly request: RequestInit constructor ( private readonly url: string, private readonly token?: string ) { if (url === '') { throw new Error('Accounts url not specified') } this.request = { keepalive: true, headers: { ...(this.token === undefined ? {} : { Authorization: 'Bearer ' + this.token }) }, credentials: 'include' } } async getProviders (): Promise { return await retry(5, async () => { const response = await fetch(concatLink(this.url, '/providers')) return await response.json() }) } private async rpc(request: Request): Promise { const response = await fetch(this.url, { ...this.request, headers: { ...this.request.headers, 'Content-Type': 'application/json' }, method: 'POST', body: JSON.stringify(request) }) const result = await response.json() if (result.error != null) { throw new PlatformError(result.error) } return result.result } private flattenStatus (ws: any): WorkspaceInfoWithStatus { if (ws === undefined) { throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, {})) } const status = ws.status if (status === undefined) { return ws } const result = { ...ws, ...status } delete result.status return result } async getUserWorkspaces (): Promise { const request = { method: 'getUserWorkspaces' as const, params: {} } return (await this.rpc(request)).map((ws) => this.flattenStatus(ws)) } async selectWorkspace ( workspaceUrl: string, kind: 'external' | 'internal' | 'byregion' = 'external', externalRegions: string[] = [] ): Promise { const request = { method: 'selectWorkspace' as const, params: { workspaceUrl, kind, externalRegions } } return await this.rpc(request) } async validateOtp (email: string, code: string): Promise { const request = { method: 'validateOtp' as const, params: { email, code } } return await this.rpc(request) } async loginOtp (email: string): Promise { const request = { method: 'loginOtp' as const, params: { email } } return await this.rpc(request) } async getLoginInfoByToken (): Promise { const request = { method: 'getLoginInfoByToken' as const, params: {} } return await this.rpc(request) } async restorePassword (password: string): Promise { const request = { method: 'restorePassword' as const, params: { password } } return await this.rpc(request) } async confirm (): Promise { const request = { method: 'confirm' as const, params: {} } return await this.rpc(request) } async requestPasswordReset (email: string): Promise { const request = { method: 'requestPasswordReset' as const, params: { email } } await this.rpc(request) } async sendInvite (email: string, role: AccountRole): Promise { const request = { method: 'sendInvite' as const, params: { email, role } } await this.rpc(request) } async resendInvite (email: string, role: AccountRole): Promise { const request = { method: 'resendInvite' as const, params: [email, role] } await this.rpc(request) } async leaveWorkspace (account: string): Promise { const request = { method: 'leaveWorkspace' as const, params: { account } } return await this.rpc(request) } async changeUsername (first: string, last: string): Promise { const request = { method: 'changeUsername' as const, params: { first, last } } await this.rpc(request) } async changePassword (oldPassword: string, newPassword: string): Promise { const request = { method: 'changePassword' as const, params: { oldPassword, newPassword } } await this.rpc(request) } async signUpJoin ( email: string, password: string, first: string, last: string, inviteId: string ): Promise { const request = { method: 'signUpJoin' as const, params: { email, password, first, last, inviteId } } return await this.rpc(request) } async join (email: string, password: string, inviteId: string): Promise { const request = { method: 'join' as const, params: { email, password, inviteId } } return await this.rpc(request) } async createInviteLink ( exp: number, emailMask: string, limit: number, role: AccountRole, personId?: any ): Promise { const request = { method: 'createInviteLink' as const, params: { exp, emailMask, limit, role, personId } } return await this.rpc(request) } async checkJoin (inviteId: string): Promise { const request = { method: 'checkJoin' as const, params: { inviteId } } return await this.rpc(request) } async getWorkspacesInfo (ids: WorkspaceUuid[]): Promise { const request = { method: 'getWorkspacesInfo' as const, params: { ids } } return await this.rpc(request) } async getWorkspaceInfo (updateLastVisit: boolean = false): Promise { const request = { method: 'getWorkspaceInfo' as const, params: updateLastVisit ? { updateLastVisit: true } : {} } return this.flattenStatus(await this.rpc(request)) } async getRegionInfo (): Promise { const request = { method: 'getRegionInfo' as const, params: {} } return await this.rpc(request) } async createWorkspace (workspaceName: string, region?: string): Promise { const request = { method: 'createWorkspace' as const, params: { workspaceName, region } } return await this.rpc(request) } async signUpOtp (email: string, firstName: string, lastName: string): Promise { const request = { method: 'signUpOtp' as const, params: { email, firstName, lastName } } return await this.rpc(request) } async signUp (email: string, password: string, firstName: string, lastName: string): Promise { const request = { method: 'signUp' as const, params: { email, password, firstName, lastName } } return await this.rpc(request) } async login (email: string, password: string): Promise { const request = { method: 'login' as const, params: { email, password } } return await this.rpc(request) } async getPerson (): Promise { const request = { method: 'getPerson' as const, params: {} } return await this.rpc(request) } async getPersonInfo (account: PersonUuid): Promise { const request = { method: 'getPersonInfo' as const, params: { account } } return await this.rpc(request) } async getSocialIds (): Promise { const request = { method: 'getSocialIds' as const, params: {} } return await this.rpc(request) } async workerHandshake (region: string, version: Data, operation: WorkspaceOperation): Promise { const request = { method: 'workerHandshake' as const, params: { region, version, operation } } await this.rpc(request) } async getPendingWorkspace ( region: string, version: Data, operation: WorkspaceOperation ): Promise { const request = { method: 'getPendingWorkspace' as const, params: { region, version, operation } } const result = await this.rpc(request) if (result == null) { return null } return this.flattenStatus(result) } async updateWorkspaceInfo ( workspaceUuid: string, event: string, version: Data, progress: number, message?: string ): Promise { const request = { method: 'updateWorkspaceInfo' as const, params: { workspaceUuid, event, version, progress, message } } await this.rpc(request) } async getWorkspaceMembers (): Promise { const request = { method: 'getWorkspaceMembers' as const, params: {} } return await this.rpc(request) } async updateWorkspaceRole (targetAccount: string, targetRole: AccountRole): Promise { const request = { method: 'updateWorkspaceRole' as const, params: { targetAccount, targetRole } } await this.rpc(request) } async updateWorkspaceName (name: string): Promise { const request = { method: 'updateWorkspaceName' as const, params: { name } } await this.rpc(request) } async deleteWorkspace (): Promise { const request = { method: 'deleteWorkspace' as const, params: {} } await this.rpc(request) } async findPerson (socialString: string): Promise { const request = { method: 'findPerson' as const, params: { socialString } } return await this.rpc(request) } async listWorkspaces (region?: string | null, mode: WorkspaceMode | null = null): Promise { const request = { method: 'listWorkspaces' as const, params: { region, mode } } return ((await this.rpc(request)) ?? []).map((ws) => this.flattenStatus(ws)) } async performWorkspaceOperation ( workspaceId: string | string[], event: WorkspaceUserOperation, ...params: any ): Promise { const request = { method: 'performWorkspaceOperation' as const, params: { workspaceId, event, params } } return await this.rpc(request) } async updateBackupInfo (backupInfo: BackupStatus): Promise { const request = { method: 'updateBackupInfo' as const, params: { backupInfo } } await this.rpc(request) } async assignWorkspace (email: string, workspaceUuid: string, role: AccountRole): Promise { const request = { method: 'assignWorkspace' as const, params: { email, workspaceUuid, role } } await this.rpc(request) } async updateWorkspaceRoleBySocialId (socialKey: string, targetRole: AccountRole): Promise { const request = { method: 'updateWorkspaceRoleBySocialId' as const, params: { socialKey, targetRole } } await this.rpc(request) } async setCookie (): Promise { const url = concatLink(this.url, '/cookie') const response = await fetch(url, { ...this.request, method: 'PUT' }) if (!response.ok) { const result = await response.json() if (result.error != null) { throw new PlatformError(result.error) } } } async deleteCookie (): Promise { const url = concatLink(this.url, '/cookie') const response = await fetch(url, { ...this.request, method: 'DELETE' }) if (!response.ok) { const result = await response.json() if (result.error != null) { throw new PlatformError(result.error) } } } } async function retry (retries: number, op: () => Promise, delay: number = 100): Promise { let error: any while (retries > 0) { retries-- try { return await op() } catch (err: any) { error = err if (retries !== 0) { await new Promise((resolve) => setTimeout(resolve, delay)) } } } throw error }