Compare commits

..

1 Commits

Author SHA1 Message Date
Flea Flicker 503235df35 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>
2026-05-25 23:36:48 +00:00
5 changed files with 14 additions and 14 deletions
-1
View File
@@ -1 +0,0 @@
GRO-1757 direct push CI trigger - 2026-05-26T00:15:41Z
+4
View File
@@ -96,6 +96,7 @@ jobs:
file: Dockerfile file: Dockerfile
target: runner target: runner
push: true push: true
provenance: false
tags: | tags: |
git.farh.net/groombook/api:${{ steps.version.outputs.tag }} git.farh.net/groombook/api:${{ steps.version.outputs.tag }}
${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/api:latest' || '' }} ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/api:latest' || '' }}
@@ -110,6 +111,7 @@ jobs:
file: Dockerfile file: Dockerfile
target: migrate target: migrate
push: true push: true
provenance: false
tags: | tags: |
git.farh.net/groombook/migrate:${{ steps.version.outputs.tag }} git.farh.net/groombook/migrate:${{ steps.version.outputs.tag }}
${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/migrate:latest' || '' }} ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/migrate:latest' || '' }}
@@ -124,6 +126,7 @@ jobs:
file: Dockerfile file: Dockerfile
target: seed target: seed
push: true push: true
provenance: false
tags: | tags: |
git.farh.net/groombook/seed:${{ steps.version.outputs.tag }} git.farh.net/groombook/seed:${{ steps.version.outputs.tag }}
${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/seed:latest' || '' }} ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/seed:latest' || '' }}
@@ -138,6 +141,7 @@ jobs:
file: Dockerfile file: Dockerfile
target: reset target: reset
push: true push: true
provenance: false
tags: | tags: |
git.farh.net/groombook/reset:${{ steps.version.outputs.tag }} git.farh.net/groombook/reset:${{ steps.version.outputs.tag }}
${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/reset:latest' || '' }} ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/reset:latest' || '' }}
+1 -1
View File
@@ -46,7 +46,7 @@ const UAT_CLIENT = {
const UAT_PETS = [ const UAT_PETS = [
{ name: "Bella", species: "Dog", breed: "Poodle", coatType: "curly" as const, weightKg: "20.00" }, { name: "Bella", species: "Dog", breed: "Poodle", coatType: "curly" as const, weightKg: "20.00" },
{ name: "Max", species: "Dog", breed: "Labrador Retriever", coatType: "smooth" as const, weightKg: "30.00" }, { name: "Max", species: "Dog", breed: "Labrador Retriever", coatType: "short" as const, weightKg: "30.00" },
]; ];
const DEMO_SERVICES = [ const DEMO_SERVICES = [
+8 -11
View File
@@ -22,7 +22,7 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
c, c,
next next
) => { ) => {
// Better-Auth\'s own routes handle their own auth — skip staff resolution // Better-Auth's own routes handle their own auth — skip staff resolution
// OOBE setup routes also handle their own auth — staff record is created during setup // OOBE setup routes also handle their own auth — staff record is created during setup
if (c.req.path.startsWith("/api/auth/") || c.req.path.startsWith("/api/setup")) { if (c.req.path.startsWith("/api/auth/") || c.req.path.startsWith("/api/setup")) {
await next(); await next();
@@ -120,21 +120,22 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
.where( .where(
and( and(
eq(account.userId, jwt.sub), eq(account.userId, jwt.sub),
sql`${account.providerId} IN (\'authentik\', \'google\', \'github\')` sql`${account.providerId} IN ('authentik', 'google', 'github')`
) )
) )
.limit(1); .limit(1);
if (oidcAccount) { if (oidcAccount) {
// Derive name: prefer jwt.name, fall back to email prefix, then "Unknown" // Derive name: prefer jwt.name, fall back to email prefix, then "Unknown"
const emailPrefix = jwt.email.split("@")[0] ?? "Unknown"; const name =
const name = jwt.name?.trim() || emailPrefix; jwt.name?.trim() ||
(jwt.email ? jwt.email.split("@")[0] : "Unknown");
const [newStaff] = await db const [newStaff] = await db
.insert(staff) .insert(staff)
.values({ .values({
userId: jwt.sub, userId: jwt.sub,
email: jwt.email, email: jwt.email ?? "",
name, name,
role: "groomer", role: "groomer",
isSuperUser: false, isSuperUser: false,
@@ -142,10 +143,6 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
}) })
.returning(); .returning();
if (!newStaff) {
return c.json({ error: "Forbidden: auto-provision failed" }, 500);
}
console.log( console.log(
`[rbac] auto-provisioned staff record for OIDC user: ${jwt.sub} -> staff:${newStaff.id} (${name})` `[rbac] auto-provisioned staff record for OIDC user: ${jwt.sub} -> staff:${newStaff.id} (${name})`
); );
@@ -180,7 +177,7 @@ export function requireRole(
if (!(allowedRoles as string[]).includes(staffRow.role)) { if (!(allowedRoles as string[]).includes(staffRow.role)) {
return c.json( return c.json(
{ {
error: `Forbidden: role \'${staffRow.role}\' is not permitted to access this resource`, error: `Forbidden: role '${staffRow.role}' is not permitted to access this resource`,
}, },
403 403
); );
@@ -213,7 +210,7 @@ export function requireRoleOrSuperUser(
{ {
error: hasAllowedRole error: hasAllowedRole
? "Forbidden: super user privileges required" ? "Forbidden: super user privileges required"
: `Forbidden: role \'${staffRow.role}\' is not permitted`, : `Forbidden: role '${staffRow.role}' is not permitted`,
}, },
403 403
); );
+1 -1
View File
@@ -46,7 +46,7 @@ const UAT_CLIENT = {
const UAT_PETS = [ const UAT_PETS = [
{ name: "Bella", species: "Dog", breed: "Poodle", coatType: "curly", weightKg: "20.00" }, { name: "Bella", species: "Dog", breed: "Poodle", coatType: "curly", weightKg: "20.00" },
{ name: "Max", species: "Dog", breed: "Labrador Retriever", coatType: "smooth", weightKg: "30.00" }, { name: "Max", species: "Dog", breed: "Labrador Retriever", coatType: "short", weightKg: "30.00" },
]; ];
const DEMO_SERVICES = [ const DEMO_SERVICES = [