2026-04-20 09:20:50 +00:00
|
|
|
|
import { type Request, type Response, type NextFunction } from "express";
|
|
|
|
|
|
import { db } from "@workspace/db";
|
|
|
|
|
|
import {
|
|
|
|
|
|
usersTable,
|
|
|
|
|
|
userRolesTable,
|
|
|
|
|
|
rolesTable,
|
2026-04-21 18:24:20 +00:00
|
|
|
|
rolePermissionsTable,
|
|
|
|
|
|
permissionsTable,
|
2026-04-22 08:40:38 +00:00
|
|
|
|
groupRolesTable,
|
|
|
|
|
|
userGroupsTable,
|
2026-04-20 09:20:50 +00:00
|
|
|
|
} from "@workspace/db";
|
2026-04-22 08:40:38 +00:00
|
|
|
|
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.
|
|
|
|
|
|
*/
|
2026-04-22 08:47:35 +00:00
|
|
|
|
export async function getEffectiveRoleIds(userId: number): Promise<number[]> {
|
2026-04-22 08:40:38 +00:00
|
|
|
|
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)));
|
|
|
|
|
|
}
|
2026-04-20 09:20:50 +00:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 08:40:38 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-04-20 09:20:50 +00:00
|
|
|
|
|
2026-04-22 08:40:38 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-04-20 09:20:50 +00:00
|
|
|
|
|
|
|
|
|
|
if (!isAdmin) {
|
|
|
|
|
|
res.status(403).json({ error: "Forbidden" });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
next();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 18:24:20 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 08:40:38 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-04-21 18:24:20 +00:00
|
|
|
|
const rows = await db
|
2026-04-21 18:34:14 +00:00
|
|
|
|
.select({ permName: permissionsTable.name })
|
2026-04-22 08:40:38 +00:00
|
|
|
|
.from(rolePermissionsTable)
|
2026-04-21 18:24:20 +00:00
|
|
|
|
.innerJoin(
|
|
|
|
|
|
permissionsTable,
|
|
|
|
|
|
eq(rolePermissionsTable.permissionId, permissionsTable.id),
|
|
|
|
|
|
)
|
|
|
|
|
|
.where(
|
|
|
|
|
|
and(
|
2026-04-22 08:40:38 +00:00
|
|
|
|
inArray(rolePermissionsTable.roleId, effectiveIds),
|
2026-04-21 18:24:20 +00:00
|
|
|
|
eq(permissionsTable.name, name),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-04-21 18:34:14 +00:00
|
|
|
|
if (rows.length === 0) {
|
2026-04-21 18:24:20 +00:00
|
|
|
|
res.status(403).json({ error: "Forbidden" });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
next();
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function userHasPermission(
|
|
|
|
|
|
userId: number,
|
|
|
|
|
|
name: string,
|
|
|
|
|
|
): Promise<boolean> {
|
2026-04-22 08:40:38 +00:00
|
|
|
|
const effectiveIds = await getEffectiveRoleIds(userId);
|
|
|
|
|
|
if (effectiveIds.length === 0) return false;
|
2026-04-21 18:24:20 +00:00
|
|
|
|
const rows = await db
|
2026-04-22 08:40:38 +00:00
|
|
|
|
.select({ id: rolePermissionsTable.permissionId })
|
|
|
|
|
|
.from(rolePermissionsTable)
|
2026-04-21 18:24:20 +00:00
|
|
|
|
.innerJoin(
|
|
|
|
|
|
permissionsTable,
|
|
|
|
|
|
eq(rolePermissionsTable.permissionId, permissionsTable.id),
|
|
|
|
|
|
)
|
|
|
|
|
|
.where(
|
2026-04-22 08:40:38 +00:00
|
|
|
|
and(
|
|
|
|
|
|
inArray(rolePermissionsTable.roleId, effectiveIds),
|
|
|
|
|
|
eq(permissionsTable.name, name),
|
|
|
|
|
|
),
|
2026-04-21 18:24:20 +00:00
|
|
|
|
);
|
|
|
|
|
|
return rows.length > 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 09:20:50 +00:00
|
|
|
|
export async function getUserRoles(userId: number): Promise<string[]> {
|
2026-04-22 08:40:38 +00:00
|
|
|
|
const effectiveIds = await getEffectiveRoleIds(userId);
|
|
|
|
|
|
if (effectiveIds.length === 0) return [];
|
2026-04-20 09:20:50 +00:00
|
|
|
|
const roles = await db
|
|
|
|
|
|
.select({ roleName: rolesTable.name })
|
2026-04-22 08:40:38 +00:00
|
|
|
|
.from(rolesTable)
|
|
|
|
|
|
.where(inArray(rolesTable.id, effectiveIds));
|
|
|
|
|
|
return Array.from(new Set(roles.map((r) => r.roleName)));
|
2026-04-20 09:20:50 +00:00
|
|
|
|
}
|
2026-04-22 08:40:38 +00:00
|
|
|
|
|