add features
This commit is contained in:
139
backend/src/routes/member.routes.ts
Normal file
139
backend/src/routes/member.routes.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import memberController from '../controllers/member.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Role/permission middleware
|
||||
//
|
||||
// The JWT currently carries: { userId, email, authentikSub }.
|
||||
// Roles come from Authentik group membership stored in the
|
||||
// `groups` array on the UserInfo response. The auth controller
|
||||
// already upserts the user in the DB on every login; this
|
||||
// middleware resolves the role from req.user (extended below).
|
||||
//
|
||||
// Until a full roles column exists in the users table, roles are
|
||||
// derived from a well-known Authentik group naming convention:
|
||||
//
|
||||
// "feuerwehr-admin" → AppRole 'admin'
|
||||
// "feuerwehr-kommandant" → AppRole 'kommandant'
|
||||
// (everything else) → AppRole 'mitglied'
|
||||
//
|
||||
// The groups are passed through the JWT as req.user.groups (added
|
||||
// by the extended type below). Replace this logic with a DB
|
||||
// lookup once a roles column is added to users.
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
type AppRole = 'admin' | 'kommandant' | 'mitglied';
|
||||
|
||||
// Extend the Express Request type to include role
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: {
|
||||
id: string;
|
||||
email: string;
|
||||
authentikSub: string;
|
||||
role?: AppRole;
|
||||
groups?: string[];
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the AppRole from Authentik groups attached to the JWT.
|
||||
* Mutates req.user.role so downstream controllers can read it directly.
|
||||
*/
|
||||
const resolveRole = (req: Request, _res: Response, next: NextFunction): void => {
|
||||
if (req.user) {
|
||||
const groups: string[] = (req.user as any).groups ?? [];
|
||||
if (groups.includes('feuerwehr-admin')) {
|
||||
req.user.role = 'admin';
|
||||
} else if (groups.includes('feuerwehr-kommandant')) {
|
||||
req.user.role = 'kommandant';
|
||||
} else {
|
||||
req.user.role = 'mitglied';
|
||||
}
|
||||
logger.debug('resolveRole', { userId: req.user.id, role: req.user.role });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory: creates a middleware that enforces the minimum required role.
|
||||
* Role hierarchy: admin > kommandant > mitglied
|
||||
*/
|
||||
const requirePermission = (permission: 'members:read' | 'members:write') => {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
const role = (req.user as any)?.role ?? 'mitglied';
|
||||
|
||||
const writeRoles: AppRole[] = ['admin', 'kommandant'];
|
||||
const readRoles: AppRole[] = ['admin', 'kommandant', 'mitglied'];
|
||||
|
||||
const allowed =
|
||||
permission === 'members:write'
|
||||
? writeRoles.includes(role)
|
||||
: readRoles.includes(role);
|
||||
|
||||
if (!allowed) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
message: 'Keine Berechtigung für diese Aktion.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Apply authentication + role resolution to every route in this
|
||||
// router. Note: requirePermission is applied per-route because
|
||||
// PATCH allows the owner to update their own limited fields even
|
||||
// without 'members:write'.
|
||||
// ----------------------------------------------------------------
|
||||
router.use(authenticate, resolveRole);
|
||||
|
||||
// IMPORTANT: The static /stats route must be registered BEFORE
|
||||
// the dynamic /:userId route, otherwise Express would match
|
||||
// "stats" as a userId parameter.
|
||||
router.get(
|
||||
'/stats',
|
||||
requirePermission('members:read'),
|
||||
memberController.getMemberStats.bind(memberController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
requirePermission('members:read'),
|
||||
memberController.getMembers.bind(memberController)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:userId',
|
||||
requirePermission('members:read'),
|
||||
memberController.getMemberById.bind(memberController)
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:userId/profile',
|
||||
requirePermission('members:write'),
|
||||
memberController.createMemberProfile.bind(memberController)
|
||||
);
|
||||
|
||||
/**
|
||||
* PATCH /:userId — open to both privileged users AND the profile owner.
|
||||
* The controller itself enforces the correct Zod schema (full vs. limited)
|
||||
* based on the caller's role.
|
||||
*/
|
||||
router.patch(
|
||||
'/:userId',
|
||||
// No requirePermission here — controller handles own-profile vs. write-role logic
|
||||
memberController.updateMember.bind(memberController)
|
||||
);
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user