@ossy/platform 3.11.0 → 3.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -7
- package/src/index.js +1 -0
- package/src/server.js +7 -0
- package/src/test/flow-runner.js +186 -67
- package/src/test/test.util.js +37 -0
- package/src/verify-sign-in-session.js +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/platform",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.1",
|
|
4
4
|
"description": "Ossy application server runtime",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"./test/flow-runner.js": "./src/test/flow-runner.js",
|
|
28
28
|
"./test/playwright.config.js": "./src/test/playwright.config.js",
|
|
29
29
|
"./locale": "./src/locale.js",
|
|
30
|
+
"./verify-sign-in-session": "./src/verify-sign-in-session.js",
|
|
30
31
|
"./storage-keys": "./src/storage/storage-keys.js",
|
|
31
32
|
"./mcp": "./src/mcp/mount-platform-mcp.js"
|
|
32
33
|
},
|
|
@@ -45,16 +46,16 @@
|
|
|
45
46
|
"@aws-sdk/util-format-url": "^3.972.17",
|
|
46
47
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
47
48
|
"@ossy/config": "^3.0.9",
|
|
48
|
-
"@ossy/event-store": "^3.11.
|
|
49
|
+
"@ossy/event-store": "^3.11.1",
|
|
49
50
|
"@ossy/locale": "^3.4.0",
|
|
50
51
|
"@ossy/manifest": "^3.9.0",
|
|
51
52
|
"@ossy/observability": "^3.0.9",
|
|
52
53
|
"@ossy/policies": "^3.0.9",
|
|
53
54
|
"@ossy/schema": "^3.8.0",
|
|
54
|
-
"@ossy/sdk": "^3.
|
|
55
|
-
"@ossy/tokens": "^3.
|
|
56
|
-
"@ossy/users": "^3.11.
|
|
57
|
-
"@ossy/workspaces": "^3.11.
|
|
55
|
+
"@ossy/sdk": "^3.11.1",
|
|
56
|
+
"@ossy/tokens": "^3.11.1",
|
|
57
|
+
"@ossy/users": "^3.11.1",
|
|
58
|
+
"@ossy/workspaces": "^3.11.1",
|
|
58
59
|
"cookie-parser": "^1.4.7",
|
|
59
60
|
"dotenv": ">=16.0.0 <18.0.0",
|
|
60
61
|
"express": ">=5.0.0 <6.0.0",
|
|
@@ -75,5 +76,5 @@
|
|
|
75
76
|
"Dockerfile",
|
|
76
77
|
"docker-healthcheck.js"
|
|
77
78
|
],
|
|
78
|
-
"gitHead": "
|
|
79
|
+
"gitHead": "f279de3535b6bc76631cbc6b943dae8f92a1565f"
|
|
79
80
|
}
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { validateSchemasForImport, ALLOWED_FIELD_TYPES, normalizeFieldType, reso
|
|
|
8
8
|
export { IntegrationService } from './integration.service.js'
|
|
9
9
|
export { ConfigService } from './config.service.js'
|
|
10
10
|
export { ActionService } from './actions/action.service.js'
|
|
11
|
+
export { applyVerifySignInSessionCookies } from './verify-sign-in-session.js'
|
|
11
12
|
export { TokenService } from './token.service.js'
|
|
12
13
|
export { UsersMiddleware } from './users.middleware.js'
|
|
13
14
|
export { WorkspacesMiddleware } from './workspaces.middleware.js'
|
package/src/server.js
CHANGED
|
@@ -43,8 +43,11 @@ import {
|
|
|
43
43
|
import {
|
|
44
44
|
clearAuthCookie,
|
|
45
45
|
clearWorkspaceFromUserAppSettings,
|
|
46
|
+
mergeUserAppSettingsCookie,
|
|
46
47
|
readWorkspaceIdFromCookies,
|
|
48
|
+
setAuthCookie,
|
|
47
49
|
} from './user-app-settings.js'
|
|
50
|
+
import { applyVerifySignInSessionCookies } from './verify-sign-in-session.js'
|
|
48
51
|
const log = createLogger('@ossy/platform')
|
|
49
52
|
const MONGO_TIMEOUT_MS = resolveTimeoutMs('OSSY_MONGO_TIMEOUT_MS', 10_000)
|
|
50
53
|
const SSR_RENDER_TIMEOUT_MS = resolveTimeoutMs('OSSY_SSR_RENDER_TIMEOUT_MS', 30_000)
|
|
@@ -427,6 +430,9 @@ export async function startServer (options = {}) {
|
|
|
427
430
|
clearAuthCookie(res)
|
|
428
431
|
clearWorkspaceFromUserAppSettings(req, res)
|
|
429
432
|
}
|
|
433
|
+
if (actionId === '@ossy/authentication/actions/verify-sign-in' && result?.token) {
|
|
434
|
+
await applyVerifySignInSessionCookies(req, res, result)
|
|
435
|
+
}
|
|
430
436
|
res.json(result ?? { ok: true })
|
|
431
437
|
} catch (err) {
|
|
432
438
|
// Still clear auth on sign-out failures so a broken session can recover.
|
|
@@ -670,6 +676,7 @@ export default startServer
|
|
|
670
676
|
export { loadLayoutsById, resolvePageLayoutRender }
|
|
671
677
|
export { ConfigService } from './config.service.js'
|
|
672
678
|
export { ActionService } from './actions/action.service.js'
|
|
679
|
+
export { applyVerifySignInSessionCookies } from './verify-sign-in-session.js'
|
|
673
680
|
export { IntegrationService } from './integration.service.js'
|
|
674
681
|
export { StorageClient } from './storage/storage.client.js'
|
|
675
682
|
export { originalObjectKey, derivativeObjectKey, assertStorageKey } from './storage/storage-keys.js'
|
package/src/test/flow-runner.js
CHANGED
|
@@ -405,13 +405,13 @@ export async function runFlow (flow, options = {}) {
|
|
|
405
405
|
const selector = actionSelector(actionId, matchers)
|
|
406
406
|
const actionTimeout = step.timeout ?? 15000
|
|
407
407
|
const actionDeadline = Date.now() + actionTimeout
|
|
408
|
-
// SSR markup is visible before React attaches onClick. Wait
|
|
409
|
-
//
|
|
410
|
-
//
|
|
408
|
+
// SSR markup is visible before React attaches onClick. Wait for visibility,
|
|
409
|
+
// then click: form submits use remount-safe evaluate + requestSubmit; other
|
|
410
|
+
// CTAs use Playwright click (programmatic el.click() often skips React onClick).
|
|
411
411
|
//
|
|
412
|
-
// Re-query the live node after
|
|
413
|
-
//
|
|
414
|
-
//
|
|
412
|
+
// Re-query the live node after settle: concurrent auth/workspace fetches can
|
|
413
|
+
// remount the tree and detach Playwright locators mid-scroll (and wipe
|
|
414
|
+
// uncontrolled form state). Retry within the action timeout.
|
|
415
415
|
// Prefer an actionable match when duplicates exist (e.g. hero CTA under a page overlay).
|
|
416
416
|
let clicked = false
|
|
417
417
|
let lastError
|
|
@@ -432,52 +432,63 @@ export async function runFlow (flow, options = {}) {
|
|
|
432
432
|
state: 'visible',
|
|
433
433
|
timeout: Math.min(5000, Math.max(250, actionDeadline - Date.now())),
|
|
434
434
|
})
|
|
435
|
-
// Scroll inside evaluate via a fresh querySelector — Playwright's
|
|
436
|
-
// scrollIntoViewIfNeeded holds a locator through a stability wait and
|
|
437
|
-
// throws "not attached" when GetWorkspace remounts PackageServiceToggle.
|
|
438
435
|
const fieldRestore = { ...(context.fields ?? {}) }
|
|
439
436
|
if (context.email != null && fieldRestore.email == null) {
|
|
440
437
|
fieldRestore.email = context.email
|
|
441
438
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
desc?.set?.call(input, asText)
|
|
465
|
-
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
466
|
-
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
439
|
+
// Form submits: restore fills + requestSubmit in one evaluate (remount-safe).
|
|
440
|
+
// Other CTAs (Enable, Publish, OpenExport): Playwright click — programmatic
|
|
441
|
+
// el.click() often never invokes React onClick (no /actions POST; publish stays off).
|
|
442
|
+
const isSubmitButton = await candidate.evaluate((el) => {
|
|
443
|
+
const form = (el instanceof HTMLButtonElement || el instanceof HTMLInputElement)
|
|
444
|
+
? el.form
|
|
445
|
+
: el.closest?.('form')
|
|
446
|
+
if (!form) return false
|
|
447
|
+
return (el instanceof HTMLButtonElement || el instanceof HTMLInputElement)
|
|
448
|
+
? el.type === 'submit'
|
|
449
|
+
: el.getAttribute?.('type') === 'submit'
|
|
450
|
+
})
|
|
451
|
+
if (isSubmitButton) {
|
|
452
|
+
await page.evaluate(async ({ selector: sel, fields }) => {
|
|
453
|
+
await new Promise((resolve) => {
|
|
454
|
+
requestAnimationFrame(() => requestAnimationFrame(resolve))
|
|
455
|
+
})
|
|
456
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
457
|
+
const el = document.querySelector(sel)
|
|
458
|
+
if (!el) throw new Error(`Action control disappeared after settle: ${sel}`)
|
|
459
|
+
if (typeof el.scrollIntoView === 'function') {
|
|
460
|
+
el.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
467
461
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
462
|
+
const form = (el instanceof HTMLButtonElement || el instanceof HTMLInputElement)
|
|
463
|
+
? el.form
|
|
464
|
+
: el.closest?.('form')
|
|
465
|
+
if (form && fields && typeof fields === 'object') {
|
|
466
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
467
|
+
if (value == null || typeof value === 'object') continue
|
|
468
|
+
const input = form.querySelector(`[name="${name}"]`)
|
|
469
|
+
if (!input || input.disabled) continue
|
|
470
|
+
if (input instanceof HTMLInputElement && input.type === 'file') continue
|
|
471
|
+
const asText = String(value)
|
|
472
|
+
if (input.value === asText) continue
|
|
473
|
+
const proto = Object.getPrototypeOf(input)
|
|
474
|
+
const desc = Object.getOwnPropertyDescriptor(proto, 'value')
|
|
475
|
+
desc?.set?.call(input, asText)
|
|
476
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
477
|
+
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (form && typeof form.requestSubmit === 'function') {
|
|
481
|
+
form.requestSubmit(el)
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
if (typeof el.click === 'function') el.click()
|
|
485
|
+
}, { selector, fields: fieldRestore })
|
|
486
|
+
} else {
|
|
487
|
+
await candidate.scrollIntoViewIfNeeded().catch(() => {})
|
|
488
|
+
await candidate.click({
|
|
489
|
+
timeout: Math.min(5000, Math.max(250, actionDeadline - Date.now())),
|
|
490
|
+
})
|
|
491
|
+
}
|
|
481
492
|
clicked = true
|
|
482
493
|
break
|
|
483
494
|
} catch (err) {
|
|
@@ -538,6 +549,42 @@ export async function runFlow (flow, options = {}) {
|
|
|
538
549
|
continue
|
|
539
550
|
}
|
|
540
551
|
|
|
552
|
+
if (step.inputs != null) {
|
|
553
|
+
if (!page) throw new Error('inputs step requires a Playwright page')
|
|
554
|
+
const inputsTimeout = step.timeout ?? 15000
|
|
555
|
+
for (const [selector, valueSpec] of Object.entries(step.inputs)) {
|
|
556
|
+
const value = resolveContextValue(valueSpec, context)
|
|
557
|
+
if (value == null) {
|
|
558
|
+
throw new Error(`inputs step: no value resolved for selector "${selector}"`)
|
|
559
|
+
}
|
|
560
|
+
const resolvedSelector = interpolateContextString(selector, context, {
|
|
561
|
+
escape: escapeCssAttrValue,
|
|
562
|
+
})
|
|
563
|
+
const locator = page.locator(resolvedSelector).first()
|
|
564
|
+
await locator.waitFor({ state: 'visible', timeout: inputsTimeout })
|
|
565
|
+
const asText = String(value)
|
|
566
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
567
|
+
await locator.fill(asText)
|
|
568
|
+
await locator.blur().catch(() => {})
|
|
569
|
+
try {
|
|
570
|
+
await expectFn(locator).toHaveValue(asText, { timeout: 2000 })
|
|
571
|
+
break
|
|
572
|
+
} catch (err) {
|
|
573
|
+
if (attempt === 2) throw err
|
|
574
|
+
await page.waitForTimeout(150)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// Remount-safe action submits restore from context.fields by input name.
|
|
578
|
+
// Keep ad-hoc fills (e.g. sign-in code) in that map so settle remounts
|
|
579
|
+
// do not wipe values before requestSubmit.
|
|
580
|
+
const name = await locator.getAttribute('name')
|
|
581
|
+
if (name) {
|
|
582
|
+
context.fields = { ...(context.fields ?? {}), [name]: asText }
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
continue
|
|
586
|
+
}
|
|
587
|
+
|
|
541
588
|
if (step.capture != null) {
|
|
542
589
|
if (!page) throw new Error('capture step requires a Playwright page')
|
|
543
590
|
for (const [key, spec] of Object.entries(step.capture)) {
|
|
@@ -567,37 +614,109 @@ export async function runFlow (flow, options = {}) {
|
|
|
567
614
|
const to = resolveContextValue(emailStep.to ?? '$email', context)
|
|
568
615
|
const templateId = emailStep.id ?? emailStep.templateId
|
|
569
616
|
const clickLabel = emailStep.click ?? emailStep.link
|
|
617
|
+
const extract = emailStep.extract
|
|
570
618
|
if (!to) throw new Error('email step requires "to" (recipient email)')
|
|
571
619
|
if (!templateId) throw new Error('email step requires "id" (email template id)')
|
|
572
|
-
if (!clickLabel
|
|
620
|
+
if (!clickLabel && !extract) {
|
|
621
|
+
throw new Error('email step requires "click" or "extract"')
|
|
622
|
+
}
|
|
573
623
|
|
|
574
624
|
const inboxPath = router.getPathname({ id: 'dev-inbox', language: locale }) ?? '/dev/inbox'
|
|
575
625
|
const params = new URLSearchParams({ to: String(to), template: String(templateId) })
|
|
576
626
|
const inboxUrl = `${baseURL}${inboxPath}?${params}`
|
|
577
|
-
const pattern = linkNamePattern(clickLabel)
|
|
578
627
|
const deadline = Date.now() + (emailStep.timeout ?? 20000)
|
|
579
628
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
629
|
+
const openInboxBody = async (inboxPage) => {
|
|
630
|
+
await inboxPage.goto(inboxUrl)
|
|
631
|
+
const body = inboxPage.locator('[data-email-body]').first()
|
|
632
|
+
await body.waitFor({ state: 'visible', timeout: 2000 })
|
|
633
|
+
return body
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Extract-only must not navigate the flow page — SPA React state (e.g. SignIn
|
|
637
|
+
// success + code form for PWA) is lost on goto/goBack remount.
|
|
638
|
+
if (extract && !clickLabel) {
|
|
639
|
+
const inboxPage = await page.context().newPage()
|
|
640
|
+
let extracted = false
|
|
587
641
|
try {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
642
|
+
while (Date.now() < deadline && !extracted) {
|
|
643
|
+
try {
|
|
644
|
+
const body = await openInboxBody(inboxPage)
|
|
645
|
+
for (const [key, spec] of Object.entries(extract)) {
|
|
646
|
+
const selector = typeof spec === 'string' ? spec : spec.selector
|
|
647
|
+
const attr = typeof spec === 'object' ? spec.attr : undefined
|
|
648
|
+
const resolvedSelector = interpolateContextString(selector, context, {
|
|
649
|
+
escape: escapeCssAttrValue,
|
|
650
|
+
})
|
|
651
|
+
const locator = body.locator(resolvedSelector).first()
|
|
652
|
+
await locator.waitFor({ state: 'attached', timeout: 2000 })
|
|
653
|
+
const value = attr
|
|
654
|
+
? await locator.getAttribute(attr)
|
|
655
|
+
: await locator.textContent()
|
|
656
|
+
context[key] = typeof value === 'string' ? value.trim() : value
|
|
657
|
+
}
|
|
658
|
+
extracted = true
|
|
659
|
+
} catch {
|
|
660
|
+
await page.waitForTimeout(500)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} finally {
|
|
664
|
+
await inboxPage.close().catch(() => {})
|
|
597
665
|
}
|
|
666
|
+
if (!extracted) {
|
|
667
|
+
throw new Error(`Email extract failed for to=${to}, template=${templateId}`)
|
|
668
|
+
}
|
|
669
|
+
continue
|
|
598
670
|
}
|
|
599
|
-
|
|
600
|
-
|
|
671
|
+
|
|
672
|
+
if (extract && clickLabel) {
|
|
673
|
+
let extracted = false
|
|
674
|
+
while (Date.now() < deadline && !extracted) {
|
|
675
|
+
try {
|
|
676
|
+
const body = await openInboxBody(page)
|
|
677
|
+
for (const [key, spec] of Object.entries(extract)) {
|
|
678
|
+
const selector = typeof spec === 'string' ? spec : spec.selector
|
|
679
|
+
const attr = typeof spec === 'object' ? spec.attr : undefined
|
|
680
|
+
const resolvedSelector = interpolateContextString(selector, context, {
|
|
681
|
+
escape: escapeCssAttrValue,
|
|
682
|
+
})
|
|
683
|
+
const locator = body.locator(resolvedSelector).first()
|
|
684
|
+
await locator.waitFor({ state: 'attached', timeout: 2000 })
|
|
685
|
+
const value = attr
|
|
686
|
+
? await locator.getAttribute(attr)
|
|
687
|
+
: await locator.textContent()
|
|
688
|
+
context[key] = typeof value === 'string' ? value.trim() : value
|
|
689
|
+
}
|
|
690
|
+
extracted = true
|
|
691
|
+
} catch {
|
|
692
|
+
await page.waitForTimeout(500)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (!extracted) {
|
|
696
|
+
throw new Error(`Email extract failed for to=${to}, template=${templateId}`)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (clickLabel) {
|
|
701
|
+
const pattern = linkNamePattern(clickLabel)
|
|
702
|
+
let clicked = false
|
|
703
|
+
while (Date.now() < deadline && !clicked) {
|
|
704
|
+
const body = await openInboxBody(page)
|
|
705
|
+
const link = body.getByRole('link', { name: pattern }).first()
|
|
706
|
+
try {
|
|
707
|
+
await link.waitFor({ state: 'visible', timeout: 2000 })
|
|
708
|
+
await Promise.all([
|
|
709
|
+
page.waitForLoadState('domcontentloaded'),
|
|
710
|
+
link.click(),
|
|
711
|
+
])
|
|
712
|
+
clicked = true
|
|
713
|
+
} catch {
|
|
714
|
+
await page.waitForTimeout(500)
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (!clicked) {
|
|
718
|
+
throw new Error(`Email link "${clickLabel}" not found for to=${to}, template=${templateId}`)
|
|
719
|
+
}
|
|
601
720
|
}
|
|
602
721
|
continue
|
|
603
722
|
}
|
package/src/test/test.util.js
CHANGED
|
@@ -173,6 +173,43 @@ export class TestUtil {
|
|
|
173
173
|
return ev.payload.token
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Resolve a six-digit sign-in code from its stored HMAC digest (integration tests only).
|
|
178
|
+
*
|
|
179
|
+
* @param {string} subjectId
|
|
180
|
+
* @returns {Promise<string>}
|
|
181
|
+
*/
|
|
182
|
+
static async getLatestVerificationCodeForSubject(subjectId) {
|
|
183
|
+
const ev = await EventStore.Collection.findOne(
|
|
184
|
+
{
|
|
185
|
+
type: TOKEN_SCHEMA,
|
|
186
|
+
event: 'Created',
|
|
187
|
+
'payload.type': 'Verification',
|
|
188
|
+
'payload.subject': subjectId,
|
|
189
|
+
},
|
|
190
|
+
{ sort: { created: -1 } },
|
|
191
|
+
)
|
|
192
|
+
const codeHash = ev?.payload?.codeHash
|
|
193
|
+
if (!codeHash) {
|
|
194
|
+
return Promise.reject(new Error('No verification code hash found for subject'))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const secret = process.env.TOKEN_SECRET
|
|
198
|
+
if (!secret) {
|
|
199
|
+
return Promise.reject(new Error('TOKEN_SECRET is required to resolve verification codes in tests'))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const { createHmac } = await import('node:crypto')
|
|
203
|
+
const hashCode = (code) => createHmac('sha256', secret).update(code).digest('hex')
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i < 1_000_000; i += 1) {
|
|
206
|
+
const code = String(i).padStart(6, '0')
|
|
207
|
+
if (hashCode(code) === codeHash) return code
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return Promise.reject(new Error('Could not resolve verification code from hash'))
|
|
211
|
+
}
|
|
212
|
+
|
|
176
213
|
static async getLatestEmailChangeJwtForSubject(subjectId) {
|
|
177
214
|
const ev = await EventStore.Collection.findOne(
|
|
178
215
|
{
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ActionService } from './actions/action.service.js'
|
|
2
|
+
import { mergeUserAppSettingsCookie, setAuthCookie } from './user-app-settings.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Set auth (and optional single-workspace default) cookies after verify-sign-in.
|
|
6
|
+
*
|
|
7
|
+
* @param {import('express').Request} req
|
|
8
|
+
* @param {import('express').Response} res
|
|
9
|
+
* @param {{ sub: string, token: string }} session
|
|
10
|
+
*/
|
|
11
|
+
export async function applyVerifySignInSessionCookies (req, res, { sub, token }) {
|
|
12
|
+
setAuthCookie(res, token)
|
|
13
|
+
try {
|
|
14
|
+
const workspaces = await ActionService.invoke('@ossy/workspaces/actions/list', {
|
|
15
|
+
payload: { userId: sub },
|
|
16
|
+
req,
|
|
17
|
+
})
|
|
18
|
+
if (workspaces.length === 1) {
|
|
19
|
+
mergeUserAppSettingsCookie(req, res, { workspaceId: workspaces[0].id })
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Cookie for the session is enough; workspace default is best-effort.
|
|
23
|
+
}
|
|
24
|
+
}
|