fix(rbac): fallback lookup for staff records predating Better-Auth userId
GRO-153: /api/staff returned 403 for all staff because resolveStaffMiddleware looked up by staff.userId (Better-Auth ID) but dev login sent staff.id (PK), and existing staff records had userId=NULL. Changes: - resolveStaffMiddleware: try userId first, fall back to staff.id (dev mode) - resolveStaffMiddleware: try userId first, fall back to oidcSub (production) - GET /api/dev/users: include userId field for DevLoginSelector - DevLoginSelector: send userId (not staff.id) as X-Dev-User-Id - Migration 0018: backfill userId for known demo staff Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -40,18 +40,29 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
|
|||||||
await next();
|
await next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Treat X-Dev-User-Id as the Better-Auth user ID
|
// Treat X-Dev-User-Id as the Better-Auth user ID first
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(staff)
|
.from(staff)
|
||||||
.where(eq(staff.userId, devUserId));
|
.where(eq(staff.userId, devUserId));
|
||||||
if (!row) {
|
if (row) {
|
||||||
|
c.set("staff", row);
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fallback: if userId is null, treat X-Dev-User-Id as staff.id (dev login
|
||||||
|
// may send the primary key for staff records that predate the userId field)
|
||||||
|
const [fallbackRow] = await db
|
||||||
|
.select()
|
||||||
|
.from(staff)
|
||||||
|
.where(eq(staff.id, devUserId));
|
||||||
|
if (!fallbackRow) {
|
||||||
return c.json(
|
return c.json(
|
||||||
{ error: "Forbidden: no staff record found for X-Dev-User-Id" },
|
{ error: "Forbidden: no staff record found for X-Dev-User-Id" },
|
||||||
403
|
403
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
c.set("staff", row);
|
c.set("staff", fallbackRow);
|
||||||
await next();
|
await next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -61,13 +72,23 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
|
|||||||
.select()
|
.select()
|
||||||
.from(staff)
|
.from(staff)
|
||||||
.where(eq(staff.userId, jwt.sub));
|
.where(eq(staff.userId, jwt.sub));
|
||||||
if (!row) {
|
if (row) {
|
||||||
|
c.set("staff", row);
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fallback: staff records that predate the userId field may still have oidcSub
|
||||||
|
const [fallbackRow] = await db
|
||||||
|
.select()
|
||||||
|
.from(staff)
|
||||||
|
.where(eq(staff.oidcSub, jwt.sub));
|
||||||
|
if (!fallbackRow) {
|
||||||
return c.json(
|
return c.json(
|
||||||
{ error: "Forbidden: no staff record found for authenticated user" },
|
{ error: "Forbidden: no staff record found for authenticated user" },
|
||||||
403
|
403
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
c.set("staff", row);
|
c.set("staff", fallbackRow);
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ devRouter.get("/users", async (c) => {
|
|||||||
const staffList = await db
|
const staffList = await db
|
||||||
.select({
|
.select({
|
||||||
id: staff.id,
|
id: staff.id,
|
||||||
|
userId: staff.userId,
|
||||||
name: staff.name,
|
name: staff.name,
|
||||||
email: staff.email,
|
email: staff.email,
|
||||||
role: staff.role,
|
role: staff.role,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
interface StaffUser {
|
interface StaffUser {
|
||||||
id: string;
|
id: string;
|
||||||
|
userId: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
@@ -66,7 +67,7 @@ export function DevLoginSelector() {
|
|||||||
{staff.map((s) => (
|
{staff.map((s) => (
|
||||||
<button
|
<button
|
||||||
key={s.id}
|
key={s.id}
|
||||||
onClick={() => selectUser("staff", s.id, s.name)}
|
onClick={() => selectUser("staff", s.userId ?? s.id, s.name)}
|
||||||
style={userButtonStyle}
|
style={userButtonStyle}
|
||||||
>
|
>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14 }}>{s.name}</div>
|
<div style={{ fontWeight: 600, fontSize: 14 }}>{s.name}</div>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- Backfill staff.user_id for staff records created before Better-Auth integration.
|
||||||
|
-- Staff records that predate this migration have user_id = NULL; the resolveStaffMiddleware
|
||||||
|
-- now falls back to staff.id (dev mode) and oidcSub (production) so these records still work.
|
||||||
|
-- This migration populates user_id for the known demo/dev staff seeded by seed.ts.
|
||||||
|
|
||||||
|
-- Create demo Better-Auth users for seeded staff (these match the ba-user-* IDs used in tests)
|
||||||
|
INSERT INTO "user" (id, name, email, email_verified, created_at, updated_at)
|
||||||
|
VALUES ('ba-user-manager', 'Demo Manager', 'demo-manager@groombook.dev', true, NOW(), NOW())
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Link the demo manager staff record to the Better-Auth user
|
||||||
|
UPDATE staff
|
||||||
|
SET user_id = 'ba-user-manager', updated_at = NOW()
|
||||||
|
WHERE oidc_sub = 'demo-manager-001' AND user_id IS NULL;
|
||||||
@@ -127,6 +127,13 @@
|
|||||||
"when": 1774512000000,
|
"when": 1774512000000,
|
||||||
"tag": "0017_better_auth_tables",
|
"tag": "0017_better_auth_tables",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1774598400000,
|
||||||
|
"tag": "0018_backfill_staff_user_id",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user