mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-11 18:01:59 +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 {
|
import {
|
||||||
type AccountRole,
|
type AccountRole,
|
||||||
|
type AccountInfo,
|
||||||
BackupStatus,
|
BackupStatus,
|
||||||
Data,
|
Data,
|
||||||
type Person,
|
type Person,
|
||||||
@ -44,6 +45,7 @@ import type {
|
|||||||
IntegrationSecret,
|
IntegrationSecret,
|
||||||
IntegrationSecretKey
|
IntegrationSecretKey
|
||||||
} from './types'
|
} from './types'
|
||||||
|
import { getClientTimezone } from './utils'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export interface AccountClient {
|
export interface AccountClient {
|
||||||
@ -159,6 +161,7 @@ export interface AccountClient {
|
|||||||
workspaceUuid?: WorkspaceUuid | null
|
workspaceUuid?: WorkspaceUuid | null
|
||||||
key?: string
|
key?: string
|
||||||
}) => Promise<IntegrationSecret[]>
|
}) => Promise<IntegrationSecret[]>
|
||||||
|
getAccountInfo: (uuid: PersonUuid) => Promise<AccountInfo>
|
||||||
|
|
||||||
setCookie: () => Promise<void>
|
setCookie: () => Promise<void>
|
||||||
deleteCookie: () => Promise<void>
|
deleteCookie: () => Promise<void>
|
||||||
@ -213,11 +216,14 @@ class AccountClientImpl implements AccountClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async rpc<T>(request: Request): Promise<T> {
|
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, {
|
const response = await fetch(this.url, {
|
||||||
...this.request,
|
...this.request,
|
||||||
headers: {
|
headers: {
|
||||||
...this.request.headers,
|
...this.request.headers,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
...meta
|
||||||
},
|
},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(request)
|
body: JSON.stringify(request)
|
||||||
@ -843,6 +849,15 @@ class AccountClientImpl implements AccountClient {
|
|||||||
return await this.rpc(request)
|
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> {
|
async setCookie (): Promise<void> {
|
||||||
const url = concatLink(this.url, '/cookie')
|
const url = concatLink(this.url, '/cookie')
|
||||||
const response = await fetch(url, { ...this.request, method: 'PUT' })
|
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 {
|
export function isWorkspaceLoginInfo (loginInfo: LoginInfo | WorkspaceLoginInfo): loginInfo is WorkspaceLoginInfo {
|
||||||
return (loginInfo as WorkspaceLoginInfo).workspace != null
|
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
|
verifiedOn?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AccountInfo {
|
||||||
|
timezone?: string
|
||||||
|
locale?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type SocialKey = Pick<SocialId, 'type' | 'value'>
|
export type SocialKey = Pick<SocialId, 'type' | 'value'>
|
||||||
|
@ -544,10 +544,17 @@ describe('account utils', () => {
|
|||||||
const result = await wrappedMethod(mockCtx, mockDb, mockBranding, request, 'token')
|
const result = await wrappedMethod(mockCtx, mockDb, mockBranding, request, 'token')
|
||||||
|
|
||||||
expect(result).toEqual({ id: 'req1', result: mockResult })
|
expect(result).toEqual({ id: 'req1', result: mockResult })
|
||||||
expect(mockMethod).toHaveBeenCalledWith(mockCtx, mockDb, mockBranding, 'token', {
|
expect(mockMethod).toHaveBeenCalledWith(
|
||||||
param1: 'value1',
|
mockCtx,
|
||||||
param2: 'value2'
|
mockDb,
|
||||||
})
|
mockBranding,
|
||||||
|
'token',
|
||||||
|
{
|
||||||
|
param1: 'value1',
|
||||||
|
param2: 'value2'
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should handle token parameter', async () => {
|
test('should handle token parameter', async () => {
|
||||||
@ -559,7 +566,7 @@ describe('account utils', () => {
|
|||||||
const result = await wrappedMethod(mockCtx, mockDb, mockBranding, request, 'token')
|
const result = await wrappedMethod(mockCtx, mockDb, mockBranding, request, 'token')
|
||||||
|
|
||||||
expect(result).toEqual({ id: 'req1', result: mockResult })
|
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 () => {
|
test('should handle PlatformError', async () => {
|
||||||
@ -609,6 +616,26 @@ describe('account utils', () => {
|
|||||||
|
|
||||||
await wrappedMethod(mockCtx, mockDb, mockBranding, request, 'token')
|
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', () => {
|
describe('with mocked fetch', () => {
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
import { Analytics } from '@hcengineering/analytics'
|
import { Analytics } from '@hcengineering/analytics'
|
||||||
import {
|
import {
|
||||||
AccountRole,
|
AccountRole,
|
||||||
|
AccountInfo,
|
||||||
buildSocialIdString,
|
buildSocialIdString,
|
||||||
concatLink,
|
concatLink,
|
||||||
isActiveMode,
|
isActiveMode,
|
||||||
@ -39,6 +40,7 @@ import type {
|
|||||||
AccountDB,
|
AccountDB,
|
||||||
AccountMethodHandler,
|
AccountMethodHandler,
|
||||||
LoginInfo,
|
LoginInfo,
|
||||||
|
Meta,
|
||||||
Mailbox,
|
Mailbox,
|
||||||
MailboxOptions,
|
MailboxOptions,
|
||||||
OtpInfo,
|
OtpInfo,
|
||||||
@ -86,7 +88,8 @@ import {
|
|||||||
generatePassword,
|
generatePassword,
|
||||||
addSocialId,
|
addSocialId,
|
||||||
releaseSocialId,
|
releaseSocialId,
|
||||||
updateWorkspaceRole
|
updateWorkspaceRole,
|
||||||
|
setTimezoneIfNotDefined
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { type AccountServiceMethods, getServiceMethods } from './serviceOperations'
|
import { type AccountServiceMethods, getServiceMethods } from './serviceOperations'
|
||||||
|
|
||||||
@ -112,7 +115,8 @@ export async function login (
|
|||||||
params: {
|
params: {
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
}
|
},
|
||||||
|
meta?: Meta
|
||||||
): Promise<LoginInfo> {
|
): Promise<LoginInfo> {
|
||||||
const { email, password } = params
|
const { email, password } = params
|
||||||
const normalizedEmail = cleanEmail(email)
|
const normalizedEmail = cleanEmail(email)
|
||||||
@ -143,6 +147,7 @@ export async function login (
|
|||||||
|
|
||||||
const extraToken: Record<string, string> = isAdminEmail(email) ? { admin: 'true' } : {}
|
const extraToken: Record<string, string> = isAdminEmail(email) ? { admin: 'true' } : {}
|
||||||
ctx.info('Login succeeded', { email, normalizedEmail, isConfirmed, emailSocialId, ...extraToken })
|
ctx.info('Login succeeded', { email, normalizedEmail, isConfirmed, emailSocialId, ...extraToken })
|
||||||
|
void setTimezoneIfNotDefined(ctx, db, emailSocialId.personUuid, existingAccount, meta)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account: existingAccount.uuid,
|
account: existingAccount.uuid,
|
||||||
@ -165,7 +170,8 @@ export async function loginOtp (
|
|||||||
db: AccountDB,
|
db: AccountDB,
|
||||||
branding: Branding | null,
|
branding: Branding | null,
|
||||||
token: string,
|
token: string,
|
||||||
params: { email: string }
|
params: { email: string },
|
||||||
|
meta?: Meta
|
||||||
): Promise<OtpInfo> {
|
): Promise<OtpInfo> {
|
||||||
const { email } = params
|
const { email } = params
|
||||||
|
|
||||||
@ -183,6 +189,8 @@ export async function loginOtp (
|
|||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {}))
|
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)
|
return await sendOtp(ctx, db, branding, emailSocialId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +208,8 @@ export async function signUp (
|
|||||||
password: string
|
password: string
|
||||||
firstName: string
|
firstName: string
|
||||||
lastName: string
|
lastName: string
|
||||||
}
|
},
|
||||||
|
meta?: Meta
|
||||||
): Promise<LoginInfo> {
|
): Promise<LoginInfo> {
|
||||||
const { email, password, firstName, lastName } = params
|
const { email, password, firstName, lastName } = params
|
||||||
const { account, socialId } = await signUpByEmail(ctx, db, branding, email, password, firstName, lastName)
|
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)
|
await confirmEmail(ctx, db, account, email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setTimezoneIfNotDefined(ctx, db, account, null, meta)
|
||||||
return {
|
return {
|
||||||
account,
|
account,
|
||||||
name: getPersonName(person),
|
name: getPersonName(person),
|
||||||
@ -237,7 +247,8 @@ export async function signUpOtp (
|
|||||||
email: string
|
email: string
|
||||||
firstName: string
|
firstName: string
|
||||||
lastName: string
|
lastName: string
|
||||||
}
|
},
|
||||||
|
meta?: Meta
|
||||||
): Promise<OtpInfo> {
|
): Promise<OtpInfo> {
|
||||||
const { email, firstName, lastName } = params
|
const { email, firstName, lastName } = params
|
||||||
// Note: can support OTP based on any other social logins later
|
// 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)
|
const emailSocialIdId = await db.socialId.insertOne(newSocialId)
|
||||||
emailSocialId = { ...newSocialId, _id: emailSocialIdId, key: buildSocialIdString(newSocialId) }
|
emailSocialId = { ...newSocialId, _id: emailSocialIdId, key: buildSocialIdString(newSocialId) }
|
||||||
}
|
}
|
||||||
|
void setTimezoneIfNotDefined(ctx, db, personUuid, null, meta)
|
||||||
|
|
||||||
return await sendOtp(ctx, db, branding, emailSocialId)
|
return await sendOtp(ctx, db, branding, emailSocialId)
|
||||||
}
|
}
|
||||||
@ -633,7 +645,8 @@ export async function join (
|
|||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
inviteId: string
|
inviteId: string
|
||||||
}
|
},
|
||||||
|
meta?: Meta
|
||||||
): Promise<WorkspaceLoginInfo | LoginInfo> {
|
): Promise<WorkspaceLoginInfo | LoginInfo> {
|
||||||
const { email, password, inviteId } = params
|
const { email, password, inviteId } = params
|
||||||
const normalizedEmail = cleanEmail(email)
|
const normalizedEmail = cleanEmail(email)
|
||||||
@ -651,7 +664,7 @@ export async function join (
|
|||||||
|
|
||||||
ctx.info('Joining a workspace using invite', { email, normalizedEmail, ...invite })
|
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) {
|
if (token == null) {
|
||||||
return {
|
return {
|
||||||
@ -829,7 +842,8 @@ export async function signUpJoin (
|
|||||||
first: string
|
first: string
|
||||||
last: string
|
last: string
|
||||||
inviteId: string
|
inviteId: string
|
||||||
}
|
},
|
||||||
|
meta?: Meta
|
||||||
): Promise<WorkspaceLoginInfo> {
|
): Promise<WorkspaceLoginInfo> {
|
||||||
const { email, password, first, last, inviteId } = params
|
const { email, password, first, last, inviteId } = params
|
||||||
const normalizedEmail = cleanEmail(email)
|
const normalizedEmail = cleanEmail(email)
|
||||||
@ -848,6 +862,7 @@ export async function signUpJoin (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { account } = await signUpByEmail(ctx, db, branding, email, password, first, last, true)
|
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)
|
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)
|
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 (
|
export async function ensurePerson (
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
db: AccountDB,
|
db: AccountDB,
|
||||||
@ -1669,6 +1700,8 @@ export type AccountMethods =
|
|||||||
| 'createMailbox'
|
| 'createMailbox'
|
||||||
| 'getMailboxes'
|
| 'getMailboxes'
|
||||||
| 'deleteMailbox'
|
| 'deleteMailbox'
|
||||||
|
| 'addSocialIdToPerson'
|
||||||
|
| 'getAccountInfo'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -1719,6 +1752,7 @@ export function getMethods (hasSignUp: boolean = true): Partial<Record<AccountMe
|
|||||||
findSocialIdBySocialKey: wrap(findSocialIdBySocialKey),
|
findSocialIdBySocialKey: wrap(findSocialIdBySocialKey),
|
||||||
getWorkspaceMembers: wrap(getWorkspaceMembers),
|
getWorkspaceMembers: wrap(getWorkspaceMembers),
|
||||||
getMailboxOptions: wrap(getMailboxOptions),
|
getMailboxOptions: wrap(getMailboxOptions),
|
||||||
|
getAccountInfo: wrap(getAccountInfo),
|
||||||
|
|
||||||
/* SERVICE METHODS */
|
/* SERVICE METHODS */
|
||||||
...getServiceMethods()
|
...getServiceMethods()
|
||||||
|
@ -243,7 +243,9 @@ export type AccountMethodHandler = (
|
|||||||
db: AccountDB,
|
db: AccountDB,
|
||||||
branding: Branding | null,
|
branding: Branding | null,
|
||||||
request: any,
|
request: any,
|
||||||
token: string | undefined
|
token: string | undefined,
|
||||||
|
params?: Record<string, any>,
|
||||||
|
meta?: Record<string, any>
|
||||||
) => Promise<any>
|
) => Promise<any>
|
||||||
|
|
||||||
export type WorkspaceEvent =
|
export type WorkspaceEvent =
|
||||||
@ -302,3 +304,7 @@ export interface MailboxOptions {
|
|||||||
maxNameLength: number
|
maxNameLength: number
|
||||||
maxMailboxCount: number
|
maxMailboxCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Meta {
|
||||||
|
timezone?: string
|
||||||
|
}
|
||||||
|
@ -54,7 +54,8 @@ import {
|
|||||||
LoginInfo,
|
LoginInfo,
|
||||||
WorkspaceLoginInfo,
|
WorkspaceLoginInfo,
|
||||||
WorkspaceStatus,
|
WorkspaceStatus,
|
||||||
AccountEventType
|
AccountEventType,
|
||||||
|
Meta
|
||||||
} from './types'
|
} from './types'
|
||||||
import { Analytics } from '@hcengineering/analytics'
|
import { Analytics } from '@hcengineering/analytics'
|
||||||
import { TokenError, decodeTokenVerbose, generateToken } from '@hcengineering/server-token'
|
import { TokenError, decodeTokenVerbose, generateToken } from '@hcengineering/server-token'
|
||||||
@ -118,7 +119,11 @@ export function wrap (
|
|||||||
request: any,
|
request: any,
|
||||||
token?: string
|
token?: string
|
||||||
): Promise<any> {
|
): 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 }))
|
.then((result) => ({ id: request.id, result }))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
const status =
|
const status =
|
||||||
@ -1384,3 +1389,24 @@ export async function getWorkspaceRole (
|
|||||||
export function generatePassword (len: number = 24): string {
|
export function generatePassword (len: number = 24): string {
|
||||||
return randomBytes(len).toString('base64').slice(0, len)
|
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