chahinebrini 056726a166 feat(admin): Phase 2 Backend — Users + Moderation endpoints + 2 schema migrations
Two parallel agent-batches consolidated:

USERS-MGMT (rebreak-backend agent):
- Schema: Profile gets banned, bannedAt, bannedReason, deletedAt + indexes
- Migration: 20260509_profile_admin_management (additive, idempotent)
- DB-layer backend/server/db/adminUsers.ts:
  listAdminUsers (cursor-pagination, search, plan-filter)
  updateAdminUser (plan-validation, ban-stamping)
  softDeleteAdminUser (DSGVO PII-scrub: nickname=null, email=deleted-{shortid}@deleted.local)
- 3 endpoints under /api/admin/users:
  GET (list with ?cursor&limit&q&plan&includeDeleted)
  PATCH /:id (plan/banned/bannedReason/lyraVoiceId)
  DELETE /:id (soft-delete idempotent)
- 12 tests passing

MODERATION (rebreak-backend agent):
- Schema: CommunityPost+CommunityReply get isModerated, isDeleted, deletedAt,
  reportedAt + index (is_moderated, reported_at)
- New ModerationAction model → audit-log table
- Migration: 20260509_moderation_queue (additive, idempotent)
- DB-layer backend/server/db/moderation.ts:
  listModerationQueue (merge posts+comments, sort by reportedAt, cursor)
  dismissModerationItem
  deleteModerationItem (content scrub + audit snapshot)
  banUserFromModerationItem (reuses banned/bannedAt/bannedReason fields)
- 4 endpoints under /api/admin/moderation:
  GET /queue, POST /:id/dismiss, POST /:id/delete, POST /:id/ban-user
- 11 tests passing

Backend total: 78 tests passing | 4 skipped (pre-existing requireAdmin tests)

Auth: x-admin-secret header (consistent with existing /admin/* endpoints).

DSGVO:
- Soft-delete scrubt PII statt hard-delete
- Email NICHT in admin user-list (lebt nur in auth.users)
- Audit-log für moderation-actions (90-day cleanup-cron pending hans-mueller-DSB-review)

⚠️ MIGRATIONS — auto-deploy via pipeline (commit b38bf17 detection):
- 20260509_profile_admin_management
- 20260509_moderation_queue

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:48:35 +02:00

63 lines
1.8 KiB
TypeScript

import { updateAdminUser } from "../../../db/adminUsers";
/**
* PATCH /api/admin/users/[id] — Admin-Update für plan / banned / lyraVoiceId
*
* Body:
* {
* plan?: "free" | "pro" | "legend",
* banned?: boolean,
* bannedReason?: string | null,
* lyraVoiceId?: string | null
* }
*
* Returns: updated user-row (admin-projection).
*
* Auth: x-admin-secret.
*
* Audit: TODO(hans-mueller) — sobald audit_log table existiert, hier write
* { actor, target_user_id, action: "admin_user_update", diff, ts }.
* Aktuell wird die Änderung nur per console.log gespiegelt.
*/
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
const secret = getHeader(event, "x-admin-secret");
if (!config.adminSecret || secret !== config.adminSecret) {
throw createError({ statusCode: 401, message: "Unauthorized" });
}
const id = getRouterParam(event, "id");
if (!id) throw createError({ statusCode: 400, message: "User-ID fehlt" });
const body = (await readBody(event).catch(() => ({}))) as Record<
string,
unknown
>;
const patch = {
plan: typeof body.plan === "string" ? body.plan : undefined,
banned: typeof body.banned === "boolean" ? body.banned : undefined,
bannedReason:
body.bannedReason === null
? null
: typeof body.bannedReason === "string"
? body.bannedReason
: undefined,
lyraVoiceId:
body.lyraVoiceId === null
? null
: typeof body.lyraVoiceId === "string"
? body.lyraVoiceId
: undefined,
};
const updated = await updateAdminUser(id, patch);
// Console-audit-trail bis dedicated audit_log table verfügbar ist
console.log(
`[admin/users] PATCH user=${id} patch=${JSON.stringify(patch)} → plan=${updated.plan} banned=${updated.banned}`,
);
return updated;
});