diff --git a/apps/admin/pages/domains.vue b/apps/admin/pages/domains.vue index 692d8e2..aed9184 100644 --- a/apps/admin/pages/domains.vue +++ b/apps/admin/pages/domains.vue @@ -1,14 +1,329 @@ diff --git a/apps/admin/pages/moderation.vue b/apps/admin/pages/moderation.vue index 81a680d..130ba2c 100644 --- a/apps/admin/pages/moderation.vue +++ b/apps/admin/pages/moderation.vue @@ -1,14 +1,518 @@ diff --git a/apps/admin/pages/stats.vue b/apps/admin/pages/stats.vue index 0a445c7..a3229b9 100644 --- a/apps/admin/pages/stats.vue +++ b/apps/admin/pages/stats.vue @@ -1,14 +1,243 @@ diff --git a/apps/admin/pages/users.vue b/apps/admin/pages/users.vue index b26a22a..e7159bd 100644 --- a/apps/admin/pages/users.vue +++ b/apps/admin/pages/users.vue @@ -1,17 +1,440 @@ diff --git a/apps/admin/server/api/domain-submissions.get.ts b/apps/admin/server/api/domain-submissions.get.ts new file mode 100644 index 0000000..67e7403 --- /dev/null +++ b/apps/admin/server/api/domain-submissions.get.ts @@ -0,0 +1,34 @@ +// apps/admin/server/api/domain-submissions.get.ts +// +// Server-side proxy: holt Pending-Domain-Submissions vom Backend. +// Admin-Secret bleibt server-only (NIE im Client-Bundle landen lassen). +// +// Auth-Modell: client ruft /api/domain-submissions auf (Nuxt-server-route), +// hier wird x-admin-secret aus runtimeConfig.adminSecret an Backend weitergereicht. + +export default defineEventHandler(async (_event) => { + const config = useRuntimeConfig(); + const apiBase = config.public.apiBase; + const adminSecret = config.adminSecret; + + if (!adminSecret) { + throw createError({ + statusCode: 500, + statusMessage: "ADMIN_SECRET nicht konfiguriert (Infisical-Var fehlt)", + }); + } + + try { + const data = await $fetch(`${apiBase}/api/admin/domain-submissions`, { + method: "GET", + headers: { "x-admin-secret": adminSecret }, + }); + return data; + } catch (err: any) { + throw createError({ + statusCode: err?.statusCode ?? 502, + statusMessage: + err?.statusMessage ?? err?.message ?? "Backend-Request fehlgeschlagen", + }); + } +}); diff --git a/apps/admin/server/api/domain-submissions/[id]/approve.post.ts b/apps/admin/server/api/domain-submissions/[id]/approve.post.ts new file mode 100644 index 0000000..de90471 --- /dev/null +++ b/apps/admin/server/api/domain-submissions/[id]/approve.post.ts @@ -0,0 +1,41 @@ +// apps/admin/server/api/domain-submissions/[id]/approve.post.ts +// +// Proxy: leitet Approve-Request inkl. optionaler note ans Backend. + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const apiBase = config.public.apiBase; + const adminSecret = config.adminSecret; + + if (!adminSecret) { + throw createError({ + statusCode: 500, + statusMessage: "ADMIN_SECRET nicht konfiguriert", + }); + } + + const id = getRouterParam(event, "id"); + if (!id) { + throw createError({ statusCode: 400, statusMessage: "ID fehlt" }); + } + + const body = await readBody(event).catch(() => ({})); + + try { + const data = await $fetch( + `${apiBase}/api/admin/domain-submissions/${encodeURIComponent(id)}/approve`, + { + method: "POST", + headers: { "x-admin-secret": adminSecret }, + body: body ?? {}, + }, + ); + return data; + } catch (err: any) { + throw createError({ + statusCode: err?.statusCode ?? 502, + statusMessage: + err?.statusMessage ?? err?.message ?? "Backend-Request fehlgeschlagen", + }); + } +}); diff --git a/apps/admin/server/api/domain-submissions/[id]/reject.post.ts b/apps/admin/server/api/domain-submissions/[id]/reject.post.ts new file mode 100644 index 0000000..c68ac53 --- /dev/null +++ b/apps/admin/server/api/domain-submissions/[id]/reject.post.ts @@ -0,0 +1,41 @@ +// apps/admin/server/api/domain-submissions/[id]/reject.post.ts +// +// Proxy: leitet Reject-Request inkl. optionaler note ans Backend. + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const apiBase = config.public.apiBase; + const adminSecret = config.adminSecret; + + if (!adminSecret) { + throw createError({ + statusCode: 500, + statusMessage: "ADMIN_SECRET nicht konfiguriert", + }); + } + + const id = getRouterParam(event, "id"); + if (!id) { + throw createError({ statusCode: 400, statusMessage: "ID fehlt" }); + } + + const body = await readBody(event).catch(() => ({})); + + try { + const data = await $fetch( + `${apiBase}/api/admin/domain-submissions/${encodeURIComponent(id)}/reject`, + { + method: "POST", + headers: { "x-admin-secret": adminSecret }, + body: body ?? {}, + }, + ); + return data; + } catch (err: any) { + throw createError({ + statusCode: err?.statusCode ?? 502, + statusMessage: + err?.statusMessage ?? err?.message ?? "Backend-Request fehlgeschlagen", + }); + } +}); diff --git a/apps/admin/server/api/moderation/[id]/ban-user.post.ts b/apps/admin/server/api/moderation/[id]/ban-user.post.ts new file mode 100644 index 0000000..21720ee --- /dev/null +++ b/apps/admin/server/api/moderation/[id]/ban-user.post.ts @@ -0,0 +1,39 @@ +// apps/admin/server/api/moderation/[id]/ban-user.post.ts +// +// Proxy: leitet Ban-User-Request ans Backend. Profile.banned wird gesetzt +// (gleicher Patch-Pattern wie /api/admin/users/[id]). + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const apiBase = config.public.apiBase; + const adminSecret = config.adminSecret; + + if (!adminSecret) { + throw createError({ + statusCode: 500, + statusMessage: "ADMIN_SECRET nicht konfiguriert", + }); + } + + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, statusMessage: "ID fehlt" }); + + const body = await readBody(event).catch(() => ({})); + + try { + return await $fetch( + `${apiBase}/api/admin/moderation/${encodeURIComponent(id)}/ban-user`, + { + method: "POST", + headers: { "x-admin-secret": adminSecret }, + body: body ?? {}, + }, + ); + } catch (err: any) { + throw createError({ + statusCode: err?.statusCode ?? 502, + statusMessage: + err?.statusMessage ?? err?.message ?? "Backend-Request fehlgeschlagen", + }); + } +}); diff --git a/apps/admin/server/api/moderation/[id]/delete.post.ts b/apps/admin/server/api/moderation/[id]/delete.post.ts new file mode 100644 index 0000000..995de37 --- /dev/null +++ b/apps/admin/server/api/moderation/[id]/delete.post.ts @@ -0,0 +1,39 @@ +// apps/admin/server/api/moderation/[id]/delete.post.ts +// +// Proxy: leitet Soft-Delete-Request ans Backend (content="", isDeleted=true). +// Original-Content + reporter-info bleiben in moderation_actions (audit-log). + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const apiBase = config.public.apiBase; + const adminSecret = config.adminSecret; + + if (!adminSecret) { + throw createError({ + statusCode: 500, + statusMessage: "ADMIN_SECRET nicht konfiguriert", + }); + } + + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, statusMessage: "ID fehlt" }); + + const body = await readBody(event).catch(() => ({})); + + try { + return await $fetch( + `${apiBase}/api/admin/moderation/${encodeURIComponent(id)}/delete`, + { + method: "POST", + headers: { "x-admin-secret": adminSecret }, + body: body ?? {}, + }, + ); + } catch (err: any) { + throw createError({ + statusCode: err?.statusCode ?? 502, + statusMessage: + err?.statusMessage ?? err?.message ?? "Backend-Request fehlgeschlagen", + }); + } +}); diff --git a/apps/admin/server/api/moderation/[id]/dismiss.post.ts b/apps/admin/server/api/moderation/[id]/dismiss.post.ts new file mode 100644 index 0000000..4466e92 --- /dev/null +++ b/apps/admin/server/api/moderation/[id]/dismiss.post.ts @@ -0,0 +1,38 @@ +// apps/admin/server/api/moderation/[id]/dismiss.post.ts +// +// Proxy: leitet Dismiss-Request (flag clear) ans Backend. + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const apiBase = config.public.apiBase; + const adminSecret = config.adminSecret; + + if (!adminSecret) { + throw createError({ + statusCode: 500, + statusMessage: "ADMIN_SECRET nicht konfiguriert", + }); + } + + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, statusMessage: "ID fehlt" }); + + const body = await readBody(event).catch(() => ({})); + + try { + return await $fetch( + `${apiBase}/api/admin/moderation/${encodeURIComponent(id)}/dismiss`, + { + method: "POST", + headers: { "x-admin-secret": adminSecret }, + body: body ?? {}, + }, + ); + } catch (err: any) { + throw createError({ + statusCode: err?.statusCode ?? 502, + statusMessage: + err?.statusMessage ?? err?.message ?? "Backend-Request fehlgeschlagen", + }); + } +}); diff --git a/apps/admin/server/api/moderation/queue.get.ts b/apps/admin/server/api/moderation/queue.get.ts new file mode 100644 index 0000000..d7d6f15 --- /dev/null +++ b/apps/admin/server/api/moderation/queue.get.ts @@ -0,0 +1,62 @@ +// apps/admin/server/api/moderation/queue.get.ts +// +// Server-side proxy: holt die Moderation-Queue (gemeldete Posts + Comments) +// vom Backend. Admin-Secret bleibt server-only. +// +// Query-Forwarding: cursor + limit werden an Backend durchgereicht. + +export interface ModerationItem { + id: string; + type: "post" | "comment"; + content: string; + postId: string | null; + userId: string; + reportedAt: string | null; + createdAt: string; + isDeleted: boolean; + author: { + id: string; + nickname: string | null; + avatar: string | null; + plan: string; + } | null; +} + +export interface ModerationQueueResponse { + items: ModerationItem[]; + nextCursor: string | null; +} + +export default defineEventHandler( + async (event): Promise => { + const config = useRuntimeConfig(); + const apiBase = config.public.apiBase; + const adminSecret = config.adminSecret; + + if (!adminSecret) { + throw createError({ + statusCode: 500, + statusMessage: "ADMIN_SECRET nicht konfiguriert (Infisical-Var fehlt)", + }); + } + + const query = getQuery(event); + + try { + return await $fetch( + `${apiBase}/api/admin/moderation/queue`, + { + method: "GET", + headers: { "x-admin-secret": adminSecret }, + query, + }, + ); + } catch (err: any) { + throw createError({ + statusCode: err?.statusCode ?? 502, + statusMessage: + err?.statusMessage ?? err?.message ?? "Backend-Request fehlgeschlagen", + }); + } + }, +); diff --git a/apps/admin/server/api/stats.get.ts b/apps/admin/server/api/stats.get.ts new file mode 100644 index 0000000..98537fc --- /dev/null +++ b/apps/admin/server/api/stats.get.ts @@ -0,0 +1,42 @@ +// apps/admin/server/api/stats.get.ts +// +// Server-side proxy: holt aggregierte Admin-Stats vom Backend. +// Admin-Secret bleibt server-only (NIE im Client-Bundle landen lassen). +// +// Auth-Modell: client ruft /api/stats auf (Nuxt-server-route), +// hier wird x-admin-secret aus runtimeConfig.adminSecret an Backend weitergereicht. + +export interface AdminStats { + users: { total: number; newThisWeek: number }; + posts: { total: number; newThisWeek: number }; + domains: { pending: number; approved: number }; + feedback: { pending: number; total: number }; + lyra: { postsLast30d: number }; +} + +export default defineEventHandler(async (_event): Promise => { + const config = useRuntimeConfig(); + const apiBase = config.public.apiBase; + const adminSecret = config.adminSecret; + + if (!adminSecret) { + throw createError({ + statusCode: 500, + statusMessage: "ADMIN_SECRET nicht konfiguriert (Infisical-Var fehlt)", + }); + } + + try { + const data = await $fetch(`${apiBase}/api/admin/stats`, { + method: "GET", + headers: { "x-admin-secret": adminSecret }, + }); + return data; + } catch (err: any) { + throw createError({ + statusCode: err?.statusCode ?? 502, + statusMessage: + err?.statusMessage ?? err?.message ?? "Backend-Request fehlgeschlagen", + }); + } +}); diff --git a/apps/admin/server/api/users.get.ts b/apps/admin/server/api/users.get.ts new file mode 100644 index 0000000..a0717ad --- /dev/null +++ b/apps/admin/server/api/users.get.ts @@ -0,0 +1,27 @@ +// Admin-App proxy: forwards to backend /api/admin/users mit x-admin-secret +// (Pattern wie andere admin-pages — admin-secret bleibt server-side, nie im Browser). +// +// Query-Params werden 1:1 weitergereicht. Backend macht die Validierung. + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const query = getQuery(event); + + const params = new URLSearchParams(); + for (const [k, v] of Object.entries(query)) { + if (v !== undefined && v !== null && v !== "") { + params.set(k, String(v)); + } + } + + const url = `${config.public.apiBase}/api/admin/users${ + params.toString() ? `?${params.toString()}` : "" + }`; + + return $fetch(url, { + method: "GET", + headers: { + "x-admin-secret": config.adminSecret as string, + }, + }); +}); diff --git a/apps/admin/server/api/users/[id].delete.ts b/apps/admin/server/api/users/[id].delete.ts new file mode 100644 index 0000000..97f4a9e --- /dev/null +++ b/apps/admin/server/api/users/[id].delete.ts @@ -0,0 +1,14 @@ +// Admin-App proxy: DELETE /api/admin/users/[id] — soft-delete (DSGVO). + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "User-ID fehlt" }); + + return $fetch(`${config.public.apiBase}/api/admin/users/${id}`, { + method: "DELETE", + headers: { + "x-admin-secret": config.adminSecret as string, + }, + }); +}); diff --git a/apps/admin/server/api/users/[id].patch.ts b/apps/admin/server/api/users/[id].patch.ts new file mode 100644 index 0000000..0ecdfb3 --- /dev/null +++ b/apps/admin/server/api/users/[id].patch.ts @@ -0,0 +1,19 @@ +// Admin-App proxy: PATCH /api/admin/users/[id] +// Forwards body 1:1 ans Backend mit x-admin-secret. + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, message: "User-ID fehlt" }); + + const body = await readBody(event).catch(() => ({})); + + return $fetch(`${config.public.apiBase}/api/admin/users/${id}`, { + method: "PATCH", + headers: { + "x-admin-secret": config.adminSecret as string, + "content-type": "application/json", + }, + body, + }); +});