mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
UBERF-13511: Support guest tokens with open account (#9834)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
@@ -97,6 +97,14 @@ export interface AccountClient {
|
||||
) => Promise<WorkspaceLoginInfo>
|
||||
join: (email: string, password: string, inviteId: string, workspaceUrl: string) => Promise<WorkspaceLoginInfo>
|
||||
createInvite: (exp: number, emailMask: string, limit: number, role: AccountRole) => Promise<string>
|
||||
/**
|
||||
* @param options.personalized
|
||||
* If true, will generate a link with a personalized token for one person access
|
||||
* If false, will generate a link with an open-ended account in the token. Every token use will generate a new account.
|
||||
* When false, notBefore and expiration parameters are mandatory.
|
||||
* @param options.notBefore - not valid before; timestamp in seconds
|
||||
* @param options.expiration - expires after; timestamp in seconds
|
||||
*/
|
||||
createAccessLink: (
|
||||
role: AccountRole,
|
||||
options?: {
|
||||
@@ -105,6 +113,9 @@ export interface AccountClient {
|
||||
navigateUrl?: string
|
||||
extra?: Record<string, any>
|
||||
spaces?: string[]
|
||||
notBefore?: number
|
||||
expiration?: number
|
||||
personalized?: boolean
|
||||
}
|
||||
) => Promise<string>
|
||||
checkJoin: (inviteId: string) => Promise<WorkspaceLoginInfo>
|
||||
@@ -435,6 +446,9 @@ class AccountClientImpl implements AccountClient {
|
||||
navigateUrl?: string
|
||||
extra?: Record<string, any>
|
||||
spaces?: string[]
|
||||
notBefore?: number
|
||||
expiration?: number
|
||||
personalized?: boolean
|
||||
}
|
||||
): Promise<string> {
|
||||
const params: any = { ...(options ?? {}), role }
|
||||
|
||||
@@ -141,6 +141,8 @@ export default plugin(platformId, {
|
||||
BadRequest: '' as StatusCode,
|
||||
Forbidden: '' as StatusCode, // 403
|
||||
Unauthorized: '' as StatusCode, // 401
|
||||
TokenExpired: '' as StatusCode, // 401
|
||||
TokenNotActive: '' as StatusCode<{ notBefore: number }>, // 401
|
||||
Conflict: '' as StatusCode, // 409
|
||||
ExpiredLink: '' as StatusCode,
|
||||
UnknownMethod: '' as StatusCode<{ method: string }>,
|
||||
|
||||
@@ -17,13 +17,18 @@
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { Timestamp } from '@hcengineering/core'
|
||||
|
||||
import { Label } from '..'
|
||||
import ui from '../plugin'
|
||||
|
||||
export let time: Timestamp
|
||||
export let showHours = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let displayTime = time
|
||||
let notified = false
|
||||
let intervalId: any | undefined = undefined
|
||||
const dayMs = 1000 * 60 * 60 * 24
|
||||
|
||||
function applyTimer (time: number): void {
|
||||
if (intervalId !== undefined) {
|
||||
@@ -56,11 +61,19 @@
|
||||
|
||||
function getDisplayTime (time: number): string {
|
||||
const options: Intl.DateTimeFormatOptions = { minute: 'numeric', second: 'numeric' }
|
||||
if (showHours) {
|
||||
options.timeZone = 'UTC'
|
||||
options.hour = 'numeric'
|
||||
}
|
||||
|
||||
return new Date(time).toLocaleString('default', options)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if displayTime > 0}
|
||||
{getDisplayTime(displayTime)}
|
||||
{#if displayTime < dayMs}
|
||||
{getDisplayTime(displayTime)}
|
||||
{:else}
|
||||
<Label label={ui.string.Days} params={{ days: Math.floor(displayTime / dayMs) }} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -75,6 +75,8 @@
|
||||
"SetPasswordNow": "Nastavím heslo nyní",
|
||||
"LoginAsGuest": "Pokračovat jako host",
|
||||
"SignUpToCreateWorkspace": "Neexistují žádné pracovní prostory s přístupem pro hosty. Prosím, zaregistrujte se a vytvořte si vlastní.",
|
||||
"WhatIsYourName": "Jak se jmenujete?"
|
||||
"WhatIsYourName": "Jak se jmenujete?",
|
||||
"AccessExpired": "Doba přístupu vypršela. Požádejte prosím o nový odkaz pro přístup.",
|
||||
"AccessNotActive": "Doba přístupu ještě nezačala. Začíná za: "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@
|
||||
"SetPasswordNow": "Ich werde jetzt ein Passwort festlegen",
|
||||
"LoginAsGuest": "Als Gast fortfahren",
|
||||
"SignUpToCreateWorkspace": "Keine Arbeitsbereiche mit Gastzugang. Bitte registrieren Sie sich, um Ihren eigenen zu erstellen.",
|
||||
"WhatIsYourName": "Wie ist Ihr Name?"
|
||||
"WhatIsYourName": "Wie ist Ihr Name?",
|
||||
"AccessExpired": "Zugriffszeit abgelaufen. Bitte fragen Sie nach einem neuen Zugangslink.",
|
||||
"AccessNotActive": "Zugriffszeit hat noch nicht begonnen. Sie beginnt in: "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
"SetPasswordNow": "I'll set a password now",
|
||||
"LoginAsGuest": "Continue as a guest",
|
||||
"SignUpToCreateWorkspace": "There are no workspaces with guest access. Please sign up to create your own.",
|
||||
"WhatIsYourName": "What is your name?"
|
||||
"WhatIsYourName": "What is your name?",
|
||||
"AccessExpired": "Access time expired. Please ask for a new access link.",
|
||||
"AccessNotActive": "Access time hasn't started yet. It is starting in: "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
"SetPasswordNow": "Estableceré una contraseña ahora",
|
||||
"LoginAsGuest": "Continuar como invitado",
|
||||
"SignUpToCreateWorkspace": "No hay espacios de trabajo con acceso de invitado. Por favor, regístrese para crear el suyo.",
|
||||
"WhatIsYourName": "¿Cuál es su nombre?"
|
||||
"WhatIsYourName": "¿Cuál es su nombre?",
|
||||
"AccessExpired": "El tiempo de acceso ha expirado. Por favor, solicite un nuevo enlace de acceso.",
|
||||
"AccessNotActive": "El tiempo de acceso aún no ha comenzado. Comienza en: "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
"SetPasswordNow": "Je définirai un mot de passe maintenant",
|
||||
"LoginAsGuest": "Continuer en tant qu'invité",
|
||||
"SignUpToCreateWorkspace": "Aucun espace de travail avec accès invité. Veuillez vous inscrire pour créer le vôtre.",
|
||||
"WhatIsYourName": "Quel est votre nom ?"
|
||||
"WhatIsYourName": "Quel est votre nom ?",
|
||||
"AccessExpired": "Le temps d'accès a expiré. Veuillez demander un nouveau lien d'accès.",
|
||||
"AccessNotActive": "Le temps d'accès n'a pas encore commencé. Il commence dans : "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
"SetPasswordNow": "Imposterò una password ora",
|
||||
"LoginAsGuest": "Continua come ospite",
|
||||
"SignUpToCreateWorkspace": "Nessuna area di lavoro con accesso ospite. Per favore, registrati per crearne una tua.",
|
||||
"WhatIsYourName": "Come ti chiami?"
|
||||
"WhatIsYourName": "Come ti chiami?",
|
||||
"AccessExpired": "Il tempo di accesso è scaduto. Per favore, richiedi un nuovo link di accesso.",
|
||||
"AccessNotActive": "Il tempo di accesso non è ancora iniziato. Inizia tra: "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
"SetPasswordNow": "今すぐパスワードを設定します",
|
||||
"LoginAsGuest": "ゲストとして続行",
|
||||
"SignUpToCreateWorkspace": "ゲストアクセス可能なワークスペースはありません。登録して独自のワークスペースを作成してください。",
|
||||
"WhatIsYourName": "お名前は何ですか?"
|
||||
"WhatIsYourName": "お名前は何ですか?",
|
||||
"AccessExpired": "アクセス時間が期限切れです。新しいアクセスリンクをリクエストしてください。",
|
||||
"AccessNotActive": "アクセス時間はまだ開始されていません。開始まで: "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
"SetPasswordNow": "Vou definir uma senha agora",
|
||||
"LoginAsGuest": "Continuar como convidado",
|
||||
"SignUpToCreateWorkspace": "Não há espaços de trabalho com acesso de convidado. Por favor, cadastre-se para criar o seu.",
|
||||
"WhatIsYourName": "Qual é o seu nome?"
|
||||
"WhatIsYourName": "Qual é o seu nome?",
|
||||
"AccessExpired": "O tempo de acesso expirou. Por favor, solicite um novo link de acesso.",
|
||||
"AccessNotActive": "O tempo de acesso ainda não começou. Começa em: "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
"SetPasswordNow": "Я установлю пароль сейчас",
|
||||
"LoginAsGuest": "Продолжить как гость",
|
||||
"SignUpToCreateWorkspace": "Нет рабочих пространств с гостевым доступом. Пожалуйста, зарегистрируйтесь, чтобы создать своё.",
|
||||
"WhatIsYourName": "Как вас зовут?"
|
||||
"WhatIsYourName": "Как вас зовут?",
|
||||
"AccessExpired": "Время доступа истекло. Пожалуйста, запросите новую ссылку доступа.",
|
||||
"AccessNotActive": "Время доступа еще не началось. Начинается через: "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
"SetPasswordNow": "现在设置密码",
|
||||
"LoginAsGuest": "以访客身份继续",
|
||||
"SignUpToCreateWorkspace": "没有支持访客访问的工作区。请注册以创建您自己的工作区。",
|
||||
"WhatIsYourName": "您的姓名是什么?"
|
||||
"WhatIsYourName": "您的姓名是什么?",
|
||||
"AccessExpired": "访问时间已过期。请申请新的访问链接。",
|
||||
"AccessNotActive": "访问时间尚未开始。开始时间:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { getCurrentLocation, Loading } from '@hcengineering/ui'
|
||||
import { getCurrentLocation, Label, Loading, TimeLeft } from '@hcengineering/ui'
|
||||
import { logIn } from '@hcengineering/workbench'
|
||||
import { trackOAuthCompletion } from '@hcengineering/analytics-providers'
|
||||
import { type LoginInfoRequest, type LoginInfoByToken } from '@hcengineering/account-client'
|
||||
import { OK, PlatformError, unknownError } from '@hcengineering/platform'
|
||||
import platform, { OK, PlatformError, Status, unknownError } from '@hcengineering/platform'
|
||||
|
||||
import type { Field } from '../types'
|
||||
import {
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
let request: LoginInfoRequest | undefined
|
||||
let fields: Field[]
|
||||
let status: Status = OK
|
||||
$: fields =
|
||||
request != null
|
||||
? [
|
||||
@@ -40,7 +41,19 @@
|
||||
return
|
||||
}
|
||||
|
||||
const result = await getLoginInfoFromQuery()
|
||||
let result: LoginInfoByToken | null = null
|
||||
|
||||
try {
|
||||
result = await getLoginInfoFromQuery()
|
||||
} catch (err: any) {
|
||||
if (
|
||||
err instanceof PlatformError &&
|
||||
[platform.status.TokenExpired, platform.status.TokenNotActive].includes(err.status.code)
|
||||
) {
|
||||
status = err.status
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
trackOAuthCompletion(result)
|
||||
await handleLoginInfo(result)
|
||||
@@ -63,8 +76,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
let status = OK
|
||||
|
||||
const action = {
|
||||
i18n: login.string.Proceed,
|
||||
func: async () => {
|
||||
@@ -80,10 +91,34 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let timer: TimeLeft | undefined
|
||||
</script>
|
||||
|
||||
{#if request != null}
|
||||
{#if status?.code === platform.status.TokenExpired}
|
||||
<span class="text ml-8">
|
||||
<Label label={login.string.AccessExpired} />
|
||||
</span>
|
||||
{:else if status?.code === platform.status.TokenNotActive}
|
||||
<span class="text ml-8">
|
||||
<Label label={login.string.AccessNotActive} />
|
||||
<TimeLeft
|
||||
bind:this={timer}
|
||||
time={status.params.notBefore * 1000}
|
||||
showHours={true}
|
||||
on:timeout={() => {
|
||||
window.location.reload()
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
{:else if request != null}
|
||||
<Form caption={login.string.WhatIsYourName} {status} {fields} object={formData} {action} ignoreInitialValidation />
|
||||
{:else}
|
||||
<Loading />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.text {
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -77,6 +77,8 @@ export default mergeIds(loginId, login, {
|
||||
SetPasswordLater: '' as IntlString,
|
||||
SetPasswordNow: '' as IntlString,
|
||||
SignUpToCreateWorkspace: '' as IntlString,
|
||||
WhatIsYourName: '' as IntlString
|
||||
WhatIsYourName: '' as IntlString,
|
||||
AccessExpired: '' as IntlString,
|
||||
AccessNotActive: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
systemAccountUuid
|
||||
} from '@hcengineering/core'
|
||||
import platform, { PlatformError, Status, Severity, getMetadata } from '@hcengineering/platform'
|
||||
import { decodeTokenVerbose } from '@hcengineering/server-token'
|
||||
import { decodeToken, decodeTokenVerbose } from '@hcengineering/server-token'
|
||||
|
||||
import * as utils from '../utils'
|
||||
import { type AccountDB, type SocialId } from '../types'
|
||||
@@ -66,13 +66,14 @@ jest.mock('@hcengineering/platform', () => {
|
||||
// Mock server-token
|
||||
jest.mock('@hcengineering/server-token', () => ({
|
||||
decodeTokenVerbose: jest.fn(),
|
||||
generateToken: jest.fn().mockImplementation((account, workspace, extra, _, grant) => {
|
||||
decodeToken: jest.fn(),
|
||||
generateToken: jest.fn().mockImplementation((account, workspace, extra, _, options) => {
|
||||
let token = `mocked-token-${account}`
|
||||
if (workspace != null) {
|
||||
token += `-${workspace}`
|
||||
}
|
||||
if (grant != null) {
|
||||
token += `-${JSON.stringify(grant)}`
|
||||
if (options != null) {
|
||||
token += `-${JSON.stringify(options)}`
|
||||
}
|
||||
if (extra != null) {
|
||||
token += `-${JSON.stringify(extra)}`
|
||||
@@ -113,7 +114,8 @@ describe('account operations', () => {
|
||||
const mockWorkspace = {
|
||||
uuid: 'workspace-uuid' as WorkspaceUuid,
|
||||
name: 'Test Workspace',
|
||||
url: 'test-workspace'
|
||||
url: 'test-workspace',
|
||||
region: 'us'
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -529,7 +531,8 @@ describe('account operations', () => {
|
||||
},
|
||||
getWorkspaceRole: jest.fn(),
|
||||
assignWorkspace: jest.fn(),
|
||||
updateWorkspaceRole: jest.fn()
|
||||
updateWorkspaceRole: jest.fn(),
|
||||
generatePersonUuid: jest.fn()
|
||||
} as unknown as AccountDB
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -586,7 +589,7 @@ describe('account operations', () => {
|
||||
account: mockPersonId,
|
||||
name: 'John Doe',
|
||||
socialId: 'social-id-1',
|
||||
token: 'mocked-token-test-person-id-workspace-uuid-{}',
|
||||
token: 'mocked-token-test-person-id-workspace-uuid-{}-{}',
|
||||
workspace: mockWorkspaceEu.uuid,
|
||||
workspaceUrl: 'test-workspace',
|
||||
endpoint: 'ws://external:3000',
|
||||
@@ -616,7 +619,7 @@ describe('account operations', () => {
|
||||
account: mockPersonId,
|
||||
name: 'John Doe',
|
||||
socialId: 'social-id-1',
|
||||
token: 'mocked-token-test-person-id-workspace-uuid-{}',
|
||||
token: 'mocked-token-test-person-id-workspace-uuid-{}-{}',
|
||||
workspace: mockWorkspaceUs.uuid,
|
||||
workspaceUrl: 'test-workspace',
|
||||
endpoint: 'ws://internal:3000',
|
||||
@@ -633,7 +636,7 @@ describe('account operations', () => {
|
||||
account: mockPersonId,
|
||||
name: 'John Doe',
|
||||
socialId: 'social-id-1',
|
||||
token: 'mocked-token-test-person-id-workspace-uuid-{}',
|
||||
token: 'mocked-token-test-person-id-workspace-uuid-{}-{}',
|
||||
workspace: mockWorkspaceEu.uuid,
|
||||
workspaceUrl: 'test-workspace',
|
||||
endpoint: 'ws://external:3000',
|
||||
@@ -663,7 +666,7 @@ describe('account operations', () => {
|
||||
account: mockPersonId,
|
||||
name: 'John Doe',
|
||||
socialId: 'social-id-1',
|
||||
token: 'mocked-token-test-person-id-{}'
|
||||
token: 'mocked-token-test-person-id-{}-{}'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -692,6 +695,37 @@ describe('account operations', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('should use sub claim as account when provided', async () => {
|
||||
const subAccount = 'sub-account-uuid' as AccountUuid
|
||||
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
|
||||
account: mockAccount.uuid,
|
||||
workspace: mockWorkspace.uuid,
|
||||
extra: {},
|
||||
sub: subAccount
|
||||
})
|
||||
;(mockDb.person.findOne as jest.Mock).mockResolvedValue({
|
||||
uuid: subAccount,
|
||||
firstName: 'Sub',
|
||||
lastName: 'User'
|
||||
})
|
||||
;(mockDb.socialId.find as jest.Mock).mockResolvedValue([{ _id: 'social-id-1' }])
|
||||
;(mockDb.getWorkspaceRole as jest.Mock).mockResolvedValue(AccountRole.User)
|
||||
;(mockDb.workspace.findOne as jest.Mock).mockResolvedValue(mockWorkspace)
|
||||
|
||||
const result = await getLoginInfoByToken(mockCtx, mockDb, mockBranding, mockToken)
|
||||
|
||||
expect(result).toEqual({
|
||||
account: subAccount, // Should use sub instead of account
|
||||
name: 'Sub User',
|
||||
socialId: 'social-id-1',
|
||||
token: expect.stringContaining(`"sub":"${subAccount}"`),
|
||||
workspace: mockWorkspace.uuid,
|
||||
workspaceUrl: mockWorkspace.url,
|
||||
endpoint: expect.any(String),
|
||||
role: AccountRole.User
|
||||
})
|
||||
})
|
||||
|
||||
describe('with grants', () => {
|
||||
const grantWorkspace = {
|
||||
uuid: 'grant-workspace-uuid' as WorkspaceUuid,
|
||||
@@ -800,12 +834,92 @@ describe('account operations', () => {
|
||||
expect(mockDb.assignWorkspace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should generate UUID and use as sub when grant present without sub', async () => {
|
||||
const generatedUuid = 'generated-uuid' as AccountUuid
|
||||
const grant = {
|
||||
workspace: mockWorkspace.uuid,
|
||||
role: AccountRole.User,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
}
|
||||
|
||||
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
|
||||
account: utils.GUEST_ACCOUNT,
|
||||
extra: {},
|
||||
grant
|
||||
})
|
||||
;(mockDb.account.findOne as jest.Mock).mockResolvedValue(null)
|
||||
;(mockDb.generatePersonUuid as jest.Mock).mockResolvedValue(generatedUuid)
|
||||
;(mockDb.person.findOne as jest.Mock).mockResolvedValue({
|
||||
uuid: generatedUuid,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
})
|
||||
;(mockDb.workspace.findOne as jest.Mock).mockResolvedValue(mockWorkspace)
|
||||
;(mockDb.getWorkspaceRole as jest.Mock).mockResolvedValue(AccountRole.User)
|
||||
|
||||
const result = await getLoginInfoByToken(mockCtx, mockDb, mockBranding, mockToken)
|
||||
|
||||
expect(mockDb.generatePersonUuid).toHaveBeenCalled()
|
||||
expect(result).toEqual({
|
||||
account: generatedUuid,
|
||||
name: 'John Doe',
|
||||
socialId: 'social-id-1',
|
||||
token: expect.stringContaining(`"sub":"${generatedUuid}"`),
|
||||
workspace: mockWorkspace.uuid,
|
||||
workspaceUrl: mockWorkspace.url,
|
||||
endpoint: expect.any(String),
|
||||
role: AccountRole.User
|
||||
})
|
||||
})
|
||||
|
||||
test('should use existing sub', async () => {
|
||||
const existingUuid = 'existing-uuid' as AccountUuid
|
||||
const grant = {
|
||||
workspace: mockWorkspace.uuid,
|
||||
role: AccountRole.User,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
}
|
||||
|
||||
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
|
||||
account: utils.GUEST_ACCOUNT,
|
||||
extra: {},
|
||||
grant,
|
||||
sub: existingUuid
|
||||
})
|
||||
;(mockDb.account.findOne as jest.Mock).mockResolvedValue(null)
|
||||
;(mockDb.person.findOne as jest.Mock).mockResolvedValue({
|
||||
uuid: existingUuid,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
})
|
||||
;(mockDb.workspace.findOne as jest.Mock).mockResolvedValue(mockWorkspace)
|
||||
;(mockDb.getWorkspaceRole as jest.Mock).mockResolvedValue(AccountRole.User)
|
||||
|
||||
const result = await getLoginInfoByToken(mockCtx, mockDb, mockBranding, mockToken)
|
||||
|
||||
expect(mockDb.generatePersonUuid).not.toHaveBeenCalled()
|
||||
expect(result).toEqual({
|
||||
account: existingUuid,
|
||||
name: 'John Doe',
|
||||
socialId: 'social-id-1',
|
||||
token: expect.stringContaining(`"sub":"${existingUuid}"`),
|
||||
workspace: mockWorkspace.uuid,
|
||||
workspaceUrl: mockWorkspace.url,
|
||||
endpoint: expect.any(String),
|
||||
role: AccountRole.User
|
||||
})
|
||||
})
|
||||
|
||||
test('should throw error for grant with system account', async () => {
|
||||
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
|
||||
account: systemAccountUuid,
|
||||
account: utils.GUEST_ACCOUNT,
|
||||
extra: {},
|
||||
grant: { workspace: grantWorkspace.uuid, role: grantRole }
|
||||
grant: { workspace: grantWorkspace.uuid, role: grantRole },
|
||||
sub: systemAccountUuid
|
||||
})
|
||||
;(mockDb.account.findOne as jest.Mock).mockResolvedValue(null)
|
||||
|
||||
await expect(getLoginInfoByToken(mockCtx, mockDb, mockBranding, mockToken)).rejects.toThrow(
|
||||
new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
@@ -836,6 +950,63 @@ describe('account operations', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test('should handle token with nbf and exp claims', async () => {
|
||||
const notBefore = Math.floor(Date.now() / 1000) - 3600 // 1 hour ago
|
||||
const expiration = Math.floor(Date.now() / 1000) + 3600 // 1 hour from now
|
||||
|
||||
;(decodeTokenVerbose as jest.Mock).mockReturnValue({
|
||||
account: mockAccount.uuid,
|
||||
workspace: mockWorkspace.uuid,
|
||||
extra: {},
|
||||
nbf: notBefore,
|
||||
exp: expiration
|
||||
})
|
||||
;(mockDb.person.findOne as jest.Mock).mockResolvedValue({
|
||||
uuid: mockAccount.uuid,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
})
|
||||
;(mockDb.socialId.find as jest.Mock).mockResolvedValue([{ _id: 'social-id-1' }])
|
||||
;(mockDb.getWorkspaceRole as jest.Mock).mockResolvedValue(AccountRole.User)
|
||||
;(mockDb.workspace.findOne as jest.Mock).mockResolvedValue(mockWorkspace)
|
||||
|
||||
const result = await getLoginInfoByToken(mockCtx, mockDb, mockBranding, mockToken)
|
||||
|
||||
expect(result).toEqual({
|
||||
account: mockAccount.uuid,
|
||||
name: 'John Doe',
|
||||
socialId: 'social-id-1',
|
||||
token: expect.stringContaining(`"nbf":${notBefore},"exp":${expiration}`),
|
||||
workspace: mockWorkspace.uuid,
|
||||
workspaceUrl: mockWorkspace.url,
|
||||
endpoint: expect.any(String),
|
||||
role: AccountRole.User
|
||||
})
|
||||
})
|
||||
|
||||
test('should throw error when token is not yet active', async () => {
|
||||
const futureTime = Math.floor(Date.now() / 1000) + 3600 // 1 hour in the future
|
||||
|
||||
;(decodeToken as jest.Mock).mockReturnValue({ nbf: futureTime })
|
||||
;(decodeTokenVerbose as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Token not yet active')
|
||||
})
|
||||
|
||||
await expect(getLoginInfoByToken(mockCtx, mockDb, mockBranding, mockToken)).rejects.toThrow(
|
||||
new PlatformError(new Status(Severity.ERROR, platform.status.TokenNotActive, { notBefore: futureTime }))
|
||||
)
|
||||
})
|
||||
|
||||
test('should throw error when token has expired', async () => {
|
||||
;(decodeTokenVerbose as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Token expired')
|
||||
})
|
||||
|
||||
await expect(getLoginInfoByToken(mockCtx, mockDb, mockBranding, mockToken)).rejects.toThrow(
|
||||
new PlatformError(new Status(Severity.ERROR, platform.status.TokenExpired, {}))
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('releaseSocialId', () => {
|
||||
@@ -956,10 +1127,13 @@ describe('account operations', () => {
|
||||
|
||||
expect(mockDb.generatePersonUuid).toHaveBeenCalled()
|
||||
expect(result).toBe(
|
||||
`${frontUrl}/login/auth?token=mocked-token-generated-person-uuid-${JSON.stringify({
|
||||
workspace: 'workspace-uuid',
|
||||
role: 'USER',
|
||||
grantedBy: 'account-uuid'
|
||||
`${frontUrl}/login/auth?token=mocked-token-b6996120-416f-49cd-841e-e4a5d2e49c9b-${JSON.stringify({
|
||||
grant: {
|
||||
workspace: 'workspace-uuid',
|
||||
role: 'USER',
|
||||
grantedBy: 'account-uuid'
|
||||
},
|
||||
sub: 'generated-person-uuid'
|
||||
})}`
|
||||
)
|
||||
})
|
||||
@@ -977,12 +1151,15 @@ describe('account operations', () => {
|
||||
|
||||
expect(mockDb.generatePersonUuid).toHaveBeenCalled()
|
||||
expect(result).toBe(
|
||||
`${frontUrl}/login/auth?token=mocked-token-generated-person-uuid-${JSON.stringify({
|
||||
workspace: mockWorkspace.uuid,
|
||||
role: AccountRole.User,
|
||||
grantedBy: 'account-uuid',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
`${frontUrl}/login/auth?token=mocked-token-b6996120-416f-49cd-841e-e4a5d2e49c9b-${JSON.stringify({
|
||||
grant: {
|
||||
workspace: mockWorkspace.uuid,
|
||||
role: AccountRole.User,
|
||||
grantedBy: 'account-uuid',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
},
|
||||
sub: 'generated-person-uuid'
|
||||
})}`
|
||||
)
|
||||
})
|
||||
@@ -1000,11 +1177,14 @@ describe('account operations', () => {
|
||||
|
||||
expect(mockDb.generatePersonUuid).toHaveBeenCalled()
|
||||
expect(result).toBe(
|
||||
`${frontUrl}/login/auth?token=mocked-token-generated-person-uuid-${JSON.stringify({
|
||||
workspace: mockWorkspace.uuid,
|
||||
role: AccountRole.User,
|
||||
grantedBy: 'account-uuid',
|
||||
extra: { param1: 'value1', param2: 'value2' }
|
||||
`${frontUrl}/login/auth?token=mocked-token-b6996120-416f-49cd-841e-e4a5d2e49c9b-${JSON.stringify({
|
||||
grant: {
|
||||
workspace: mockWorkspace.uuid,
|
||||
role: AccountRole.User,
|
||||
grantedBy: 'account-uuid',
|
||||
extra: { param1: 'value1', param2: 'value2' }
|
||||
},
|
||||
sub: 'generated-person-uuid'
|
||||
})}`
|
||||
)
|
||||
})
|
||||
@@ -1022,10 +1202,13 @@ describe('account operations', () => {
|
||||
|
||||
expect(mockDb.generatePersonUuid).toHaveBeenCalled()
|
||||
expect(result).toBe(
|
||||
`${frontUrl}/login/auth?token=mocked-token-generated-person-uuid-${JSON.stringify({
|
||||
workspace: 'workspace-uuid',
|
||||
role: 'USER',
|
||||
grantedBy: 'account-uuid'
|
||||
`${frontUrl}/login/auth?token=mocked-token-b6996120-416f-49cd-841e-e4a5d2e49c9b-${JSON.stringify({
|
||||
grant: {
|
||||
workspace: 'workspace-uuid',
|
||||
role: 'USER',
|
||||
grantedBy: 'account-uuid'
|
||||
},
|
||||
sub: 'generated-person-uuid'
|
||||
})}&navigateUrl=${encodeURIComponent(navigateUrl)}`
|
||||
)
|
||||
})
|
||||
@@ -1058,6 +1241,86 @@ describe('account operations', () => {
|
||||
|
||||
expect(mockDb.generatePersonUuid).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should create non-personalized access link without UUID with notBefore and expiration', async () => {
|
||||
const notBefore = 1703894400 // December 30, 2023 00:00:00 UTC in seconds
|
||||
const expiration = 1704067200 // January 1, 2024 00:00:00 UTC in seconds
|
||||
;(mockDb.account.findOne as jest.Mock).mockResolvedValue(mockAccount)
|
||||
;(mockDb.workspace.findOne as jest.Mock).mockResolvedValue(mockWorkspace)
|
||||
;(mockDb.getWorkspaceRole as jest.Mock).mockResolvedValue(AccountRole.Maintainer)
|
||||
|
||||
const result = await createAccessLink(mockCtx, mockDb, mockBranding, mockToken, {
|
||||
role: AccountRole.User,
|
||||
personalized: false,
|
||||
notBefore,
|
||||
expiration
|
||||
})
|
||||
|
||||
expect(result).toBe(
|
||||
`${frontUrl}/login/auth?token=mocked-token-b6996120-416f-49cd-841e-e4a5d2e49c9b-${JSON.stringify({
|
||||
grant: {
|
||||
workspace: mockWorkspace.uuid,
|
||||
role: AccountRole.User,
|
||||
grantedBy: 'account-uuid'
|
||||
},
|
||||
exp: expiration,
|
||||
nbf: notBefore
|
||||
})}`
|
||||
)
|
||||
expect(mockDb.generatePersonUuid).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should throw error when notBefore is in milliseconds', async () => {
|
||||
;(mockDb.account.findOne as jest.Mock).mockResolvedValue(mockAccount)
|
||||
;(mockDb.workspace.findOne as jest.Mock).mockResolvedValue(mockWorkspace)
|
||||
;(mockDb.getWorkspaceRole as jest.Mock).mockResolvedValue(AccountRole.Maintainer)
|
||||
|
||||
await expect(
|
||||
createAccessLink(mockCtx, mockDb, mockBranding, mockToken, {
|
||||
role: AccountRole.User,
|
||||
notBefore: Date.now() // Milliseconds instead of seconds
|
||||
})
|
||||
).rejects.toThrow(new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})))
|
||||
|
||||
expect(mockCtx.error).toHaveBeenCalledWith(
|
||||
'Not before appears to be in milliseconds instead of seconds',
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
test('should throw error when expiration is in milliseconds', async () => {
|
||||
;(mockDb.account.findOne as jest.Mock).mockResolvedValue(mockAccount)
|
||||
;(mockDb.workspace.findOne as jest.Mock).mockResolvedValue(mockWorkspace)
|
||||
;(mockDb.getWorkspaceRole as jest.Mock).mockResolvedValue(AccountRole.Maintainer)
|
||||
|
||||
await expect(
|
||||
createAccessLink(mockCtx, mockDb, mockBranding, mockToken, {
|
||||
role: AccountRole.User,
|
||||
expiration: Date.now() // Milliseconds instead of seconds
|
||||
})
|
||||
).rejects.toThrow(new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})))
|
||||
|
||||
expect(mockCtx.error).toHaveBeenCalledWith(
|
||||
'Expiration appears to be in milliseconds instead of seconds',
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
test('should throw error when expiration is before notBefore', async () => {
|
||||
;(mockDb.account.findOne as jest.Mock).mockResolvedValue(mockAccount)
|
||||
;(mockDb.workspace.findOne as jest.Mock).mockResolvedValue(mockWorkspace)
|
||||
;(mockDb.getWorkspaceRole as jest.Mock).mockResolvedValue(AccountRole.Maintainer)
|
||||
|
||||
await expect(
|
||||
createAccessLink(mockCtx, mockDb, mockBranding, mockToken, {
|
||||
role: AccountRole.User,
|
||||
notBefore: 1000,
|
||||
expiration: 900
|
||||
})
|
||||
).rejects.toThrow(new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})))
|
||||
|
||||
expect(mockCtx.error).toHaveBeenCalledWith('Expiration time must be after Not Before time', expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
describe('authentication operations', () => {
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform'
|
||||
import { decodeTokenVerbose, generateToken, type PermissionsGrant } from '@hcengineering/server-token'
|
||||
import { decodeToken, decodeTokenVerbose, generateToken, type PermissionsGrant } from '@hcengineering/server-token'
|
||||
|
||||
import { isAdminEmail } from './admin'
|
||||
import { accountPlugin } from './plugin'
|
||||
@@ -665,9 +665,13 @@ export async function createAccessLink (
|
||||
extra?: string
|
||||
navigateUrl?: string
|
||||
spaces?: string[]
|
||||
|
||||
notBefore?: number
|
||||
expiration?: number
|
||||
personalized?: boolean
|
||||
}
|
||||
): Promise<string> {
|
||||
const { role, firstName, lastName, navigateUrl, spaces } = params
|
||||
const { role, firstName, lastName, navigateUrl, spaces, notBefore, expiration, personalized = true } = params
|
||||
const { account, workspace: workspaceUuid, extra } = decodeTokenVerbose(ctx, token)
|
||||
|
||||
const currentAccount = await db.account.findOne({ uuid: account })
|
||||
@@ -675,6 +679,10 @@ export async function createAccessLink (
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account }))
|
||||
}
|
||||
|
||||
if (workspaceUuid == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
|
||||
}
|
||||
|
||||
const workspace = await db.workspace.findOne({ uuid: workspaceUuid })
|
||||
if (workspace == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
|
||||
@@ -691,12 +699,30 @@ export async function createAccessLink (
|
||||
}
|
||||
}
|
||||
|
||||
const RECENT_PAST_MS = 1577836800000 // January 1, 2020 in milliseconds
|
||||
|
||||
if (notBefore !== undefined && notBefore > RECENT_PAST_MS) {
|
||||
ctx.error('Not before appears to be in milliseconds instead of seconds', { nbf: notBefore })
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
|
||||
}
|
||||
|
||||
if (expiration !== undefined && expiration > RECENT_PAST_MS) {
|
||||
ctx.error('Expiration appears to be in milliseconds instead of seconds', { exp: expiration })
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
|
||||
}
|
||||
|
||||
if (notBefore !== undefined && expiration !== undefined && expiration <= notBefore) {
|
||||
ctx.error('Expiration time must be after Not Before time', { nbf: notBefore, exp: expiration })
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
|
||||
}
|
||||
|
||||
const callerRole = await db.getWorkspaceRole(account, workspace.uuid)
|
||||
verifyAllowedRole(callerRole, AccountRole.User, extra)
|
||||
verifyAllowedRole(callerRole, role, extra)
|
||||
|
||||
const newUuid = await db.generatePersonUuid()
|
||||
const accessToken = generateToken(newUuid, undefined, undefined, undefined, {
|
||||
const newUuid = personalized ? await db.generatePersonUuid() : undefined
|
||||
|
||||
const grant = {
|
||||
workspace: workspaceUuid,
|
||||
role,
|
||||
grantedBy: account,
|
||||
@@ -704,16 +730,28 @@ export async function createAccessLink (
|
||||
lastName,
|
||||
extra: extraObj,
|
||||
spaces
|
||||
})
|
||||
let path = `/login/auth?token=${accessToken}`
|
||||
if (navigateUrl != null) {
|
||||
path += `&navigateUrl=${encodeURIComponent(navigateUrl.trim())}`
|
||||
}
|
||||
|
||||
const front = getFrontUrl(branding)
|
||||
const link = concatLink(front, path)
|
||||
try {
|
||||
const accessToken = generateToken(GUEST_ACCOUNT, undefined, undefined, undefined, {
|
||||
grant,
|
||||
sub: newUuid,
|
||||
exp: expiration,
|
||||
nbf: notBefore
|
||||
})
|
||||
let path = `/login/auth?token=${accessToken}`
|
||||
if (navigateUrl != null) {
|
||||
path += `&navigateUrl=${encodeURIComponent(navigateUrl.trim())}`
|
||||
}
|
||||
|
||||
return link
|
||||
const front = getFrontUrl(branding)
|
||||
const link = concatLink(front, path)
|
||||
|
||||
return link
|
||||
} catch (err: any) {
|
||||
ctx.error('Failed to create access link', { err })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function createInviteLink (
|
||||
@@ -1578,12 +1616,30 @@ export async function getLoginInfoByToken (
|
||||
let workspaceUuid: WorkspaceUuid
|
||||
let extra: any
|
||||
let grant: PermissionsGrant | undefined
|
||||
let sub: AccountUuid | undefined
|
||||
let account: AccountUuid | undefined
|
||||
let nbf: number | undefined
|
||||
let exp: number | undefined
|
||||
|
||||
try {
|
||||
;({ account: accountUuid, workspace: workspaceUuid, extra, grant } = decodeTokenVerbose(ctx, token))
|
||||
;({ account, workspace: workspaceUuid, extra, grant, nbf, exp, sub } = decodeTokenVerbose(ctx, token))
|
||||
if (grant != null && sub == null) {
|
||||
sub = (await db.generatePersonUuid()) as AccountUuid
|
||||
}
|
||||
accountUuid = sub ?? account
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
ctx.error('Invalid token', { token })
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Unauthorized, {}))
|
||||
ctx.error('Invalid token', { token, errMsg: err.message })
|
||||
switch (err.message) {
|
||||
case 'Token not yet active': {
|
||||
const { nbf } = decodeToken(token, false)
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.TokenNotActive, { notBefore: nbf }))
|
||||
}
|
||||
case 'Token expired':
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.TokenExpired, {}))
|
||||
default:
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Unauthorized, {}))
|
||||
}
|
||||
}
|
||||
|
||||
if (accountUuid == null) {
|
||||
@@ -1595,7 +1651,6 @@ export async function getLoginInfoByToken (
|
||||
const isAdmin = extra?.admin === 'true'
|
||||
|
||||
// Check if token has grants and create automatic account if needed
|
||||
// NOTE: grants with open account UUID are not currently supported
|
||||
if (grant != null) {
|
||||
if (workspaceUuid != null) {
|
||||
ctx.error('Grants are not allowed in workspace-specific tokens', { workspaceUuid, grant })
|
||||
@@ -1690,7 +1745,7 @@ export async function getLoginInfoByToken (
|
||||
account: accountUuid,
|
||||
name: getPersonName(person),
|
||||
socialId: socialId?._id,
|
||||
token: generateToken(accountUuid, workspaceUuid, extra, undefined, grant)
|
||||
token: generateToken(accountUuid, workspaceUuid, extra, undefined, { grant, nbf, exp, sub })
|
||||
}
|
||||
|
||||
if (!isSystem) {
|
||||
@@ -1853,7 +1908,8 @@ export async function getSocialIds (
|
||||
params: { confirmed: boolean, includeDeleted: boolean }
|
||||
): Promise<SocialId[]> {
|
||||
const { confirmed = true, includeDeleted = false } = params
|
||||
const { account } = decodeTokenVerbose(ctx, token)
|
||||
const { account: accountUuid, sub } = decodeTokenVerbose(ctx, token)
|
||||
const account = sub ?? accountUuid
|
||||
|
||||
// do not expose not-confirmed social ids for now
|
||||
if (!confirmed) {
|
||||
@@ -2180,7 +2236,7 @@ async function exchangeGuestToken (
|
||||
)
|
||||
}
|
||||
|
||||
return generateToken(GUEST_ACCOUNT as PersonUuid, workspace.uuid, { linkId, guest: 'true' })
|
||||
return generateToken(GUEST_ACCOUNT, workspace.uuid, { linkId, guest: 'true' })
|
||||
}
|
||||
|
||||
return token
|
||||
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
} from './types'
|
||||
import { isAdminEmail } from './admin'
|
||||
|
||||
export const GUEST_ACCOUNT = 'b6996120-416f-49cd-841e-e4a5d2e49c9b'
|
||||
export const GUEST_ACCOUNT = 'b6996120-416f-49cd-841e-e4a5d2e49c9b' as PersonUuid
|
||||
|
||||
export async function getAccountDB (
|
||||
uri: string,
|
||||
@@ -612,6 +612,9 @@ export async function selectWorkspace (
|
||||
let accountUuid: AccountUuid
|
||||
let extra: Record<string, any> | undefined
|
||||
let grant: PermissionsGrant | undefined
|
||||
let sub: AccountUuid | undefined
|
||||
let exp: number | undefined
|
||||
let nbf: number | undefined
|
||||
try {
|
||||
const decodedToken = decodeTokenVerbose(ctx, token ?? '')
|
||||
accountUuid = decodedToken.account
|
||||
@@ -620,6 +623,9 @@ export async function selectWorkspace (
|
||||
}
|
||||
extra = decodedToken.extra
|
||||
grant = decodedToken.grant
|
||||
sub = decodedToken.sub
|
||||
exp = decodedToken.exp
|
||||
nbf = decodedToken.nbf
|
||||
} catch (e) {
|
||||
if (workspace?.allowReadOnlyGuest === true) {
|
||||
accountUuid = readOnlyGuestAccountUuid
|
||||
@@ -668,7 +674,12 @@ export async function selectWorkspace (
|
||||
if (accountUuid === systemAccountUuid) {
|
||||
return {
|
||||
account: accountUuid,
|
||||
token: generateToken(accountUuid, workspace.uuid, extra, undefined, grant),
|
||||
token: generateToken(accountUuid, workspace.uuid, extra, undefined, {
|
||||
grant,
|
||||
sub,
|
||||
exp,
|
||||
nbf
|
||||
}),
|
||||
endpoint: getEndpoint(workspace.uuid, workspace.region, getKind(workspace.region)),
|
||||
workspace: workspace.uuid,
|
||||
workspaceUrl: workspace.url,
|
||||
@@ -725,7 +736,12 @@ export async function selectWorkspace (
|
||||
|
||||
return {
|
||||
account: accountUuid,
|
||||
token: generateToken(accountUuid, workspace.uuid, extra, undefined, grant),
|
||||
token: generateToken(accountUuid, workspace.uuid, extra, undefined, {
|
||||
grant,
|
||||
sub,
|
||||
exp,
|
||||
nbf
|
||||
}),
|
||||
endpoint: getEndpoint(workspace.uuid, workspace.region, getKind(workspace.region)),
|
||||
workspace: workspace.uuid,
|
||||
workspaceUrl: workspace.url,
|
||||
|
||||
@@ -12,6 +12,10 @@ export interface Token {
|
||||
workspace: WorkspaceUuid
|
||||
extra?: Record<string, any>
|
||||
grant?: PermissionsGrant
|
||||
|
||||
sub?: AccountUuid // Subject
|
||||
exp?: number // Expiration, seconds since epoch
|
||||
nbf?: number // Not valid before, seconds since epoch
|
||||
}
|
||||
|
||||
// Permissions grant provides the token presenter access to a specific workspace
|
||||
@@ -53,7 +57,12 @@ export function generateToken (
|
||||
workspaceUuid?: WorkspaceUuid,
|
||||
extra?: Record<string, string>,
|
||||
secret?: string,
|
||||
grant?: PermissionsGrant
|
||||
options?: {
|
||||
grant?: PermissionsGrant
|
||||
nbf?: number
|
||||
exp?: number
|
||||
sub?: PersonUuid
|
||||
}
|
||||
): string {
|
||||
if (!validate(accountUuid)) {
|
||||
throw new TokenError(`Invalid account uuid: "${accountUuid}"`)
|
||||
@@ -61,10 +70,15 @@ export function generateToken (
|
||||
if (workspaceUuid !== undefined && !validate(workspaceUuid)) {
|
||||
throw new TokenError(`Invalid workspace uuid: "${workspaceUuid}"`)
|
||||
}
|
||||
const { grant, nbf, exp, sub } = options ?? {}
|
||||
if (grant?.workspace !== undefined && !validate(grant?.workspace)) {
|
||||
throw new TokenError(`Invalid grant workspace uuid: "${grant?.workspace}"`)
|
||||
}
|
||||
|
||||
if (grant != null && sub == null && (nbf == null || exp == null)) {
|
||||
throw new TokenError('nbf and exp are required when sub is not provided')
|
||||
}
|
||||
|
||||
const service = getMetadata(serverPlugin.metadata.Service)
|
||||
if (service !== undefined) {
|
||||
extra = { service, ...extra }
|
||||
@@ -88,7 +102,10 @@ export function generateToken (
|
||||
...(extra !== undefined ? { extra } : {}),
|
||||
account: accountUuid,
|
||||
workspace: workspaceUuid,
|
||||
grant: sanitizedGrant
|
||||
grant: sanitizedGrant,
|
||||
sub,
|
||||
exp,
|
||||
nbf
|
||||
},
|
||||
secret ?? getSecret()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user