/**
* @module middleware/workspaceScope
* @description Middleware that resolves the authenticated user's workspace
* and role, injecting `req.workspaceId` and `req.userRole` on every request.
*
* Must run AFTER `requireAuth` (which sets `req.authUser`).
*
* ### Resolution strategy
* The JWT contains `workspaceId` as a hint for which workspace the user last
* used. The **role is always resolved from the database** so that permission
* changes (promote / demote / remove) take effect immediately — not after the
* JWT expires. This follows the Slack / GitHub model: identity in the token,
* authorization from the DB.
*
* If the user has no workspace membership at all, returns 403.
*
* @example
* import { workspaceScope } from "../middleware/workspaceScope.js";
* app.use("/api/projects", requireAuth, workspaceScope, projectsRouter);
*/
import * as workspaceRepo from "../database/repositories/workspaceRepo.js";
/**
* Express middleware that injects workspace context onto the request.
*
* Sets:
* - `req.workspaceId` — The active workspace ID.
* - `req.userRole` — The user's role in that workspace ('admin' | 'qa_lead' | 'viewer').
*/
export function workspaceScope(req, res, next) {
// Skip for non-user auth strategies (e.g. trigger tokens)
if (!req.authUser) return next();
const { sub: userId, workspaceId: jwtWorkspaceId } = req.authUser;
// If the JWT contains a workspaceId hint, verify membership and resolve
// the current role from the DB (never trust the JWT for authorization).
if (jwtWorkspaceId) {
const membership = workspaceRepo.getMembership(jwtWorkspaceId, userId);
if (membership) {
req.workspaceId = jwtWorkspaceId;
req.userRole = membership.role;
return next();
}
// Membership was revoked since the JWT was issued — fall through to
// check if the user has any other workspace.
}
// Resolve from all memberships (no JWT hint, or hint was stale).
const workspaces = workspaceRepo.getByUserId(userId);
if (!workspaces || workspaces.length === 0) {
return res.status(403).json({
error: "You are not a member of any workspace. Please contact your administrator.",
});
}
// Use the first workspace. Future: allow workspace switching via header.
const ws = workspaces[0];
req.workspaceId = ws.id;
req.userRole = ws.role;
next();
}