platform/packages/account-client/src/client.ts
Artyom Savchenko 163cca55da
UBERF-9734: Set default account timezone (#8469)
Signed-off-by: Artem Savchenko <armisav@gmail.com>
2025-04-07 14:28:55 +07:00

901 lines
24 KiB
TypeScript

//
// 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,
type AccountInfo,
BackupStatus,
Data,
type Person,
type PersonUuid,
type PersonInfo,
SocialId,
Version,
type WorkspaceInfoWithStatus,
type WorkspaceMemberInfo,
WorkspaceMode,
concatLink,
type WorkspaceUserOperation,
type WorkspaceUuid,
type PersonId,
type SocialIdType
} from '@hcengineering/core'
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
import type {
LoginInfo,
MailboxOptions,
OtpInfo,
WorkspaceLoginInfo,
RegionInfo,
WorkspaceOperation,
MailboxInfo,
Integration,
IntegrationKey,
IntegrationSecret,
IntegrationSecretKey
} from './types'
import { getClientTimezone } from './utils'
/** @public */
export interface AccountClient {
// Static methods
getProviders: () => Promise<string[]>
// RPC
getUserWorkspaces: () => Promise<WorkspaceInfoWithStatus[]>
selectWorkspace: (
workspaceUrl: string,
kind?: 'external' | 'internal' | 'byregion',
externalRegions?: string[]
) => Promise<WorkspaceLoginInfo>
validateOtp: (email: string, code: string) => Promise<LoginInfo>
loginOtp: (email: string) => Promise<OtpInfo>
getLoginInfoByToken: () => Promise<LoginInfo | WorkspaceLoginInfo>
restorePassword: (password: string) => Promise<LoginInfo>
confirm: () => Promise<LoginInfo>
requestPasswordReset: (email: string) => Promise<void>
sendInvite: (email: string, role: AccountRole) => Promise<void>
resendInvite: (email: string, role: AccountRole) => Promise<void>
createInviteLink: (
email: string,
role: AccountRole,
autoJoin: boolean,
firstName: string,
lastName: string,
navigateUrl?: string,
expHours?: number
) => Promise<string>
leaveWorkspace: (account: string) => Promise<LoginInfo | null>
changeUsername: (first: string, last: string) => Promise<void>
changePassword: (oldPassword: string, newPassword: string) => Promise<void>
signUpJoin: (
email: string,
password: string,
first: string,
last: string,
inviteId: string
) => Promise<WorkspaceLoginInfo>
join: (email: string, password: string, inviteId: string) => Promise<WorkspaceLoginInfo>
createInvite: (exp: number, emailMask: string, limit: number, role: AccountRole) => Promise<string>
checkJoin: (inviteId: string) => Promise<WorkspaceLoginInfo>
checkAutoJoin: (inviteId: string, firstName?: string, lastName?: string) => Promise<WorkspaceLoginInfo>
getWorkspaceInfo: (updateLastVisit?: boolean) => Promise<WorkspaceInfoWithStatus>
getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise<WorkspaceInfoWithStatus[]>
getRegionInfo: () => Promise<RegionInfo[]>
createWorkspace: (name: string, region?: string) => Promise<WorkspaceLoginInfo>
signUpOtp: (email: string, first: string, last: string) => Promise<OtpInfo>
signUp: (email: string, password: string, first: string, last: string) => Promise<LoginInfo>
login: (email: string, password: string) => Promise<LoginInfo>
getPerson: () => Promise<Person>
getPersonInfo: (account: PersonUuid) => Promise<PersonInfo>
getSocialIds: () => Promise<SocialId[]>
getWorkspaceMembers: () => Promise<WorkspaceMemberInfo[]>
updateWorkspaceRole: (account: string, role: AccountRole) => Promise<void>
updateWorkspaceName: (name: string) => Promise<void>
deleteWorkspace: () => Promise<void>
findPersonBySocialKey: (socialKey: string, requireAccount?: boolean) => Promise<PersonUuid | undefined>
findPersonBySocialId: (socialId: PersonId, requireAccount?: boolean) => Promise<PersonUuid | undefined>
findSocialIdBySocialKey: (socialKey: string) => Promise<PersonId | undefined>
getMailboxOptions: () => Promise<MailboxOptions>
createMailbox: (name: string, domain: string) => Promise<{ mailbox: string, socialId: PersonId }>
getMailboxes: () => Promise<MailboxInfo[]>
deleteMailbox: (mailbox: string) => Promise<void>
// Service methods
workerHandshake: (region: string, version: Data<Version>, operation: WorkspaceOperation) => Promise<void>
getPendingWorkspace: (
region: string,
version: Data<Version>,
operation: WorkspaceOperation
) => Promise<WorkspaceInfoWithStatus | null>
updateWorkspaceInfo: (
wsUuid: string,
event: string,
version: Data<Version>,
progress: number,
message?: string
) => Promise<void>
listWorkspaces: (region?: string | null, mode?: WorkspaceMode | null) => Promise<WorkspaceInfoWithStatus[]>
performWorkspaceOperation: (
workspaceId: string | string[],
event: WorkspaceUserOperation,
...params: any
) => Promise<boolean>
assignWorkspace: (email: string, workspaceUuid: string, role: AccountRole) => Promise<void>
updateBackupInfo: (info: BackupStatus) => Promise<void>
updateWorkspaceRoleBySocialKey: (socialKey: string, targetRole: AccountRole) => Promise<void>
ensurePerson: (
socialType: SocialIdType,
socialValue: string,
firstName: string,
lastName: string
) => Promise<{ uuid: PersonUuid, socialId: PersonId }>
addSocialIdToPerson: (person: PersonUuid, type: SocialIdType, value: string, confirmed: boolean) => Promise<PersonId>
createIntegration: (integration: Integration) => Promise<void>
updateIntegration: (integration: Integration) => Promise<void>
deleteIntegration: (integrationKey: IntegrationKey) => Promise<void>
getIntegration: (integrationKey: IntegrationKey) => Promise<Integration | null>
listIntegrations: (filter: {
socialId?: PersonId
kind?: string
workspaceUuid?: WorkspaceUuid | null
}) => Promise<Integration[]>
addIntegrationSecret: (integrationSecret: IntegrationSecret) => Promise<void>
updateIntegrationSecret: (integrationSecret: IntegrationSecret) => Promise<void>
deleteIntegrationSecret: (integrationSecretKey: IntegrationSecretKey) => Promise<void>
getIntegrationSecret: (integrationSecretKey: IntegrationSecretKey) => Promise<IntegrationSecret | null>
listIntegrationsSecrets: (filter: {
socialId?: PersonId
kind?: string
workspaceUuid?: WorkspaceUuid | null
key?: string
}) => Promise<IntegrationSecret[]>
getAccountInfo: (uuid: PersonUuid) => Promise<AccountInfo>
setCookie: () => Promise<void>
deleteCookie: () => Promise<void>
}
/** @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<string, any>
}
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')
}
const isBrowser = typeof window !== 'undefined'
this.request = {
keepalive: true,
headers: {
...(this.token === undefined
? {}
: {
Authorization: 'Bearer ' + this.token
})
},
...(isBrowser ? { credentials: 'include' } : {})
}
}
async getProviders (): Promise<string[]> {
return await retry(5, async () => {
const response = await fetch(concatLink(this.url, '/providers'))
return await response.json()
})
}
private async rpc<T>(request: Request): Promise<T> {
const timezone = getClientTimezone()
const meta: Record<string, string> = timezone !== undefined ? { 'X-Timezone': timezone } : {}
const response = await fetch(this.url, {
...this.request,
headers: {
...this.request.headers,
'Content-Type': 'application/json',
...meta
},
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<WorkspaceInfoWithStatus[]> {
const request = {
method: 'getUserWorkspaces' as const,
params: {}
}
return (await this.rpc<any[]>(request)).map((ws) => this.flattenStatus(ws))
}
async selectWorkspace (
workspaceUrl: string,
kind: 'external' | 'internal' | 'byregion' = 'external',
externalRegions: string[] = []
): Promise<WorkspaceLoginInfo> {
const request = {
method: 'selectWorkspace' as const,
params: { workspaceUrl, kind, externalRegions }
}
return await this.rpc(request)
}
async validateOtp (email: string, code: string): Promise<LoginInfo> {
const request = {
method: 'validateOtp' as const,
params: { email, code }
}
return await this.rpc(request)
}
async loginOtp (email: string): Promise<OtpInfo> {
const request = {
method: 'loginOtp' as const,
params: { email }
}
return await this.rpc(request)
}
async getLoginInfoByToken (): Promise<LoginInfo | WorkspaceLoginInfo> {
const request = {
method: 'getLoginInfoByToken' as const,
params: {}
}
return await this.rpc(request)
}
async restorePassword (password: string): Promise<LoginInfo> {
const request = {
method: 'restorePassword' as const,
params: { password }
}
return await this.rpc(request)
}
async confirm (): Promise<LoginInfo> {
const request = {
method: 'confirm' as const,
params: {}
}
return await this.rpc(request)
}
async requestPasswordReset (email: string): Promise<void> {
const request = {
method: 'requestPasswordReset' as const,
params: { email }
}
await this.rpc(request)
}
async sendInvite (email: string, role: AccountRole): Promise<void> {
const request = {
method: 'sendInvite' as const,
params: { email, role }
}
await this.rpc(request)
}
async resendInvite (email: string, role: AccountRole): Promise<void> {
const request = {
method: 'resendInvite' as const,
params: [email, role]
}
await this.rpc(request)
}
async createInviteLink (
email: string,
role: AccountRole,
autoJoin: boolean,
firstName: string,
lastName: string,
navigateUrl?: string,
expHours?: number
): Promise<string> {
const request = {
method: 'createInviteLink' as const,
params: { email, role, autoJoin, firstName, lastName, navigateUrl, expHours }
}
return await this.rpc(request)
}
async leaveWorkspace (account: string): Promise<LoginInfo | null> {
const request = {
method: 'leaveWorkspace' as const,
params: { account }
}
return await this.rpc(request)
}
async changeUsername (first: string, last: string): Promise<void> {
const request = {
method: 'changeUsername' as const,
params: { first, last }
}
await this.rpc(request)
}
async changePassword (oldPassword: string, newPassword: string): Promise<void> {
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<WorkspaceLoginInfo> {
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<WorkspaceLoginInfo> {
const request = {
method: 'join' as const,
params: { email, password, inviteId }
}
return await this.rpc(request)
}
async createInvite (exp: number, emailMask: string, limit: number, role: AccountRole): Promise<string> {
const request = {
method: 'createInvite' as const,
params: { exp, emailMask, limit, role }
}
return await this.rpc(request)
}
async checkJoin (inviteId: string): Promise<WorkspaceLoginInfo> {
const request = {
method: 'checkJoin' as const,
params: { inviteId }
}
return await this.rpc(request)
}
async checkAutoJoin (inviteId: string, firstName?: string, lastName?: string): Promise<WorkspaceLoginInfo> {
const request = {
method: 'checkAutoJoin' as const,
params: { inviteId, firstName, lastName }
}
return await this.rpc(request)
}
async getWorkspacesInfo (ids: WorkspaceUuid[]): Promise<WorkspaceInfoWithStatus[]> {
const request = {
method: 'getWorkspacesInfo' as const,
params: { ids }
}
return await this.rpc(request)
}
async getWorkspaceInfo (updateLastVisit: boolean = false): Promise<WorkspaceInfoWithStatus> {
const request = {
method: 'getWorkspaceInfo' as const,
params: updateLastVisit ? { updateLastVisit: true } : {}
}
return this.flattenStatus(await this.rpc(request))
}
async getRegionInfo (): Promise<RegionInfo[]> {
const request = {
method: 'getRegionInfo' as const,
params: {}
}
return await this.rpc(request)
}
async createWorkspace (workspaceName: string, region?: string): Promise<WorkspaceLoginInfo> {
const request = {
method: 'createWorkspace' as const,
params: { workspaceName, region }
}
return await this.rpc(request)
}
async signUpOtp (email: string, firstName: string, lastName: string): Promise<OtpInfo> {
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<LoginInfo> {
const request = {
method: 'signUp' as const,
params: { email, password, firstName, lastName }
}
return await this.rpc(request)
}
async login (email: string, password: string): Promise<LoginInfo> {
const request = {
method: 'login' as const,
params: { email, password }
}
return await this.rpc(request)
}
async getPerson (): Promise<Person> {
const request = {
method: 'getPerson' as const,
params: {}
}
return await this.rpc(request)
}
async getPersonInfo (account: PersonUuid): Promise<PersonInfo> {
const request = {
method: 'getPersonInfo' as const,
params: { account }
}
return await this.rpc(request)
}
async getSocialIds (): Promise<SocialId[]> {
const request = {
method: 'getSocialIds' as const,
params: {}
}
return await this.rpc(request)
}
async workerHandshake (region: string, version: Data<Version>, operation: WorkspaceOperation): Promise<void> {
const request = {
method: 'workerHandshake' as const,
params: { region, version, operation }
}
await this.rpc(request)
}
async getPendingWorkspace (
region: string,
version: Data<Version>,
operation: WorkspaceOperation
): Promise<WorkspaceInfoWithStatus | null> {
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<Version>,
progress: number,
message?: string
): Promise<void> {
const request = {
method: 'updateWorkspaceInfo' as const,
params: { workspaceUuid, event, version, progress, message }
}
await this.rpc(request)
}
async getWorkspaceMembers (): Promise<WorkspaceMemberInfo[]> {
const request = {
method: 'getWorkspaceMembers' as const,
params: {}
}
return await this.rpc(request)
}
async updateWorkspaceRole (targetAccount: string, targetRole: AccountRole): Promise<void> {
const request = {
method: 'updateWorkspaceRole' as const,
params: { targetAccount, targetRole }
}
await this.rpc(request)
}
async updateWorkspaceName (name: string): Promise<void> {
const request = {
method: 'updateWorkspaceName' as const,
params: { name }
}
await this.rpc(request)
}
async deleteWorkspace (): Promise<void> {
const request = {
method: 'deleteWorkspace' as const,
params: {}
}
await this.rpc(request)
}
async findPersonBySocialKey (socialString: string, requireAccount?: boolean): Promise<PersonUuid | undefined> {
const request = {
method: 'findPersonBySocialKey' as const,
params: { socialString, requireAccount }
}
return await this.rpc(request)
}
async findPersonBySocialId (socialId: PersonId, requireAccount?: boolean): Promise<PersonUuid | undefined> {
const request = {
method: 'findPersonBySocialId' as const,
params: { socialId, requireAccount }
}
return await this.rpc(request)
}
async findSocialIdBySocialKey (socialKey: string): Promise<PersonId | undefined> {
const request = {
method: 'findSocialIdBySocialKey' as const,
params: { socialKey }
}
return await this.rpc(request)
}
async listWorkspaces (region?: string | null, mode: WorkspaceMode | null = null): Promise<WorkspaceInfoWithStatus[]> {
const request = {
method: 'listWorkspaces' as const,
params: { region, mode }
}
return ((await this.rpc<any[]>(request)) ?? []).map((ws) => this.flattenStatus(ws))
}
async performWorkspaceOperation (
workspaceId: string | string[],
event: WorkspaceUserOperation,
...params: any
): Promise<boolean> {
const request = {
method: 'performWorkspaceOperation' as const,
params: { workspaceId, event, params }
}
return await this.rpc(request)
}
async updateBackupInfo (backupInfo: BackupStatus): Promise<void> {
const request = {
method: 'updateBackupInfo' as const,
params: { backupInfo }
}
await this.rpc(request)
}
async assignWorkspace (email: string, workspaceUuid: string, role: AccountRole): Promise<void> {
const request = {
method: 'assignWorkspace' as const,
params: { email, workspaceUuid, role }
}
await this.rpc(request)
}
async updateWorkspaceRoleBySocialKey (socialKey: string, targetRole: AccountRole): Promise<void> {
const request = {
method: 'updateWorkspaceRoleBySocialKey' as const,
params: { socialKey, targetRole }
}
await this.rpc(request)
}
async ensurePerson (
socialType: SocialIdType,
socialValue: string,
firstName: string,
lastName: string
): Promise<{ uuid: PersonUuid, socialId: PersonId }> {
const request = {
method: 'ensurePerson' as const,
params: { socialType, socialValue, firstName, lastName }
}
return await this.rpc(request)
}
async addSocialIdToPerson (
person: PersonUuid,
type: SocialIdType,
value: string,
confirmed: boolean
): Promise<PersonId> {
const request = {
method: 'addSocialIdToPerson' as const,
params: { person, type, value, confirmed }
}
return await this.rpc(request)
}
async getMailboxOptions (): Promise<MailboxOptions> {
const request = {
method: 'getMailboxOptions' as const,
params: {}
}
return await this.rpc(request)
}
async createMailbox (name: string, domain: string): Promise<{ mailbox: string, socialId: PersonId }> {
const request = {
method: 'createMailbox' as const,
params: { name, domain }
}
return await this.rpc(request)
}
async getMailboxes (): Promise<MailboxInfo[]> {
const request = {
method: 'getMailboxes' as const,
params: {}
}
return await this.rpc(request)
}
async deleteMailbox (mailbox: string): Promise<void> {
const request = {
method: 'deleteMailbox' as const,
params: { mailbox }
}
await this.rpc(request)
}
async createIntegration (integration: Integration): Promise<void> {
const request = {
method: 'createIntegration' as const,
params: integration
}
await this.rpc(request)
}
async updateIntegration (integration: Integration): Promise<void> {
const request = {
method: 'updateIntegration' as const,
params: integration
}
await this.rpc(request)
}
async deleteIntegration (integrationKey: IntegrationKey): Promise<void> {
const request = {
method: 'deleteIntegration' as const,
params: integrationKey
}
await this.rpc(request)
}
async getIntegration (integrationKey: IntegrationKey): Promise<Integration | null> {
const request = {
method: 'getIntegration' as const,
params: integrationKey
}
return await this.rpc(request)
}
async listIntegrations (filter: {
socialId?: PersonId
kind?: string
workspaceUuid?: WorkspaceUuid | null
}): Promise<Integration[]> {
const request = {
method: 'listIntegrations' as const,
params: filter
}
return await this.rpc(request)
}
async addIntegrationSecret (integrationSecret: IntegrationSecret): Promise<void> {
const request = {
method: 'addIntegrationSecret' as const,
params: integrationSecret
}
await this.rpc(request)
}
async updateIntegrationSecret (integrationSecret: IntegrationSecret): Promise<void> {
const request = {
method: 'updateIntegrationSecret' as const,
params: integrationSecret
}
await this.rpc(request)
}
async deleteIntegrationSecret (integrationSecretKey: IntegrationSecretKey): Promise<void> {
const request = {
method: 'deleteIntegrationSecret' as const,
params: integrationSecretKey
}
await this.rpc(request)
}
async getIntegrationSecret (integrationSecretKey: IntegrationSecretKey): Promise<IntegrationSecret | null> {
const request = {
method: 'getIntegrationSecret' as const,
params: integrationSecretKey
}
return await this.rpc(request)
}
async listIntegrationsSecrets (filter: {
socialId?: PersonId
kind?: string
workspaceUuid?: WorkspaceUuid | null
key?: string
}): Promise<IntegrationSecret[]> {
const request = {
method: 'listIntegrationsSecrets' as const,
params: filter
}
return await this.rpc(request)
}
async getAccountInfo (uuid: PersonUuid): Promise<AccountInfo> {
const request = {
method: 'getAccountInfo' as const,
params: { accountId: uuid }
}
return await this.rpc(request)
}
async setCookie (): Promise<void> {
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<void> {
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<T> (retries: number, op: () => Promise<T>, delay: number = 100): Promise<T> {
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
}