chahinebrini f7c9c79365 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:30:17 +02:00

73 lines
2.0 KiB
Vue

<template>
<div class="min-h-screen bg-gray-950 flex items-center justify-center">
<div class="w-full max-w-sm rounded-xl border border-gray-800 bg-gray-900 p-8">
<div class="mb-8 text-center">
<p class="text-xs text-gray-600 uppercase tracking-widest mb-1">rebreak</p>
<h1 class="text-lg font-semibold text-white">Admin Login</h1>
<p class="text-sm text-gray-500 mt-1">Interner Zugang -- kein Public-Access</p>
</div>
<form @submit.prevent="handleLogin" class="space-y-4">
<UFormField label="E-Mail" name="email">
<UInput
v-model="email"
type="email"
placeholder="admin@rebreak.org"
autocomplete="email"
class="w-full"
:disabled="loading"
/>
</UFormField>
<UFormField label="Passwort" name="password">
<UInput
v-model="password"
type="password"
placeholder="Passwort"
autocomplete="current-password"
class="w-full"
:disabled="loading"
/>
</UFormField>
<UButton
type="submit"
label="Einloggen"
class="w-full justify-center"
:loading="loading"
:disabled="!email || !password"
/>
<p v-if="error" class="text-sm text-red-400 text-center">{{ error }}</p>
</form>
</div>
</div>
</template>
<script setup lang="ts">
// Login-Page -- kein Layout (eigene vollstaendige Seite)
definePageMeta({
layout: false,
})
const { loginWithPassword } = useAdminAuth()
const email = ref("")
const password = ref("")
const loading = ref(false)
const error = ref("")
async function handleLogin() {
loading.value = true
error.value = ""
try {
await loginWithPassword(email.value, password.value)
await navigateTo("/")
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : "Login fehlgeschlagen"
} finally {
loading.value = false
}
}
</script>