4 page-implementations + server-route-proxies (admin-secret stays server-only): DOMAINS (apps/admin/pages/domains.vue): - UTable mit pending-submissions queue - Approve / Reject buttons per row - Reject-confirm-modal mit optional note - useToast + refresh nach action - 3 server-routes: GET list + POST approve/reject STATS (apps/admin/pages/stats.vue): - Stat-cards: Total Users + delta-week, Total Posts + delta-week, Domains pending (link to /domains), Domains approved, Feedback pending, Lyra-Posts (30d) - UProgress für Domain-Approval-Quote - Auto-refresh 60s + manual refresh-button - USkeleton während loading - 1 server-route: GET /api/stats USERS (apps/admin/pages/users.vue): - UTable mit avatar+nickname/username, plan-badge, streak, status, createdAt - Search-input + plan-filter dropdown - Action-dropdown per row: Plan-Change / Ban-Toggle / Soft-Delete - 3 separate UModals mit confirm-pattern - Cursor-pagination (Mehr laden button) - 3 server-routes: GET list, PATCH /:id, DELETE /:id MODERATION (apps/admin/pages/moderation.vue): - Stack-layout mit card-pro-item (statt table — content-preview braucht space) - Type-badge (Post/Comment), Author + Plan-badge, content-preview (200 chars), reportedAt - Action-buttons: Dismiss (gray), Delete Content (red soft + reason-modal), Ban User (red solid + warning-modal) - Empty-state, cursor-pagination - 4 server-routes: GET /queue, POST /:id/dismiss/delete/ban-user Server-route pattern (apps/admin/server/api/...): - Use useRuntimeConfig().adminSecret server-only - Client never sees x-admin-secret - Body/query passthrough to backend Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
43 lines
1.3 KiB
TypeScript
43 lines
1.3 KiB
TypeScript
// 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<AdminStats> => {
|
|
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<AdminStats>(`${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",
|
|
});
|
|
}
|
|
});
|