first commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user