fix(api): auto-link staff to Better-Auth user via email on first SSO login (GRO-480)

When a staff record exists with a matching email but no userId (e.g. seed data
or admin UI-created records), resolveStaffMiddleware now auto-links it to the
Better-Auth user record on first SSO login instead of returning 403.

Safety: only links when userId IS NULL, never overwrites an existing link.
Email matching is safe since it comes from the trusted SSO provider (Authentik).
Staff emails are unique by schema.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Flea Flicker
2026-04-05 14:30:25 +00:00
parent 90ad46f0d5
commit 711981e6f3
+21 -1
View File
@@ -1,5 +1,6 @@
import type { MiddlewareHandler } from "hono";
import { eq, getDb, staff } from "@groombook/db";
import { isNull } from "drizzle-orm";
import { and, eq, getDb, staff } from "@groombook/db";
export type StaffRole = "groomer" | "receptionist" | "manager";
export type StaffRow = typeof staff.$inferSelect;
@@ -89,6 +90,25 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
.from(staff)
.where(eq(staff.oidcSub, jwt.sub));
if (!fallbackRow) {
// Auto-link: staff record exists with matching email but no userId — link it now
if (jwt.email) {
const [linkedStaff] = await db
.select()
.from(staff)
.where(and(eq(staff.email, jwt.email), isNull(staff.userId)));
if (linkedStaff) {
await db
.update(staff)
.set({ userId: jwt.sub })
.where(eq(staff.id, linkedStaff.id));
console.log(
`[rbac] Auto-linked staff ${linkedStaff.id} to Better-Auth user ${jwt.sub} via email ${jwt.email}`
);
c.set("staff", linkedStaff);
await next();
return;
}
}
return c.json(
{ error: "Forbidden: no staff record found for authenticated user" },
403