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>
73 lines
2.0 KiB
Vue
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>
|