From dc3b3ddcb701192c7ccc8af28b214893a133f922 Mon Sep 17 00:00:00 2001 From: "groombook-cto[bot]" <269737991+groombook-cto[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 05:50:59 +0000 Subject: [PATCH] fix(auth): add email-based staff auto-linking in resolveStaffMiddleware Auto-link staff records by email when userId is NULL on first authenticated request. Resolves GRO-667 UAT 403 blocker. Co-Authored-By: Flea Flicker --- apps/api/src/middleware/rbac.ts | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/apps/api/src/middleware/rbac.ts b/apps/api/src/middleware/rbac.ts index b8473e8..b253eed 100644 --- a/apps/api/src/middleware/rbac.ts +++ b/apps/api/src/middleware/rbac.ts @@ -1,5 +1,5 @@ import type { MiddlewareHandler } from "hono"; -import { eq, getDb, staff } from "@groombook/db"; +import { and, eq, getDb, sql, staff } from "@groombook/db"; export type StaffRole = "groomer" | "receptionist" | "manager"; export type StaffRow = typeof staff.$inferSelect; @@ -89,14 +89,31 @@ export const resolveStaffMiddleware: MiddlewareHandler = async ( .select() .from(staff) .where(eq(staff.oidcSub, jwt.sub)); - if (!fallbackRow) { - return c.json( - { error: "Forbidden: no staff record found for authenticated user" }, - 403 - ); + if (fallbackRow) { + c.set("staff", fallbackRow); + await next(); + return; } - c.set("staff", fallbackRow); - await next(); + // Auto-link by email: staff record exists with matching email but no userId + if (jwt.email) { + const [byEmail] = await db + .select() + .from(staff) + .where(and(eq(staff.email, jwt.email), sql`${staff.userId} IS NULL`)); + if (byEmail) { + await db + .update(staff) + .set({ userId: jwt.sub, updatedAt: new Date() }) + .where(eq(staff.id, byEmail.id)); + c.set("staff", { ...byEmail, userId: jwt.sub }); + await next(); + return; + } + } + return c.json( + { error: "Forbidden: no staff record found for authenticated user" }, + 403 + ); }; /**