mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-11 01:40:32 +00:00
UBERF-9734: Set default account timezone (#8469)
Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
parent
e2a2011ad8
commit
163cca55da
@ -14,6 +14,7 @@
|
||||
//
|
||||
import {
|
||||
type AccountRole,
|
||||
type AccountInfo,
|
||||
BackupStatus,
|
||||
Data,
|
||||
type Person,
|
||||
@ -44,6 +45,7 @@ import type {
|
||||
IntegrationSecret,
|
||||
IntegrationSecretKey
|
||||
} from './types'
|
||||
import { getClientTimezone } from './utils'
|
||||
|
||||
/** @public */
|
||||
export interface AccountClient {
|
||||
@ -159,6 +161,7 @@ export interface AccountClient {
|
||||
workspaceUuid?: WorkspaceUuid | null
|
||||
key?: string
|
||||
}) => Promise<IntegrationSecret[]>
|
||||
getAccountInfo: (uuid: PersonUuid) => Promise<AccountInfo>
|
||||
|
||||
setCookie: () => Promise<void>
|
||||
deleteCookie: () => Promise<void>
|
||||
@ -213,11 +216,14 @@ class AccountClientImpl implements AccountClient {
|
||||
}
|
||||
|
||||
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'
|
||||
'Content-Type': 'application/json',
|
||||
...meta
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request)
|
||||
@ -843,6 +849,15 @@ class AccountClientImpl implements AccountClient {
|
||||
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' })
|
||||
|
@ -18,3 +18,12 @@ import type { LoginInfo, WorkspaceLoginInfo } from './types'
|
||||
export function isWorkspaceLoginInfo (loginInfo: LoginInfo | WorkspaceLoginInfo): loginInfo is WorkspaceLoginInfo {
|
||||
return (loginInfo as WorkspaceLoginInfo).workspace != null
|
||||
}
|
||||
|
||||
export function getClientTimezone (): string | undefined {
|
||||
try {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
} catch (err: any) {
|
||||
console.error('Failed to get client timezone', err)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
@ -859,4 +859,9 @@ export interface SocialId {
|
||||
verifiedOn?: number
|
||||
}
|
||||
|
||||
export interface AccountInfo {
|
||||
timezone?: string
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export type SocialKey = Pick<SocialId, 'type' | 'value'>
|
||||
|
@ -544,10 +544,17 @@ describe('account utils', () => {
|
||||
const result = await wrappedMethod(mockCtx, mockDb, mockBranding, request, 'token')
|
||||
|
||||
expect(result).toEqual({ id: 'req1', result: mockResult })
|
||||
expect(mockMethod).toHaveBeenCalledWith(mockCtx, mockDb, mockBranding, 'token', {
|
||||
param1: 'value1',
|
||||
param2: 'value2'
|
||||
})
|
||||
expect(mockMethod).toHaveBeenCalledWith(
|
||||
mockCtx,
|
||||
mockDb,
|
||||
mockBranding,
|
||||
'token',
|
||||
{
|
||||
param1: 'value1',
|
||||
param2: 'value2'
|
||||
},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
test('should handle token parameter', async () => {
|
||||
@ -559,7 +566,7 @@ describe('account utils', () => {
|
||||
const result = await wrappedMethod(mockCtx, mockDb, mockBranding, request, 'token')
|
||||
|
||||
expect(result).toEqual({ id: 'req1', result: mockResult })
|
||||
expect(mockMethod).toHaveBeenCalledWith(mockCtx, mockDb, mockBranding, 'token', { param1: 'value1' })
|
||||
expect(mockMethod).toHaveBeenCalledWith(mockCtx, mockDb, mockBranding, 'token', { param1: 'value1' }, {})
|
||||
})
|
||||
|
||||
test('should handle PlatformError', async () => {
|
||||
@ -609,6 +616,26 @@ describe('account utils', () => {
|
||||
|
||||
await wrappedMethod(mockCtx, mockDb, mockBranding, request, 'token')
|
||||
})
|
||||
|
||||
test('should handle timezone parameter', async () => {
|
||||
const mockResult = { data: 'test' }
|
||||
const mockMethod = jest.fn().mockResolvedValue(mockResult)
|
||||
const wrappedMethod = wrap(mockMethod)
|
||||
const mockTimezone = 'America/New_York'
|
||||
const request = { id: 'req1', params: { param1: 'value1' }, headers: { 'X-Timezone': mockTimezone } }
|
||||
|
||||
const result = await wrappedMethod(mockCtx, mockDb, mockBranding, request, 'token')
|
||||
|
||||
expect(result).toEqual({ id: 'req1', result: mockResult })
|
||||
expect(mockMethod).toHaveBeenCalledWith(
|
||||
mockCtx,
|
||||
mockDb,
|
||||
mockBranding,
|
||||
'token',
|
||||
{ param1: 'value1' },
|
||||
{ timezone: mockTimezone }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with mocked fetch', () => {
|
||||
|
@ -15,6 +15,7 @@
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import {
|
||||
AccountRole,
|
||||
AccountInfo,
|
||||
buildSocialIdString,
|
||||
concatLink,
|
||||
isActiveMode,
|
||||
@ -39,6 +40,7 @@ import type {
|
||||
AccountDB,
|
||||
AccountMethodHandler,
|
||||
LoginInfo,
|
||||
Meta,
|
||||
Mailbox,
|
||||
MailboxOptions,
|
||||
OtpInfo,
|
||||
@ -86,7 +88,8 @@ import {
|
||||
generatePassword,
|
||||
addSocialId,
|
||||
releaseSocialId,
|
||||
updateWorkspaceRole
|
||||
updateWorkspaceRole,
|
||||
setTimezoneIfNotDefined
|
||||
} from './utils'
|
||||
import { type AccountServiceMethods, getServiceMethods } from './serviceOperations'
|
||||
|
||||
@ -112,7 +115,8 @@ export async function login (
|
||||
params: {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
},
|
||||
meta?: Meta
|
||||
): Promise<LoginInfo> {
|
||||
const { email, password } = params
|
||||
const normalizedEmail = cleanEmail(email)
|
||||
@ -143,6 +147,7 @@ export async function login (
|
||||
|
||||
const extraToken: Record<string, string> = isAdminEmail(email) ? { admin: 'true' } : {}
|
||||
ctx.info('Login succeeded', { email, normalizedEmail, isConfirmed, emailSocialId, ...extraToken })
|
||||
void setTimezoneIfNotDefined(ctx, db, emailSocialId.personUuid, existingAccount, meta)
|
||||
|
||||
return {
|
||||
account: existingAccount.uuid,
|
||||
@ -165,7 +170,8 @@ export async function loginOtp (
|
||||
db: AccountDB,
|
||||
branding: Branding | null,
|
||||
token: string,
|
||||
params: { email: string }
|
||||
params: { email: string },
|
||||
meta?: Meta
|
||||
): Promise<OtpInfo> {
|
||||
const { email } = params
|
||||
|
||||
@ -183,6 +189,8 @@ export async function loginOtp (
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {}))
|
||||
}
|
||||
|
||||
void setTimezoneIfNotDefined(ctx, db, emailSocialId.personUuid, account, meta)
|
||||
|
||||
return await sendOtp(ctx, db, branding, emailSocialId)
|
||||
}
|
||||
|
||||
@ -200,7 +208,8 @@ export async function signUp (
|
||||
password: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
},
|
||||
meta?: Meta
|
||||
): Promise<LoginInfo> {
|
||||
const { email, password, firstName, lastName } = params
|
||||
const { account, socialId } = await signUpByEmail(ctx, db, branding, email, password, firstName, lastName)
|
||||
@ -220,6 +229,7 @@ export async function signUp (
|
||||
await confirmEmail(ctx, db, account, email)
|
||||
}
|
||||
|
||||
void setTimezoneIfNotDefined(ctx, db, account, null, meta)
|
||||
return {
|
||||
account,
|
||||
name: getPersonName(person),
|
||||
@ -237,7 +247,8 @@ export async function signUpOtp (
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
},
|
||||
meta?: Meta
|
||||
): Promise<OtpInfo> {
|
||||
const { email, firstName, lastName } = params
|
||||
// Note: can support OTP based on any other social logins later
|
||||
@ -263,6 +274,7 @@ export async function signUpOtp (
|
||||
const emailSocialIdId = await db.socialId.insertOne(newSocialId)
|
||||
emailSocialId = { ...newSocialId, _id: emailSocialIdId, key: buildSocialIdString(newSocialId) }
|
||||
}
|
||||
void setTimezoneIfNotDefined(ctx, db, personUuid, null, meta)
|
||||
|
||||
return await sendOtp(ctx, db, branding, emailSocialId)
|
||||
}
|
||||
@ -633,7 +645,8 @@ export async function join (
|
||||
email: string
|
||||
password: string
|
||||
inviteId: string
|
||||
}
|
||||
},
|
||||
meta?: Meta
|
||||
): Promise<WorkspaceLoginInfo | LoginInfo> {
|
||||
const { email, password, inviteId } = params
|
||||
const normalizedEmail = cleanEmail(email)
|
||||
@ -651,7 +664,7 @@ export async function join (
|
||||
|
||||
ctx.info('Joining a workspace using invite', { email, normalizedEmail, ...invite })
|
||||
|
||||
const { token, account } = await login(ctx, db, branding, _token, { email: normalizedEmail, password })
|
||||
const { token, account } = await login(ctx, db, branding, _token, { email: normalizedEmail, password }, meta)
|
||||
|
||||
if (token == null) {
|
||||
return {
|
||||
@ -829,7 +842,8 @@ export async function signUpJoin (
|
||||
first: string
|
||||
last: string
|
||||
inviteId: string
|
||||
}
|
||||
},
|
||||
meta?: Meta
|
||||
): Promise<WorkspaceLoginInfo> {
|
||||
const { email, password, first, last, inviteId } = params
|
||||
const normalizedEmail = cleanEmail(email)
|
||||
@ -848,6 +862,7 @@ export async function signUpJoin (
|
||||
}
|
||||
|
||||
const { account } = await signUpByEmail(ctx, db, branding, email, password, first, last, true)
|
||||
void setTimezoneIfNotDefined(ctx, db, account, null, meta)
|
||||
|
||||
return await doJoinByInvite(ctx, db, branding, generateToken(account, workspaceUuid), account, workspace, invite)
|
||||
}
|
||||
@ -1493,6 +1508,22 @@ export async function getWorkspaceMembers (
|
||||
return await db.getWorkspaceMembers(workspace)
|
||||
}
|
||||
|
||||
export async function getAccountInfo (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
branding: Branding | null,
|
||||
token: string,
|
||||
params: { accountId: PersonUuid }
|
||||
): Promise<AccountInfo> {
|
||||
decodeTokenVerbose(ctx, token)
|
||||
const { accountId } = params
|
||||
const account = await getAccount(db, accountId)
|
||||
if (account === undefined || account === null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {}))
|
||||
}
|
||||
return { timezone: account?.timezone, locale: account?.locale }
|
||||
}
|
||||
|
||||
export async function ensurePerson (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
@ -1669,6 +1700,8 @@ export type AccountMethods =
|
||||
| 'createMailbox'
|
||||
| 'getMailboxes'
|
||||
| 'deleteMailbox'
|
||||
| 'addSocialIdToPerson'
|
||||
| 'getAccountInfo'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -1719,6 +1752,7 @@ export function getMethods (hasSignUp: boolean = true): Partial<Record<AccountMe
|
||||
findSocialIdBySocialKey: wrap(findSocialIdBySocialKey),
|
||||
getWorkspaceMembers: wrap(getWorkspaceMembers),
|
||||
getMailboxOptions: wrap(getMailboxOptions),
|
||||
getAccountInfo: wrap(getAccountInfo),
|
||||
|
||||
/* SERVICE METHODS */
|
||||
...getServiceMethods()
|
||||
|
@ -243,7 +243,9 @@ export type AccountMethodHandler = (
|
||||
db: AccountDB,
|
||||
branding: Branding | null,
|
||||
request: any,
|
||||
token: string | undefined
|
||||
token: string | undefined,
|
||||
params?: Record<string, any>,
|
||||
meta?: Record<string, any>
|
||||
) => Promise<any>
|
||||
|
||||
export type WorkspaceEvent =
|
||||
@ -302,3 +304,7 @@ export interface MailboxOptions {
|
||||
maxNameLength: number
|
||||
maxMailboxCount: number
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
timezone?: string
|
||||
}
|
||||
|
@ -54,7 +54,8 @@ import {
|
||||
LoginInfo,
|
||||
WorkspaceLoginInfo,
|
||||
WorkspaceStatus,
|
||||
AccountEventType
|
||||
AccountEventType,
|
||||
Meta
|
||||
} from './types'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { TokenError, decodeTokenVerbose, generateToken } from '@hcengineering/server-token'
|
||||
@ -118,7 +119,11 @@ export function wrap (
|
||||
request: any,
|
||||
token?: string
|
||||
): Promise<any> {
|
||||
return await accountMethod(ctx, db, branding, token, { ...request.params })
|
||||
const meta =
|
||||
request.headers !== undefined && request.headers['X-Timezone'] !== undefined
|
||||
? { timezone: request.headers['X-Timezone'] }
|
||||
: {}
|
||||
return await accountMethod(ctx, db, branding, token, { ...request.params }, meta)
|
||||
.then((result) => ({ id: request.id, result }))
|
||||
.catch((err) => {
|
||||
const status =
|
||||
@ -1384,3 +1389,24 @@ export async function getWorkspaceRole (
|
||||
export function generatePassword (len: number = 24): string {
|
||||
return randomBytes(len).toString('base64').slice(0, len)
|
||||
}
|
||||
|
||||
export async function setTimezoneIfNotDefined (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
accountId: PersonUuid,
|
||||
account: Account | null | undefined,
|
||||
meta?: Meta
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (meta?.timezone === undefined) return
|
||||
const existingAccount = account ?? (await db.account.findOne({ uuid: accountId }))
|
||||
if (existingAccount === undefined || existingAccount === null) {
|
||||
ctx.warn('Failed to find account')
|
||||
return
|
||||
}
|
||||
if (existingAccount.timezone != null) return
|
||||
await db.account.updateOne({ uuid: accountId }, { timezone: meta.timezone })
|
||||
} catch (err: any) {
|
||||
ctx.error('Failed to set account timezone', err)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user