Add moderation permissions and durable player records

This commit is contained in:
2026-10-15 21:06:08 -04:00
parent c1758cce25
commit c91d49c298
17 changed files with 699 additions and 21 deletions
+47 -2
View File
@@ -1,18 +1,61 @@
import { describe, expect, it } from 'vitest';
import { readdirSync } from 'node:fs';
import { relative, resolve } from 'node:path';
import type { UserRole } from '../../src/lib/auth.js';
import {
canModerate,
canReadModeration,
moderatorMaximumRestrictionMs,
type UserRole
} from '../../src/lib/auth.js';
import {
roleAllows,
routePermission,
type RoutePermission
} from '../../src/lib/server/auth/access.js';
const roles: UserRole[] = ['viewer', 'super_viewer', 'operator', 'admin'];
const roles: UserRole[] = ['viewer', 'super_viewer', 'operator', 'moderator', 'admin'];
const allows = (role: UserRole, route: string | null, method = 'GET') =>
roleAllows(role, routePermission(route, method));
describe('management route permissions', () => {
it.each([
'/moderation',
'/moderation/players/[guid]',
'/api/moderation',
'/api/moderation/players/[guid]',
'/api/moderation/sanctions/[id]/revoke'
])('limits private moderation access at %s without changing fleet privileges', (route) => {
for (const method of ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) {
const reading = ['GET', 'HEAD'].includes(method);
expect(routePermission(route, method)).toBe(reading ? 'moderation-read' : 'moderation');
for (const role of roles) {
expect(allows(role, route, method)).toBe(
role === 'admin' || role === 'moderator' || (reading && role === 'super_viewer')
);
}
}
});
it('keeps moderators within safe summary reads and explicit moderation operations', () => {
expect(allows('moderator', '/api/fleet')).toBe(true);
expect(allows('moderator', '/api/servers/[name]/chat')).toBe(true);
expect(allows('moderator', '/api/servers', 'POST')).toBe(false);
expect(allows('moderator', '/api/users')).toBe(false);
expect(allows('moderator', '/api/configs/[id]')).toBe(false);
expect(allows('moderator', '/api/drift')).toBe(false);
expect(allows('moderator', '/api/moderation-extra', 'POST')).toBe(false);
expect(allows('moderator', '/api/auth/password', 'POST')).toBe(true);
for (const role of roles) {
expect(canModerate(role)).toBe(role === 'moderator' || role === 'admin');
expect(canReadModeration(role)).toBe(
role === 'moderator' || role === 'admin' || role === 'super_viewer'
);
}
expect(canModerate(null)).toBe(false);
expect(canReadModeration(undefined)).toBe(false);
expect(moderatorMaximumRestrictionMs).toBe(604_800_000);
});
it('denies Super Viewer writes across every registered management route', () => {
const routes = resolve('src/routes');
const entries = readdirSync(routes, { recursive: true, withFileTypes: true });
@@ -285,6 +328,8 @@ describe('authentication and machine boundaries', () => {
'operator',
'operator-read',
'admin-read',
'moderation',
'moderation-read',
'admin',
'machine'
] as RoutePermission[]) {