chahinebrini d3dfa74cf8 feat(admin): Admin App initial commit + Deploy-Infrastructure
apps/admin/:
- Nuxt 4.1.3 + @nuxt/ui 4 + @nuxtjs/supabase, port 3017 staging
- 7 pages: index (59 LOC dashboard), login (72 LOC), auth/confirm, plus stubs
  für domains/users/stats/moderation (14-17 LOC each, content für separate
  Phase 2 Session)
- composables/useAdminAuth.ts: Supabase login + verifyAdminRole hook
- middleware/admin-auth.ts: route guard (Phase 3 backend-check ready)
- layouts/default.vue, app.vue, README.md
- nuxt.config.ts: SSR=true, port 3017, dark-mode preference, Supabase
  pkce-flow, runtimeConfig.adminSecret für Phase 3 backend-binding

Deploy-Infrastructure:
- .github/workflows/deploy-admin-staging.yml: build admin auf push to main mit
  path-filter apps/admin/**, scp tar zu Server, atomic-mv + pm2 restart
- scripts/deploy-admin-from-artifact.sh: Server-side deploy (extract, atomic mv,
  pm2 reload). Kein prisma-migrate (admin hat kein eigenes DB-Schema).
- apps/admin/start-admin-staging.sh: pm2 start-script mit Infisical-wrapper,
  port 3017, mappt Infisical SUPABASE_URL/KEY auf NUXT_PUBLIC_*
- ecosystem.config.js: rebreak-admin-staging Eintrag (port 3017,
  max_memory_restart 400M)
- ops/nginx/admin-staging.rebreak.org.conf: HTTP→HTTPS redirect, SSL paths,
  proxy auf 127.0.0.1:3017, noindex header

Pending User-Actions für go-live:
1. DNS-A-Record admin.staging.rebreak.org → 49.13.55.22
2. SSL-cert via certbot (oder bestehender wildcard *.staging.rebreak.org)
3. nginx-config auf Server aktivieren (sudo cp + ln + reload)
4. pm2 initial start: pm2 start ecosystem.config.js --only rebreak-admin-staging
5. Infisical-secret ADMIN_SECRET (server-only, Phase 3 binding)

GH-Actions: keine neuen Secrets (nutzt bestehende HETZNER_SSH_KEY/HOST/USER)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:17:20 +02:00

67 lines
2.3 KiB
TypeScript

// composables/useAdminAuth.ts
//
// Admin-Auth-Composable.
//
// Auth-Architektur:
// 1. User loggt sich via Supabase (Email/Password) ein -- normaler Supabase-JWT.
// 2. Nach Login: Backend-Aufruf gegen GET /api/admin/verify-admin (Phase 3).
// Das Backend prueft ob die Supabase-User-ID in der admin_users-Tabelle steht.
// Bei Fehlschlag: sofort ausloggen (kein einfacher User darf rein).
// 3. Admin-Status wird in useSupabaseUser() gehalten -- kein extra State noetig.
//
// Phase 3 TODO: Backend muss /api/admin/verify-admin implementieren mit requireAdmin-Middleware.
//
// DSGVO-Note: Admin-Logins werden server-side in audit_log geloggt (Phase 4 -- hans-mueller).
export function useAdminAuth() {
const supabase = useSupabaseClient()
const user = useSupabaseUser()
const config = useRuntimeConfig()
// Computed E-Mail fuer Topbar-Anzeige
const adminEmail = computed(() => user.value?.email ?? "")
// Login via Supabase Email/Password
async function loginWithPassword(email: string, password: string) {
const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error) throw new Error(error.message)
// Phase 3: Admin-Verifikation gegen Backend.
// Aktuell nur Supabase-Login -- requireAdmin-Check kommt in Phase 3.
// TODO: await verifyAdminRole()
}
// Logout -- Supabase-Session beenden, zurueck zu /login
async function logout() {
await supabase.auth.signOut()
await navigateTo("/login")
}
// Phase 3: Backend-Check ob Supabase-User in admin_users-Tabelle steht.
// Wirft Error wenn nicht -- Caller soll dann logout() aufrufen.
async function verifyAdminRole() {
const session = await supabase.auth.getSession()
const token = session.data.session?.access_token
if (!token) throw new Error("Keine aktive Session")
const res = await $fetch(`${config.public.apiBase}/api/admin/verify-admin`, {
method: "GET",
headers: { Authorization: `Bearer ${token}` },
})
// Backend gibt { isAdmin: true } zurueck -- alles andere ist Zugriffsverweigerung.
if (!(res as { isAdmin: boolean }).isAdmin) {
await supabase.auth.signOut()
throw new Error("Kein Admin-Zugriff")
}
}
return {
user,
adminEmail,
loginWithPassword,
logout,
verifyAdminRole,
}
}