feat(isac): add moderated data contributions
This commit is contained in:
@@ -31,6 +31,15 @@ export async function requirePermission(user: AuthUser | null, permission: strin
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function requireDataView(user: AuthUser | null): Promise<AuthUser> {
|
||||
if (!user) error(401, 'Sign in required');
|
||||
const permissions = await getPermissions(user);
|
||||
if (!permissions.includes('*') && !['data.view', 'data.manage', 'data.propose', 'data.approve'].some((permission) => permissions.includes(permission))) {
|
||||
error(403, 'ISAC data access required');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
export function requireAdmin(user: AuthUser | null): asserts user is AuthUser {
|
||||
if (!user) error(401, 'Sign in required');
|
||||
if (user.role !== 'admin') error(403, 'Administrator permission required');
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { getDatabase } from '$lib/server/database';
|
||||
import { getPermissions } from '$lib/server/auth/authorization';
|
||||
import type { AuthUser } from '$lib/server/auth/session';
|
||||
import { recordIsacAudit } from '$lib/server/audit';
|
||||
|
||||
export const DATA_MODULES = ['stats', 'talents', 'brands', 'gear', 'weapons', 'protocols'] as const;
|
||||
export type DataModule = typeof DATA_MODULES[number];
|
||||
|
||||
type SerializedEntry =
|
||||
| { name: string; kind: 'text'; value: string }
|
||||
| { name: string; kind: 'file'; value: string; fileName: string; type: string };
|
||||
|
||||
async function serialize(form: FormData): Promise<SerializedEntry[]> {
|
||||
const entries: SerializedEntry[] = [];
|
||||
for (const [name, value] of form.entries()) {
|
||||
if (typeof value === 'string') entries.push({ name, kind: 'text', value });
|
||||
else if (value.size > 0) entries.push({
|
||||
name,
|
||||
kind: 'file',
|
||||
value: Buffer.from(await value.arrayBuffer()).toString('base64'),
|
||||
fileName: value.name,
|
||||
type: value.type
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function deserializeProposal(payload: string): FormData {
|
||||
const form = new FormData();
|
||||
for (const entry of JSON.parse(payload) as SerializedEntry[]) {
|
||||
if (entry.kind === 'text') form.append(entry.name, entry.value);
|
||||
else form.append(entry.name, new Blob([Buffer.from(entry.value, 'base64')], { type: entry.type }), entry.fileName);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
|
||||
export async function queueDataProposal(user: AuthUser, module: DataModule, action: string, form: FormData) {
|
||||
const id = randomUUID();
|
||||
const entityId = String(form.get('id') ?? '').trim() || null;
|
||||
const name = String(form.get('name') ?? '').trim();
|
||||
const summary = `${action.replace(/([A-Z])/g, ' $1').toUpperCase()} · ${name || entityId || 'NEW RECORD'}`;
|
||||
const now = new Date().toISOString();
|
||||
const payload = JSON.stringify(await serialize(form));
|
||||
const database = await getDatabase();
|
||||
await (await database.prepare(`INSERT INTO data_change_proposals
|
||||
(id,module,action,entity_id,summary,payload_json,status,proposed_by,created_at)
|
||||
VALUES(?,?,?,?,?,?,'pending',?,?)`)).run(id, module, action, entityId, summary, payload, user.id, now);
|
||||
await recordIsacAudit({
|
||||
actorId: user.id,
|
||||
category: 'data_moderation',
|
||||
action: 'proposal.created',
|
||||
entityId: id,
|
||||
before: null,
|
||||
after: { module, action, entityId, summary }
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
export async function shouldModerateDataMutation(user: AuthUser | null) {
|
||||
const permissions = await getPermissions(user);
|
||||
return {
|
||||
canManage: permissions.includes('*') || permissions.includes('data.manage'),
|
||||
canPropose: permissions.includes('*') || permissions.includes('data.propose')
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user