add features
This commit is contained in:
234
backend/src/controllers/member.controller.ts
Normal file
234
backend/src/controllers/member.controller.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { Request, Response } from 'express';
|
||||
import memberService from '../services/member.service';
|
||||
import logger from '../utils/logger';
|
||||
import { AppError } from '../middleware/error.middleware';
|
||||
import {
|
||||
CreateMemberProfileSchema,
|
||||
UpdateMemberProfileSchema,
|
||||
SelfUpdateMemberProfileSchema,
|
||||
} from '../models/member.model';
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Role helpers
|
||||
// These helpers inspect req.user.role which is populated by the
|
||||
// requireRole / requirePermission middleware (see member.routes.ts).
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
type AppRole = 'admin' | 'kommandant' | 'mitglied';
|
||||
|
||||
function getRole(req: Request): AppRole {
|
||||
return (req.user as any)?.role ?? 'mitglied';
|
||||
}
|
||||
|
||||
function canWrite(req: Request): boolean {
|
||||
const role = getRole(req);
|
||||
return role === 'admin' || role === 'kommandant';
|
||||
}
|
||||
|
||||
function isOwnProfile(req: Request, userId: string): boolean {
|
||||
return req.user?.id === userId;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Controller
|
||||
// ----------------------------------------------------------------
|
||||
class MemberController {
|
||||
/**
|
||||
* GET /api/members
|
||||
* Returns a paginated list of all active members.
|
||||
* Supports ?search=, ?status[]=, ?dienstgrad[]=, ?page=, ?pageSize=
|
||||
*/
|
||||
async getMembers(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const {
|
||||
search,
|
||||
page,
|
||||
pageSize,
|
||||
} = req.query as Record<string, string | undefined>;
|
||||
|
||||
// Arrays can be sent as ?status[]=aktiv&status[]=passiv or CSV
|
||||
const statusParam = req.query['status'] as string | string[] | undefined;
|
||||
const dienstgradParam = req.query['dienstgrad'] as string | string[] | undefined;
|
||||
|
||||
const normalizeArray = (v?: string | string[]): string[] | undefined => {
|
||||
if (!v) return undefined;
|
||||
return Array.isArray(v) ? v : v.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
};
|
||||
|
||||
const { items, total } = await memberService.getAllMembers({
|
||||
search,
|
||||
status: normalizeArray(statusParam) as any,
|
||||
dienstgrad: normalizeArray(dienstgradParam) as any,
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? Math.min(parseInt(pageSize, 10), 100) : 25,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: items,
|
||||
meta: { total, page: page ? parseInt(page, 10) : 1 },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('getMembers error', { error });
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Mitglieder.' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/members/stats
|
||||
* Returns aggregate member counts for each status.
|
||||
* Must be registered BEFORE /:userId to avoid route collision.
|
||||
*/
|
||||
async getMemberStats(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const stats = await memberService.getMemberStats();
|
||||
res.status(200).json({ success: true, data: stats });
|
||||
} catch (error) {
|
||||
logger.error('getMemberStats error', { error });
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden der Statistiken.' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/members/:userId
|
||||
* Returns full member detail including profile and rank history.
|
||||
*
|
||||
* Role rules:
|
||||
* - Kommandant/Admin: all fields
|
||||
* - Mitglied reading own profile: all fields
|
||||
* - Mitglied reading another member: geburtsdatum and emergency contact redacted
|
||||
*/
|
||||
async getMemberById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const requestorId = req.user!.id;
|
||||
const requestorRole = getRole(req);
|
||||
const ownProfile = isOwnProfile(req, userId);
|
||||
|
||||
const member = await memberService.getMemberById(userId);
|
||||
|
||||
if (!member) {
|
||||
res.status(404).json({ success: false, message: 'Mitglied nicht gefunden.' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Sensitive field masking for non-privileged reads of other members
|
||||
const canReadSensitive = canWrite(req) || ownProfile;
|
||||
|
||||
if (!canReadSensitive && member.profile) {
|
||||
// Replace geburtsdatum with only the age (year of birth omitted for DSGVO)
|
||||
const ageMasked: any = { ...member.profile };
|
||||
|
||||
if (ageMasked.geburtsdatum) {
|
||||
const birthYear = new Date(ageMasked.geburtsdatum).getFullYear();
|
||||
const age = new Date().getFullYear() - birthYear;
|
||||
ageMasked.geburtsdatum = null;
|
||||
ageMasked._age = age; // synthesised non-DB field
|
||||
}
|
||||
|
||||
// Redact emergency contact entirely
|
||||
ageMasked.notfallkontakt_name = null;
|
||||
ageMasked.notfallkontakt_telefon = null;
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: { ...member, profile: ageMasked },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true, data: member });
|
||||
} catch (error) {
|
||||
logger.error('getMemberById error', { error, userId: req.params.userId });
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Laden des Mitglieds.' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/members/:userId/profile
|
||||
* Creates the mitglieder_profile row for an existing auth user.
|
||||
* Restricted to Kommandant/Admin.
|
||||
*/
|
||||
async createMemberProfile(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
|
||||
const parseResult = CreateMemberProfileSchema.safeParse(req.body);
|
||||
if (!parseResult.success) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Ungültige Eingabedaten.',
|
||||
errors: parseResult.error.flatten().fieldErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await memberService.createMemberProfile(userId, parseResult.data);
|
||||
logger.info('createMemberProfile', { userId, createdBy: req.user!.id });
|
||||
|
||||
res.status(201).json({ success: true, data: profile });
|
||||
} catch (error: any) {
|
||||
if (error?.message?.includes('existiert bereits')) {
|
||||
res.status(409).json({ success: false, message: error.message });
|
||||
return;
|
||||
}
|
||||
logger.error('createMemberProfile error', { error, userId: req.params.userId });
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Erstellen des Profils.' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/members/:userId
|
||||
* Updates the mitglieder_profile row.
|
||||
*
|
||||
* Role rules:
|
||||
* - Kommandant/Admin: full update (all fields)
|
||||
* - Mitglied (own profile only): restricted to SelfUpdateMemberProfileSchema fields
|
||||
*/
|
||||
async updateMember(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const updaterId = req.user!.id;
|
||||
const ownProfile = isOwnProfile(req, userId);
|
||||
|
||||
if (!canWrite(req) && !ownProfile) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
message: 'Keine Berechtigung, dieses Profil zu bearbeiten.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Choose validation schema based on role
|
||||
const schema = canWrite(req) ? UpdateMemberProfileSchema : SelfUpdateMemberProfileSchema;
|
||||
const parseResult = schema.safeParse(req.body);
|
||||
|
||||
if (!parseResult.success) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Ungültige Eingabedaten.',
|
||||
errors: parseResult.error.flatten().fieldErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await memberService.updateMemberProfile(
|
||||
userId,
|
||||
parseResult.data as any,
|
||||
updaterId
|
||||
);
|
||||
|
||||
logger.info('updateMember', { userId, updatedBy: updaterId });
|
||||
res.status(200).json({ success: true, data: profile });
|
||||
} catch (error: any) {
|
||||
if (error?.message === 'Mitgliedsprofil nicht gefunden.') {
|
||||
res.status(404).json({ success: false, message: error.message });
|
||||
return;
|
||||
}
|
||||
logger.error('updateMember error', { error, userId: req.params.userId });
|
||||
res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren des Profils.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MemberController();
|
||||
Reference in New Issue
Block a user