fix(GRO-1757): auto-provision staff for OIDC users + UAT playbook updates
- Add OIDC auto-provision step to resolveStaffMiddleware in rbac.ts: query account table for OAuth provider (authentik/google/github) linked to jwt.sub, if found create groomer staff record with least-privilege defaults - Guard: only auto-provision if OIDC account exists, never superuser/manager - Name derived from jwt.name > email prefix > "Unknown" - Log auto-creation for observability - Add SSO Login Journey (TC-API-1.17 to 1.21) and OOBE Flow (TC-API-1.22 to 1.26) test cases to groombook-api UAT_PLAYBOOK.md §4.1 Updated UAT_PLAYBOOK.md §5.4.1 (SSO Login Journey) and §5.4.2 (OOBE Flow Post-Login) in groombook-web. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -48,6 +48,26 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet
|
||||
| TC-API-1.15 | Name fallback — no name, no email | Auto-provision where Better-Auth user has name = null, email = null | Staff name = "Unknown" |
|
||||
| TC-API-1.16 | OIDC login — Terraform-provisioned user | Initiate OIDC login as any UAT persona (uat-super, uat-groomer, uat-customer, uat-tester), complete authentik callback | 200 OK, session created — no account_not_linked error |
|
||||
|
||||
#### SSO Login Journey (Authentik OIDC end-to-end)
|
||||
|
||||
| # | Scenario | Steps | Pass Criteria | Fail Criteria |
|
||||
|---|----------|-------|---------------|---------------|
|
||||
| TC-API-1.17 | SSO redirect to Authentik | Navigate to app → sign-in page shown → click "Sign in with SSO" | Redirected to Authentik at auth.farh.net | 403 error, redirect loop, no SSO button |
|
||||
| TC-API-1.18 | Authenticate with valid OIDC credentials | At Authentik login page, enter valid credentials and authenticate | Redirected back to app with valid session | Redirect loop, 403, missing session cookie |
|
||||
| TC-API-1.19 | SSO user auto-provisioned as groomer | Complete SSO login as a user with no pre-existing staff record | 200 response; groomer staff record auto-created; session active | 403 Forbidden, staff record not created |
|
||||
| TC-API-1.20 | Existing staff record resolves correctly | Complete SSO login as uat-groomer (pre-existing staff) | 200 OK, correct staff identity resolved, no duplicate record created | 403, duplicate record, wrong staff data |
|
||||
| TC-API-1.21 | SSO session grants dashboard access | After TC-API-1.18 SSO login, GET /api/staff/me | 200 OK, valid staff record returned, correct role displayed | 401/403, missing session, wrong identity |
|
||||
|
||||
#### OOBE Flow Post-Login
|
||||
|
||||
| # | Scenario | Steps | Pass Criteria | Fail Criteria |
|
||||
|---|----------|-------|---------------|---------------|
|
||||
| TC-API-1.22 | Fresh DB reports needsSetup | On a fresh DB (no super user), GET /api/setup/status | needsSetup: true returned | needsSetup: false when it should be true |
|
||||
| TC-API-1.23 | Configure OIDC via auth-provider endpoint | POST /api/setup/auth-provider with valid OIDC config | 200 OK, auth provider configured, no 403 | 403, setup blocked, invalid config rejected |
|
||||
| TC-API-1.24 | Complete setup creates super user | POST /api/setup with business name (after TC-API-1.23) | First user becomes super user, setup completes | Setup errors, 403 on admin endpoints |
|
||||
| TC-API-1.25 | Super user accesses admin features | After TC-API-1.24, GET /api/staff/me and verify isSuperUser: true | isSuperUser: true, admin endpoints accessible | 403 on admin, isSuperUser: false |
|
||||
| TC-API-1.26 | Auto-provision skipped during OOBE | During fresh setup (needsSetup: true), complete OIDC login — verify no duplicate staff record created before setup completes | No duplicate staff, OOBE completes successfully | Duplicate staff record, 403 before setup, auto-provision interferes with OOBE |
|
||||
|
||||
### 4.2 Client Management
|
||||
|
||||
| # | Scenario | Steps | Expected |
|
||||
|
||||
+43
-1
@@ -1,5 +1,5 @@
|
||||
import type { MiddlewareHandler } from "hono";
|
||||
import { and, eq, getDb, sql, staff } from "@groombook/db";
|
||||
import { and, eq, getDb, sql, staff, account } from "@groombook/db";
|
||||
|
||||
export type StaffRole = "groomer" | "receptionist" | "manager";
|
||||
export type StaffRow = typeof staff.$inferSelect;
|
||||
@@ -110,6 +110,48 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-provision for OIDC users: check if jwt.sub has an OAuth/OIDC account
|
||||
// (e.g. authentik). If so, create a groomer staff record on the fly.
|
||||
if (jwt.email) {
|
||||
const [oidcAccount] = await db
|
||||
.select({ id: account.id })
|
||||
.from(account)
|
||||
.where(
|
||||
and(
|
||||
eq(account.userId, jwt.sub),
|
||||
sql`${account.providerId} IN ('authentik', 'google', 'github')`
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (oidcAccount) {
|
||||
// Derive name: prefer jwt.name, fall back to email prefix, then "Unknown"
|
||||
const name =
|
||||
jwt.name?.trim() ||
|
||||
(jwt.email ? jwt.email.split("@")[0] : "Unknown");
|
||||
|
||||
const [newStaff] = await db
|
||||
.insert(staff)
|
||||
.values({
|
||||
userId: jwt.sub,
|
||||
email: jwt.email ?? "",
|
||||
name,
|
||||
role: "groomer",
|
||||
isSuperUser: false,
|
||||
active: true,
|
||||
})
|
||||
.returning();
|
||||
|
||||
console.log(
|
||||
`[rbac] auto-provisioned staff record for OIDC user: ${jwt.sub} -> staff:${newStaff.id} (${name})`
|
||||
);
|
||||
c.set("staff", newStaff);
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return c.json(
|
||||
{ error: "Forbidden: no staff record found for authenticated user" },
|
||||
403
|
||||
|
||||
Reference in New Issue
Block a user