first commit

This commit is contained in:
2026-07-16 09:35:32 +02:00
commit dd0746e2e6
179 changed files with 25679 additions and 0 deletions
@@ -0,0 +1,51 @@
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { updateRankSchema, updateUserRolesSchema, DEPARTMENTS } from '@d4rk-tablet/shared';
import { getRankMatrix, updateRank, listUsers, updateUserRoles } from './admin.service';
const deptParam = z.object({ department: z.enum(DEPARTMENTS) });
export async function adminRoutes(app: FastifyInstance): Promise<void> {
// Rechte-Matrix einer Behörde
app.get<{ Params: { department: string } }>(
'/admin/ranks/:department',
{ preHandler: app.requirePermission('admin.ranks.manage') },
async (req, reply) => {
const parsed = deptParam.safeParse(req.params);
if (!parsed.success) return reply.status(400).send({ message: 'Unbekannte Behörde' });
return getRankMatrix(parsed.data.department);
},
);
// Einen Rang aktualisieren
app.put<{ Params: { department: string; grade: string } }>(
'/admin/ranks/:department/:grade',
{ preHandler: app.requirePermission('admin.ranks.manage') },
async (req, reply) => {
const params = deptParam.safeParse({ department: req.params.department });
const body = updateRankSchema.safeParse(req.body);
if (!params.success || !body.success) {
return reply.status(400).send({ message: 'Ungültige Anfrage' });
}
return updateRank(params.data.department, Number(req.params.grade), body.data);
},
);
// Mitarbeiterliste
app.get('/admin/users', { preHandler: app.requirePermission('admin.users.manage') }, async () => {
return listUsers();
});
// Override-Rollen eines Mitarbeiters setzen
app.patch<{ Params: { id: string } }>(
'/admin/users/:id/roles',
{ preHandler: app.requirePermission('admin.users.manage') },
async (req, reply) => {
const parsed = updateUserRolesSchema.safeParse(req.body);
if (!parsed.success) return reply.status(400).send({ message: 'Ungültige Rollen' });
const user = await updateUserRoles(Number(req.params.id), parsed.data.roles);
if (!user) return reply.status(404).send({ message: 'Mitarbeiter nicht gefunden' });
return user;
},
);
}
@@ -0,0 +1,120 @@
import { and, eq } from 'drizzle-orm';
import {
PERMISSIONS,
ROLE_PERMISSIONS,
type Department,
type Permission,
type Role,
type Rank,
type MdtUser,
} from '@d4rk-tablet/shared';
import { db } from '../../db/client';
import { mdtRanks, mdtUsers } from '../../db/schema';
import { DEFAULT_RANKS, defaultPermissionsFor } from '../../config/rankDefaults';
const OVERRIDE_ROLES: Role[] = ['admin', 'dispatch'];
function asRoles(v: unknown): Role[] {
if (!Array.isArray(v)) return [];
const set = new Set<string>(['admin', 'dispatch', 'police', 'ems', 'fire']);
return v.filter((r): r is Role => typeof r === 'string' && set.has(r));
}
/**
* Effektive Rechte eines Users: Rang-Matrix(Department, Grade) Override-Rollen.
* admin → alle Rechte. DB-Rang überschreibt Code-Default.
*/
export async function computePermissions(
department: Department | null,
grade: number | null,
overrideRoles: Role[],
): Promise<Permission[]> {
if (overrideRoles.includes('admin')) return [...PERMISSIONS];
const perms = new Set<Permission>();
if (department && grade != null) {
const [row] = await db
.select()
.from(mdtRanks)
.where(and(eq(mdtRanks.department, department), eq(mdtRanks.grade, grade)))
.limit(1);
const rankPerms = row ? (row.permissions as Permission[]) : defaultPermissionsFor(department, grade);
for (const p of rankPerms) perms.add(p);
}
for (const role of overrideRoles) {
if (role === 'dispatch') for (const p of ROLE_PERMISSIONS.dispatch) perms.add(p);
}
return [...perms];
}
/** Rechte-Matrix einer Behörde: DB-Ränge über Defaults gemerged. */
export async function getRankMatrix(department: Department): Promise<Rank[]> {
const rows = await db.select().from(mdtRanks).where(eq(mdtRanks.department, department));
const byGrade = new Map(rows.map((r) => [r.grade, r]));
const grades = new Set<number>([
...DEFAULT_RANKS[department].map((r) => r.grade),
...rows.map((r) => r.grade),
]);
return [...grades]
.sort((a, b) => a - b)
.map((grade) => {
const dbRow = byGrade.get(grade);
if (dbRow) {
return {
department,
grade,
label: dbRow.label,
permissions: dbRow.permissions as Permission[],
};
}
const def = DEFAULT_RANKS[department].find((r) => r.grade === grade)!;
return { department, grade, label: def.label, permissions: def.permissions };
});
}
export async function updateRank(
department: Department,
grade: number,
input: { label?: string; permissions: Permission[] },
): Promise<Rank> {
const label =
input.label ?? DEFAULT_RANKS[department].find((r) => r.grade === grade)?.label ?? `Grade ${grade}`;
await db
.insert(mdtRanks)
.values({ department, grade, label, permissions: input.permissions })
.onDuplicateKeyUpdate({ set: { label, permissions: input.permissions } });
return { department, grade, label, permissions: input.permissions };
}
// ── Mitarbeiter ──
function mapUser(row: typeof mdtUsers.$inferSelect): MdtUser {
return {
id: row.id,
citizenid: row.citizenid,
discordId: row.discordId,
name: row.name,
callsign: row.callsign,
department: row.department,
grade: row.grade,
roles: asRoles(row.roles),
};
}
export async function listUsers(): Promise<MdtUser[]> {
const rows = await db.select().from(mdtUsers);
return rows.map(mapUser);
}
export async function updateUserRoles(id: number, roles: string[]): Promise<MdtUser | null> {
const filtered = asRoles(roles).filter((r) => OVERRIDE_ROLES.includes(r));
await db.update(mdtUsers).set({ roles: filtered }).where(eq(mdtUsers.id, id));
const [row] = await db.select().from(mdtUsers).where(eq(mdtUsers.id, id)).limit(1);
return row ? mapUser(row) : null;
}