Compare commits

...

42 Commits

Author SHA1 Message Date
Flea Flicker 17b44e3b00 Merge origin/uat into promote/dev-to-uat-gro-1866
CI / Lint & Typecheck (pull_request) Successful in 36s
CI / Test (pull_request) Successful in 33s
CI / Build & Push Docker Images (pull_request) Successful in 3m11s
Conflicts resolved:
- src/middleware/rbac.ts: keep dev version (email null-guard, type assertion, single null-check)
- .gitea/workflows/ci.yml: keep uat version (branches: [main, dev, uat])

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-28 21:39:39 +00:00
Flea Flicker 2e0d63f7f6 fix(gro-1866): address QA review failures — portalSession null-guard,
CI / Test (push) Successful in 32s
CI / Lint & Typecheck (push) Successful in 34s
CI / Build & Push Docker Images (push) Successful in 2m34s
email null-dereference guard, externalize DEMO_STAFF_ID

1. portal.ts:138 — add null guard for portalSession before accessing .id
   (TS18048: 'portalSession' is possibly 'undefined')
2. rbac.ts:130 — guard jwt.email before split() to prevent runtime throw
3. portal.ts:39,105 — externalize DEMO_STAFF_ID as env var
   (process.env.DEMO_STAFF_ID ?? "00000000-...")

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 19:50:14 +00:00
The Dogfather 7bdb92999a Merge pull request 'fix(gro-1866): add session-from-auth portal endpoint + role scope' (#93) from fix/gro-1866-sso-bridge into dev
CI / Test (push) Successful in 34s
CI / Lint & Typecheck (push) Successful in 38s
CI / Build & Push Docker Images (push) Failing after 1m46s
fix(gro-1866): add session-from-auth portal endpoint + role scope (#93)

Bridges Better Auth SSO sessions to portal sessions for real customers.
Adds role to genericOAuth scopes for Authentik role propagation.

Closes GRO-1866
2026-05-28 18:46:38 +00:00
Flea Flicker b96b6c06fc fix: add missing getAuth import and fix db.insert() mock chain
Fixes two bugs found in QA review:
- ReferenceError: getAuth not defined in beforeEach - add import
- TypeError: wrong mock chain insert().into().values() vs insert().values()

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-28 15:59:41 +00:00
Flea Flicker fa67b75b76 docs: add UAT test cases TC-API-8.8 through TC-API-8.11 for SSO bridge
Adds manual test cases covering:
- TC-API-8.8: valid Better Auth session → portal session (201)
- TC-API-8.9: no session → 401
- TC-API-8.10: no matching client → 404
- TC-API-8.11: returned sessionId works on subsequent portal calls

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-28 15:01:24 +00:00
Flea Flicker 7e329ff72f fix(gro-1866): add session-from-auth portal endpoint and role scope
Adds POST /api/portal/session-from-auth which bridges a valid Better Auth
customer session (from SSO login) to a portal impersonation session, so
real SSO customers can access the client portal.

The endpoint is registered before the validatePortalSession catch-all so it
is not subject to that middleware. It validates the Better Auth session
from request cookies, looks up the client by email, creates an active
impersonation session, and returns { sessionId, clientId, clientName }.

Also adds "role" to the genericOAuth scopes so Authentik propagates the
role claim into Better Auth user objects (GRO-1862 root cause fix).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-28 15:00:15 +00:00
Flea Flicker b050fb9a5f Merge pull request 'feat(db): add migration 0034 for extended pet profile columns (GRO-1850)' (#92) from fix/gro-1850-pet-profile-migration into dev
CI / Lint & Typecheck (push) Successful in 14s
CI / Test (push) Successful in 13s
CI / Build & Push Docker Images (push) Successful in 1m20s
2026-05-28 11:39:51 +00:00
The Dogfather 45b3d4343d Merge pull request 'promote: dev → uat (GRO-1790 pet profile summary fixes)' (#91) from promote/dev-to-uat-gro-1790 into uat
CI / Lint & Typecheck (push) Successful in 12s
CI / Test (push) Successful in 15s
CI / Build & Push Docker Images (push) Successful in 35s
promote: dev → uat (GRO-1790 pet profile summary fixes)

Merged by CTO after QA approval.
Refs: GRO-1798, GRO-1790
2026-05-26 12:36:06 +00:00
Flea Flicker 32156e9a45 fix: restore pet profile summary endpoint from dev (GRO-1177)
CI / Lint & Typecheck (pull_request) Successful in 13s
CI / Test (pull_request) Successful in 12s
CI / Build & Push Docker Images (pull_request) Successful in 41s
2026-05-26 12:30:10 +00:00
Flea Flicker ed3d7df1c9 Merge dev into promote/dev-to-uat-gro-1790
CI / Lint & Typecheck (pull_request) Successful in 11s
CI / Test (pull_request) Successful in 11s
CI / Build & Push Docker Images (pull_request) Successful in 1m9s
Resolve .ci-trigger conflict preferring dev version.
2026-05-26 12:11:44 +00:00
Lint Roller 385ed10211 fix(rbac): guard noUncheckedIndexedAccess in name derivation and newStaff insert
CI / Test (push) Successful in 10s
CI / Lint & Typecheck (push) Successful in 10s
CI / Build & Push Docker Images (push) Successful in 43s
CI / Test (pull_request) Successful in 9s
CI / Lint & Typecheck (pull_request) Successful in 10s
CI / Build & Push Docker Images (pull_request) Failing after 10s
With noUncheckedIndexedAccess:true, split("@")[0] returns string|undefined,
making `name` typed as string|undefined and failing the notNull staff.name
insert constraint. Fix by using ?? fallback on the array access.

Also add newStaff null guard after .returning() destructure — array
destructuring yields T|undefined with noUncheckedIndexedAccess enabled.
2026-05-26 01:48:12 +00:00
Lint Roller 8e8a87767c fix(ci): remove duplicate provenance keys + add uat push trigger (GRO-1762)
CI / Lint & Typecheck (push) Successful in 12s
CI / Test (push) Successful in 13s
CI / Build & Push Docker Images (push) Failing after 41s
2026-05-26 01:34:42 +00:00
The Dogfather 2f17b1ab85 Promo/Gro 1764 Uat (#86) 2026-05-26 00:36:15 +00:00
Flea Flicker 2a0b3cf3d3 Merge remote-tracking branch 'origin/dev' into dev-to-uat 2026-05-25 23:54:49 +00:00
Lint Roller 78762b5278 Merge pull request 'promote: dev → uat (GRO-1757 SSO auto-provision fix)' (#84) from dev into uat
promote: dev → uat (GRO-1757 SSO auto-provision fix)
2026-05-25 23:48:09 +00:00
The Dogfather aa9670d4dc Merge pull request 'promote(dev→uat): add missing extended pet profile fields (GRO-1752)' (#79) from dev into uat
promote(dev→uat): add missing extended pet profile fields (GRO-1752)
2026-05-25 19:08:13 +00:00
Flea Flicker e5f16a5fe5 Merge pull request 'chore: promote dev → uat (GRO-1749 seed data sync)' (#72) from promo/gro-1749-uat into uat 2026-05-25 18:02:30 +00:00
The Dogfather baeff6c4f5 Merge pull request 'chore: promote dev → uat (GRO-1743 seed data)' (#70) from dev into uat
Merge PR #70: chore: promote dev → uat (GRO-1743 seed data)
2026-05-25 15:37:38 +00:00
The Dogfather 8d9a9d8dba Merge pull request 'chore: promote dev → uat (GRO-1678 TCP resilience + backlog fixes)' (#67) from dev into uat
chore: promote dev → uat (GRO-1678 TCP resilience + backlog fixes)
2026-05-24 23:49:11 +00:00
The Dogfather 2380698128 Merge pull request 'Promote dev → uat: provenance: false CI fix' (#65) from dev into uat
Promote dev → uat: provenance: false CI fix (#65)

Includes fix(GRO-1576): add provenance: false to all build-push-action steps.

Approved-by: The Dogfather (CTO)
2026-05-23 01:40:59 +00:00
The Dogfather 00c6a36021 Merge pull request 'Promote dev to UAT: GRO-1566 auth bypass fix' (#62) from dev into uat 2026-05-22 22:39:58 +00:00
The Dogfather f4561b539f Merge pull request 'chore: promote dev → uat (GRO-1544 health endpoint fix)' (#59) from dev into uat
chore: promote dev → uat (GRO-1544 health endpoint fix)

Merge dev → uat. CI auto-deploys to UAT environment.
2026-05-22 21:50:13 +00:00
The Dogfather d847343090 Merge pull request 'promote: dev → uat (migration 0031 fix, GRO-1533)' (#58) from dev into uat
promote: dev → uat — migration 0031 fix (GRO-1533) (#58)
2026-05-22 15:22:24 +00:00
The Dogfather 190c39f905 Merge pull request 'chore: promote dev → uat (GRO-1533 migration fix)' (#56) from dev into uat
chore: promote dev → uat (GRO-1533 migration fix)

Promotes 0032_staff_read_at.sql migration file to uat branch.
Unblocks UAT migration pipeline.
2026-05-22 14:39:42 +00:00
The Dogfather 122d32d635 Merge pull request 'chore: promote dev → uat (GRO-1533 migration fix)' (#54) from dev into uat
Promote dev → uat: GRO-1533 migration fix (PR #53)
2026-05-22 14:09:56 +00:00
The Dogfather d458f93600 Merge pull request 'promote: dev → uat (revert Dockerfile + GRO-1533 CI fix)' (#51) from dev into uat
CI / Lint & Typecheck (pull_request) Successful in 9s
CI / Test (pull_request) Successful in 10s
CI / Build & Push Docker Images (pull_request) Successful in 46s
promote: dev → uat (revert Dockerfile + GRO-1533 CI fix)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 13:32:48 +00:00
The Dogfather 634e9d03e1 Merge pull request 'promote: dev → uat (GRO-1533 Dockerfile fix)' (#49) from dev into uat
CI / Lint & Typecheck (pull_request) Successful in 9s
CI / Test (pull_request) Successful in 11s
CI / Build & Push Docker Images (pull_request) Failing after 19s
promote: dev → uat (GRO-1533 Dockerfile fix)

Promotes PR #47 fix to uat. Reverts Dockerfile to build from apps/api/src/.
Fixes HTTP 500 on authenticated admin routes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 13:24:33 +00:00
The Dogfather 974dade8f7 Merge pull request 'promote: dev → uat (pnpm-lock.yaml fix + CI/enum fixes + seed Docker fix)' (#48) from dev into uat
CI / Lint & Typecheck (pull_request) Successful in 12s
CI / Test (pull_request) Successful in 11s
CI / Build & Push Docker Images (pull_request) Successful in 53s
promote: dev → uat (pnpm-lock.yaml fix + CI/enum fixes + seed Docker fix)

Includes PR #45 seed fix, lockfile, CI, and enum alignment.
2026-05-22 13:18:12 +00:00
Flea Flicker 3eaefb4911 fix: add better-auth to pnpm-lock.yaml packages/db specifiers
CI / Lint & Typecheck (pull_request) Failing after 13s
CI / Test (pull_request) Failing after 23s
CI / Build & Push Docker Image (pull_request) Has been skipped
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 13:11:39 +00:00
The Dogfather ff6f8471d5 Merge pull request 'promote: dev → uat (GRO-1509 OIDC account_not_linked fix)' (#43) from dev into uat
CI / Lint & Typecheck (pull_request) Failing after 5s
CI / Test (pull_request) Failing after 6s
CI / Build & Push Docker Image (pull_request) Has been skipped
promote: dev → uat (GRO-1509 OIDC account_not_linked fix)

Merged-by: The Dogfather (CTO)
Gitea-approved-by: Lint Roller (GRO-1512)
2026-05-21 22:53:49 +00:00
The Dogfather 6045024150 Merge pull request 'Promote dev → uat: GRO-1178 enhanced pet profile editor' (#39) from dev into uat
Promote dev → uat: GRO-1178 enhanced pet profile editor
2026-05-21 19:19:10 +00:00
The Dogfather df5e413930 Merge pull request 'chore: promote dev → uat (GRO-1463 UAT playbook expansion)' (#38) from dev into uat
chore: promote dev → uat (GRO-1463 UAT playbook expansion)
2026-05-21 16:49:18 +00:00
The Dogfather 7cb5fda3e3 Merge pull request 'promote: dev → uat (GRO-1272 auto-provision staff on OIDC login)' (#36) from dev into uat
promote: dev → uat (GRO-1272 auto-provision staff on OIDC login) (#36)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 14:17:40 +00:00
The Dogfather 76540cea0d Merge pull request 'chore(promote): dev → uat (Buffer Rules CRUD — GRO-1171)' (#34) from dev into uat
chore(promote): dev → uat (Buffer Rules CRUD — GRO-1171)

Promote PR #12 merge to UAT for regression testing.
2026-05-21 10:18:10 +00:00
Lint Roller d83210e7e2 Merge pull request 'chore(promote): dev → uat (petsExtendedFields test fix GRO-1390)' (#33) from dev into uat 2026-05-21 07:03:24 +00:00
The Dogfather 5c9cac7a28 Merge pull request 'promote: dev → uat (GRO-1395 drizzle-orm root dep fix)' (#31) from dev into uat
promote: dev → uat (GRO-1395 drizzle-orm root dep fix) (#31)
2026-05-21 04:11:29 +00:00
The Dogfather fad99dc032 Merge pull request 'promote: dev → uat (Renovate config, GRO-1081)' (#26) from dev into uat
promote: dev → uat (Renovate config, GRO-1081) (#26)
2026-05-20 12:37:23 +00:00
The Dogfather 247570abc8 Merge pull request 'Promote dev → uat: GRO-1326 UAT email+password credentials' (#25) from dev into uat
Promote dev → uat: GRO-1326 UAT email+password credentials (#25)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-20 04:25:29 +00:00
the-dogfather-cto[bot] 4f5ec60961 chore: promote dev to uat — Dockerfile pnpm-workspace fix (GRO-1231)
chore: promote dev to uat (GRO-1231 pnpm-workspace fix)
2026-05-14 17:15:52 +00:00
the-dogfather-cto[bot] 39ffdccac7 promote: dev → uat (rate limit override) (#13)
promote: dev → uat (rate limit override)
2026-05-14 10:55:45 +00:00
the-dogfather-cto[bot] 1ff0d4230c promote: dev → uat (UAT Tester seed fix + TypeScript CI compliance)
promote: dev → uat (UAT Tester seed fix + TypeScript CI compliance)
2026-05-14 08:07:54 +00:00
the-dogfather-cto[bot] be5e9d8fc7 chore: promote dev to uat (PR #5 mock path fix)
chore: promote dev to uat (PR #5 mock path fix)
2026-05-12 21:34:03 +00:00
7 changed files with 276 additions and 8 deletions
+2 -2
View File
@@ -2,9 +2,9 @@ name: CI
on:
push:
branches: [main, dev]
branches: [main, dev, uat]
pull_request:
branches: [main, dev]
branches: [main, dev, uat]
workflow_dispatch:
inputs:
ref:
+4
View File
@@ -163,6 +163,10 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet
| TC-API-8.5 | Add waitlist entry | POST /api/portal/waitlist with pet and service | 201 Created, waitlist entry created |
| TC-API-8.6 | View portal invoices | GET /api/portal/invoices | 200 OK, list of client's invoices returned |
| TC-API-8.7 | Pay multiple invoices | POST /api/portal/invoices/pay-multiple with invoice IDs | 200 OK, payment intent created |
| TC-API-8.8 | SSO bridge — valid Better Auth session | POST /api/portal/session-from-auth with valid Better Auth session cookie (authenticated SSO user with matching client email) | 201 Created, `{sessionId, clientId, clientName}` returned |
| TC-API-8.9 | SSO bridge — no Better Auth session | POST /api/portal/session-from-auth without Better Auth session cookie | 401 Unauthorized |
| TC-API-8.10 | SSO bridge — no matching client | POST /api/portal/session-from-auth with valid Better Auth session for a user with no client record | 404 Not Found, error "No client record found for this user" |
| TC-API-8.11 | SSO bridge — returned session works on portal routes | After TC-API-8.8, use returned sessionId as `X-Impersonation-Session-Id` header on GET /api/portal/me | 200 OK, client profile returned |
### 4.9 Waitlist
+13
View File
@@ -970,66 +970,79 @@ packages:
resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.3':
resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.3':
resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.3':
resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.3':
resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.3':
resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.3':
resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.3':
resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.3':
resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.3':
resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.3':
resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.3':
resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.3':
resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.3':
resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==}
+175
View File
@@ -0,0 +1,175 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { Hono } from "hono";
import { getAuth } from "../lib/auth.js";
const CLIENT_ID = "550e8400-e29b-41d4-a716-446655440001";
const CLIENT_EMAIL = "alice@example.com";
const CLIENT_NAME = "Alice Smith";
const BETTER_AUTH_SESSION = {
user: {
id: "auth-user-001",
email: CLIENT_EMAIL,
name: CLIENT_NAME,
},
session: {
id: "ba-session-001",
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
},
};
const MOCK_CLIENT = {
id: CLIENT_ID,
email: CLIENT_EMAIL,
name: CLIENT_NAME,
};
let mockGetAuth: ReturnType<typeof vi.fn>;
let mockGetSession: ReturnType<typeof vi.fn>;
let insertedSession: Record<string, unknown> | null = null;
let mockClientRow: Record<string, unknown> | null = null;
let mockStaffRow: Record<string, unknown> | null = null;
function makeChainable(data: unknown[]): unknown {
const arr = [...data];
return new Proxy(arr, {
get(target, prop) {
if (prop === "where" || prop === "orderBy" || prop === "limit") {
return () => makeChainable(target);
}
// @ts-expect-error proxy
return target[prop];
},
});
}
vi.mock("@groombook/db", () => {
const impersonationSessions = new Proxy(
{ _name: "impersonationSessions" },
{ get: (t, p) => (p === "_name" ? "impersonationSessions" : { table: "impersonationSessions", column: p }) }
);
const clients = new Proxy(
{ _name: "clients" },
{ get: (t, p) => (p === "_name" ? "clients" : { table: "clients", column: p }) }
);
const staff = new Proxy(
{ _name: "staff" },
{ get: (t, p) => (p === "_name" ? "staff" : { table: "staff", column: p }) }
);
return {
getDb: () => ({
select: () => ({
from: (table: { _name: string }) => {
if (table._name === "clients") {
return makeChainable(mockClientRow ? [mockClientRow] : []);
}
if (table._name === "staff") {
return makeChainable(mockStaffRow ? [mockStaffRow] : []);
}
return makeChainable([]);
},
}),
insert: (table: { _name: string }) => ({
values: (vals: Record<string, unknown>) => ({
returning: () => {
if (table._name === "impersonationSessions") {
insertedSession = { id: "new-session-001", ...vals };
return [insertedSession];
}
return [];
},
}),
}),
}),
impersonationSessions,
clients,
staff,
eq: vi.fn(),
and: vi.fn(),
inArray: vi.fn(),
};
});
vi.mock("../lib/auth.js", () => ({
getAuth: vi.fn(),
}));
const { portalRouter } = await import("../routes/portal.js");
const app = new Hono();
app.route("/portal", portalRouter);
describe("POST /portal/session-from-auth", () => {
beforeEach(() => {
insertedSession = null;
mockClientRow = null;
mockStaffRow = null;
mockGetSession = vi.fn();
mockGetAuth = vi.fn(() => ({
api: {
getSession: mockGetSession,
},
}));
vi.mocked(getAuth).mockImplementation(mockGetAuth);
});
it("returns 401 when no Better Auth session", async () => {
mockGetSession.mockResolvedValue(null);
const res = await app.request("/portal/session-from-auth", {
method: "POST",
});
expect(res.status).toBe(401);
const body = await res.json();
expect(body.error).toBe("Unauthorized");
});
it("returns 404 when authenticated user has no client record", async () => {
mockGetSession.mockResolvedValue(BETTER_AUTH_SESSION);
mockClientRow = null;
const res = await app.request("/portal/session-from-auth", {
method: "POST",
});
expect(res.status).toBe(404);
const body = await res.json();
expect(body.error).toBe("No client record found for this user");
});
it("returns a portal session with sessionId, clientId, clientName when client is found", async () => {
mockGetSession.mockResolvedValue(BETTER_AUTH_SESSION);
mockClientRow = MOCK_CLIENT;
mockStaffRow = { id: "00000000-0000-0000-0000-000000000001" };
const res = await app.request("/portal/session-from-auth", {
method: "POST",
});
expect(res.status).toBe(201);
const body = await res.json();
expect(body).toHaveProperty("sessionId");
expect(body).toHaveProperty("clientId", CLIENT_ID);
expect(body).toHaveProperty("clientName", CLIENT_NAME);
});
it("creates a portal session with reason sso-bridge", async () => {
mockGetSession.mockResolvedValue(BETTER_AUTH_SESSION);
mockClientRow = MOCK_CLIENT;
mockStaffRow = { id: "00000000-0000-0000-0000-000000000001" };
const res = await app.request("/portal/session-from-auth", {
method: "POST",
});
expect(res.status).toBe(201);
expect(insertedSession).not.toBeNull();
expect((insertedSession as Record<string, unknown>).reason).toBe("sso-bridge");
});
it("returns 503 when auth is not configured", async () => {
mockGetAuth.mockImplementation(() => {
throw new Error("Auth not initialized");
});
const res = await app.request("/portal/session-from-auth", {
method: "POST",
});
expect(res.status).toBe(503);
});
});
+1 -1
View File
@@ -172,7 +172,7 @@ export async function initAuth(): Promise<void> {
clientSecret: oidcClientSecret,
issuerUrl: oidcIssuer,
internalBaseUrl: process.env.OIDC_INTERNAL_BASE,
scopes: "openid profile email",
scopes: "openid profile email role",
};
console.log("[auth] Using env var config (no DB config found)");
}
+4 -4
View File
@@ -127,20 +127,20 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
if (oidcAccount) {
// Derive name: prefer jwt.name, fall back to email prefix, then "Unknown"
const emailPrefix = jwt.email.split("@")[0] ?? "Unknown";
const emailPrefix = jwt.email ? jwt.email.split("@")[0] : "Unknown";
const name = jwt.name?.trim() || emailPrefix;
const [newStaff] = await db
.insert(staff)
.values({
userId: jwt.sub,
email: jwt.email,
email: (jwt.email ?? "") as string,
name,
role: "groomer",
isSuperUser: false,
active: true,
})
.returning();
} as Parameters<typeof db.insert>[0] extends { values: infer V } ? V : never)
.returning()!;
if (!newStaff) {
return c.json({ error: "Forbidden: auto-provision failed" }, 500);
+77 -1
View File
@@ -36,7 +36,7 @@ portalRouter.post(
return c.json({ error: "Client not found" }, 404);
}
const DEMO_STAFF_ID = "00000000-0000-0000-0000-000000000001";
const DEMO_STAFF_ID = process.env.DEMO_STAFF_ID ?? "00000000-0000-0000-0000-000000000001";
let staffId = DEMO_STAFF_ID;
const [demoStaff] = await db
@@ -71,6 +71,82 @@ portalRouter.post(
}
);
// Bridge Better Auth session → portal session for real SSO customers (GRO-1866).
// Registered BEFORE the /* middleware so it is NOT subject to validatePortalSession.
import { getAuth } from "../lib/auth.js";
portalRouter.post("/session-from-auth", async (c) => {
let auth;
try {
auth = getAuth();
} catch {
return c.json({ error: "Authentication not configured" }, 503);
}
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
return c.json({ error: "Unauthorized" }, 401);
}
const db = getDb();
const [client] = await db
.select()
.from(clients)
.where(eq(clients.email, session.user.email))
.limit(1);
if (!client) {
return c.json({ error: "No client record found for this user" }, 404);
}
const DEMO_STAFF_ID = process.env.DEMO_STAFF_ID ?? "00000000-0000-0000-0000-000000000001";
let staffId = DEMO_STAFF_ID;
const [demoStaff] = await db
.select({ id: staff.id })
.from(staff)
.where(eq(staff.id, DEMO_STAFF_ID))
.limit(1);
if (!demoStaff) {
const [firstStaff] = await db
.select({ id: staff.id })
.from(staff)
.where(eq(staff.active, true))
.limit(1);
if (!firstStaff) {
return c.json({ error: "No staff records found" }, 500);
}
staffId = firstStaff.id;
}
const [portalSession] = await db
.insert(impersonationSessions)
.values({
staffId,
clientId: client.id,
reason: "sso-bridge",
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
})
.returning();
if (!portalSession) {
return c.json({ error: "Failed to create session" }, 500);
}
return c.json(
{
sessionId: portalSession.id,
clientId: client.id,
clientName: client.name,
},
201
);
});
// Apply middleware to all portal routes
portalRouter.use("/*", validatePortalSession, portalAudit);