Files
TX/artifacts/api-server/src/middlewares/auth.ts
T

242 lines
6.2 KiB
TypeScript
Raw Normal View History

import { type Request, type Response, type NextFunction } from "express";
import { db } from "@workspace/db";
import {
usersTable,
userRolesTable,
rolesTable,
rolePermissionsTable,
permissionsTable,
groupRolesTable,
userGroupsTable,
} from "@workspace/db";
import { eq, and, inArray, or } from "drizzle-orm";
/**
* Effective roles for a user = direct user_roles roles attached to any of
* the user's groups via group_roles. Returns distinct role names.
*/
export async function getEffectiveRoleIds(userId: number): Promise<number[]> {
const rows = await db
.select({ roleId: rolesTable.id })
.from(rolesTable)
.where(
or(
inArray(
rolesTable.id,
db
.select({ rid: userRolesTable.roleId })
.from(userRolesTable)
.where(eq(userRolesTable.userId, userId)),
),
inArray(
rolesTable.id,
db
.select({ rid: groupRolesTable.roleId })
.from(groupRolesTable)
.innerJoin(
userGroupsTable,
eq(userGroupsTable.groupId, groupRolesTable.groupId),
)
.where(eq(userGroupsTable.userId, userId)),
),
),
);
return Array.from(new Set(rows.map((r) => r.roleId)));
}
declare module "express-session" {
interface SessionData {
userId: number;
}
}
export async function requireAuth(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
if (!req.session.userId) {
res.status(401).json({ error: "Unauthorized" });
return;
}
const [user] = await db
.select()
.from(usersTable)
.where(eq(usersTable.id, req.session.userId));
if (!user || !user.isActive) {
res.status(401).json({ error: "Unauthorized" });
return;
}
next();
}
export async function requireAdmin(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
if (!req.session.userId) {
res.status(401).json({ error: "Unauthorized" });
return;
}
const [user] = await db
.select({ isActive: usersTable.isActive })
.from(usersTable)
.where(eq(usersTable.id, req.session.userId));
if (!user || !user.isActive) {
res.status(401).json({ error: "Unauthorized" });
return;
}
const effectiveIds = await getEffectiveRoleIds(req.session.userId);
let isAdmin = false;
if (effectiveIds.length > 0) {
const adminRows = await db
.select({ id: rolesTable.id })
.from(rolesTable)
.where(and(inArray(rolesTable.id, effectiveIds), eq(rolesTable.name, "admin")));
isAdmin = adminRows.length > 0;
}
if (!isAdmin) {
res.status(403).json({ error: "Forbidden" });
return;
}
next();
}
export function requirePermission(name: string) {
return async function (
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
if (!req.session.userId) {
res.status(401).json({ error: "Unauthorized" });
return;
}
const [user] = await db
.select({ isActive: usersTable.isActive })
.from(usersTable)
.where(eq(usersTable.id, req.session.userId));
if (!user || !user.isActive) {
res.status(401).json({ error: "Unauthorized" });
return;
}
const effectiveIds = await getEffectiveRoleIds(req.session.userId);
if (effectiveIds.length === 0) {
res.status(403).json({ error: "Forbidden" });
return;
}
const rows = await db
.select({ permName: permissionsTable.name })
.from(rolePermissionsTable)
.innerJoin(
permissionsTable,
eq(rolePermissionsTable.permissionId, permissionsTable.id),
)
.where(
and(
inArray(rolePermissionsTable.roleId, effectiveIds),
eq(permissionsTable.name, name),
),
);
if (rows.length === 0) {
res.status(403).json({ error: "Forbidden" });
return;
}
next();
};
}
export async function userHasPermission(
userId: number,
name: string,
): Promise<boolean> {
const effectiveIds = await getEffectiveRoleIds(userId);
if (effectiveIds.length === 0) return false;
const rows = await db
.select({ id: rolePermissionsTable.permissionId })
.from(rolePermissionsTable)
.innerJoin(
permissionsTable,
eq(rolePermissionsTable.permissionId, permissionsTable.id),
)
.where(
and(
inArray(rolePermissionsTable.roleId, effectiveIds),
eq(permissionsTable.name, name),
),
);
return rows.length > 0;
}
export async function getUserRoles(userId: number): Promise<string[]> {
const effectiveIds = await getEffectiveRoleIds(userId);
if (effectiveIds.length === 0) return [];
const roles = await db
.select({ roleName: rolesTable.name })
.from(rolesTable)
.where(inArray(rolesTable.id, effectiveIds));
return Array.from(new Set(roles.map((r) => r.roleName)));
}
const EXECUTIVE_READ_ROLES: ReadonlyArray<string> = [
"admin",
"executive_ceo",
"executive_office_manager",
"executive_coord_lead",
"executive_coordinator",
"executive_viewer",
];
/**
* Gate any read access to the Executive Meetings module. Allows admin and
* any of the executive_* roles. Mutate / approve / audit-view sub-roles
* are layered on top via local helpers in the routes file.
*/
export async function requireExecutiveAccess(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
if (!req.session.userId) {
res.status(401).json({ error: "Unauthorized", code: "unauthorized" });
return;
}
const [user] = await db
.select({ isActive: usersTable.isActive })
.from(usersTable)
.where(eq(usersTable.id, req.session.userId));
if (!user || !user.isActive) {
res.status(401).json({ error: "Unauthorized", code: "unauthorized" });
return;
}
const ids = await getEffectiveRoleIds(req.session.userId);
if (ids.length === 0) {
res.status(403).json({ error: "Forbidden", code: "forbidden" });
return;
}
const rows = await db
.select({ name: rolesTable.name })
.from(rolesTable)
.where(inArray(rolesTable.id, ids));
const names = new Set(rows.map((r) => r.name));
const ok = EXECUTIVE_READ_ROLES.some((r) => names.has(r));
if (!ok) {
res.status(403).json({ error: "Forbidden", code: "forbidden" });
return;
}
next();
}