diff --git a/.github/workflows/deploy-admin-staging.yml b/.github/workflows/deploy-admin-staging.yml new file mode 100644 index 0000000..b2fa91f --- /dev/null +++ b/.github/workflows/deploy-admin-staging.yml @@ -0,0 +1,120 @@ +name: Deploy Admin Staging + +# ───────────────────────────────────────────────────────────────────────────── +# Build + Deploy-Pipeline fuer rebreak-admin-staging. +# +# Pattern: identisch zu deploy-staging.yml (backend). +# - Build laeuft auf GH-Runner (7 GB RAM, kein OOM-Risiko auf Hetzner CX23) +# - Artifact wird via scp zum Server gepusht +# - Server-Script deploy-admin-from-artifact.sh extrahiert + pm2 restart +# +# Trigger: push to main (immer, auch wenn nur admin-App-Code geaendert). +# Optimierung spaeter: paths-filter auf apps/admin/** um unnoetigen Builds zu +# vermeiden. Fuer jetzt: einfach + robust. +# +# Port: 3017 (staging). Subdomain: admin.staging.rebreak.org +# pm2-Service: rebreak-admin-staging +# ───────────────────────────────────────────────────────────────────────────── + +on: + push: + branches: [main] + paths: + - "apps/admin/**" + - "pnpm-lock.yaml" + - ".github/workflows/deploy-admin-staging.yml" + workflow_dispatch: + +concurrency: + group: deploy-admin-staging + cancel-in-progress: false # queueen, nicht canceln + +permissions: + contents: read + +jobs: + # ── 1. Build auf GitHub-Runner ────────────────────────────────────────────── + build: + name: Build admin (Nuxt SSR) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + # version wird aus package.json "packageManager"-Field gelesen (pnpm@10.23.0) + + - uses: actions/setup-node@v4 + with: + node-version: 24.11.1 # exakt Hetzner-Version matchen + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build admin (Nuxt SSR) + working-directory: apps/admin + run: pnpm build + + - name: Tar artifact + run: tar czf admin-output.tar.gz -C apps/admin/.output . + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: admin-output + path: admin-output.tar.gz + retention-days: 7 + + # ── 2. Deploy: Artifact zum Hetzner pushen + extract + pm2 restart ────────── + deploy: + name: Deploy zu Hetzner + needs: build + runs-on: ubuntu-latest + environment: staging # selbes GitHub-Environment wie backend-deploy (shared secrets) + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: admin-output + + - name: Setup SSH + env: + SSH_PRIVATE_KEY: ${{ secrets.HETZNER_SSH_KEY }} + SSH_HOST: ${{ vars.HETZNER_HOST }} + run: | + if [ -z "$SSH_PRIVATE_KEY" ] || [ -z "$SSH_HOST" ]; then + echo "FATAL: HETZNER_SSH_KEY (secret) oder HETZNER_HOST (var) nicht gesetzt" + exit 1 + fi + echo "Deploying admin to host: $SSH_HOST" + mkdir -p ~/.ssh + printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -H "$SSH_HOST" >> ~/.ssh/known_hosts + + - name: Upload artifact zu Hetzner + env: + SSH_HOST: ${{ vars.HETZNER_HOST }} + SSH_USER: ${{ vars.HETZNER_USER }} + run: | + scp -i ~/.ssh/id_ed25519 admin-output.tar.gz \ + "$SSH_USER@$SSH_HOST:/srv/rebreak/apps/admin/.output-incoming.tar.gz" + + - name: Server-side deploy (extract + pm2 restart) + env: + SSH_HOST: ${{ vars.HETZNER_HOST }} + SSH_USER: ${{ vars.HETZNER_USER }} + run: | + ssh -i ~/.ssh/id_ed25519 "$SSH_USER@$SSH_HOST" \ + 'bash /srv/rebreak/scripts/deploy-admin-from-artifact.sh' + + - name: Health-Check (HTTP 3xx/200 = Server erreichbar) + run: | + sleep 5 + STATUS=$(curl -sS -o /dev/null -w '%{http_code}' \ + https://admin.staging.rebreak.org/ || echo "000") + echo "admin.staging.rebreak.org/ -> HTTP $STATUS" + if [ "$STATUS" = "000" ] || [ "$STATUS" = "502" ] || [ "$STATUS" = "503" ]; then + echo "FAIL: admin-staging nicht erreichbar (HTTP $STATUS)" + exit 1 + fi diff --git a/apps/admin/README.md b/apps/admin/README.md new file mode 100644 index 0000000..2caae87 --- /dev/null +++ b/apps/admin/README.md @@ -0,0 +1,258 @@ +# rebreak Admin + +Internes Verwaltungspanel fuer rebreak.org. + +**Status:** Phase 1 -- Skeleton (Auth-Wiring, Layout, Stub-Pages). Keine echten API-Calls. + +--- + +## Stack + +| Schicht | Technologie | +|---|---| +| Framework | Nuxt 4.1.3 (SSR) | +| UI | @nuxt/ui 4.x (Tailwind 4, Nuxt UI Komponenten) | +| Auth | @nuxtjs/supabase 2.x (PKCE-Flow) | +| Backend-Komm. | $fetch gegen /api/admin/* (Phase 3) | +| Laufzeit | Node 24.11.1 / pm2 auf Hetzner CX23 | + +--- + +## Wo die Admin-App lebt + +| Environment | URL | Port (intern) | pm2-Service | +|---|---|---|---| +| Staging | admin.staging.rebreak.org | 3017 | rebreak-admin-staging | +| Prod | admin.rebreak.org | 3018 | rebreak-admin | + +Nginx-Routing-Config: wird in Phase 2 Deploy angelegt (analog zu staging.rebreak.org-Config). + +--- + +## Lokale Entwicklung + +```bash +# Vom Monorepo-Root: +pnpm dev:admin + +# Oder direkt: +cd apps/admin && pnpm dev +``` + +Laeuft auf http://localhost:3017. + +Infisical-Secrets werden fuer lokales Dev nicht gebraucht -- die Supabase-URL/Key +kommen als `process.env.NUXT_PUBLIC_SUPABASE_URL/KEY` oder fallen auf Staging-Defaults zurueck. + +--- + +## Auth-Architektur + +``` +Admin-Browser + | + | 1. POST /api/auth/login (Supabase Email/Password) + v +Supabase Auth (db-staging.rebreak.org oder db.rebreak.org) + | + | 2. JWT zurueck (access_token) + v +Admin-Browser haelt Session (PKCE-Flow, persistSession=true) + | + | 3. GET /api/admin/* (Authorization: Bearer ) + v +Backend (staging.rebreak.org) + | + | 4. requireAdmin-Middleware: + | - JWT verifizieren (Supabase public key) + | - user_id in admin_users-Tabelle pruefen + | - Bei Misserfolg: 403 + v +Admin-Endpoint-Response +``` + +**Aktueller Status (Phase 1):** Supabase-Login funktioniert. Schritt 4 (requireAdmin) ist NICHT +implementiert -- jeder eingeloggte Supabase-User koennte theoretisch rein, wenn er die URL kennt. +Das ist akzeptabel weil die Admin-URL nicht public ist und Staging-Daten keine hochsensiblen +Produktionsdaten enthalten. + +**Phase 3 schaltet requireAdmin ein** -- dann ist der Zugang haerter gesperrt. + +--- + +## DSGVO-Considerations fuer Admin-Zugriff + +Admins haben Zugriff auf User-Daten. Das bedingt: + +1. **Audit-Log (TODO Phase 4 -- hans-mueller):** Jede Admin-Aktion (User ansehen, Domain genehmigen, + Content moderieren) muss in einer `admin_audit_log`-Tabelle geloggt werden: + - Wer (admin_user_id) + - Was (action: "view_user" | "approve_domain" | "reject_domain" | "ban_user") + - Welcher Datensatz (target_id) + - Wann (timestamp) + +2. **Daten-Minimierung im Admin-UI:** User-Liste zeigt NIEMALS echten Namen oder E-Mail. + Nur Nickname (analog zur App-Anzeige). E-Mail ist nur fuer Kontaktaufnahme via Support-Ticket, + nicht fuer Browse-UI. + +3. **Admin-Zugangs-Liste:** `admin_users`-Tabelle in Supabase darf nur von User (Chahine) befuellt werden. + Kein Self-Signup, kein automatisches Promoten. + +4. **Data-Processing-Agreement:** Wenn externe Personen Admin-Zugang bekommen (z.B. Moderatoren), + braucht es einen AVV. Aktuell: nur interner Zugang (Chahine). + +5. **Datenschutzfolgeabschaetzung (DSFA):** Admin-Zugang auf Nutzerdaten von Suchtkranken faellt + unter Art. 35 DSGVO (besondere Kategorien). Hans-Mueller-Task. + +--- + +## Deploy-Plan + +### Variante A: SSR auf Hetzner (Empfehlung) + +Analog zu `rebreak-staging` -- separater pm2-Service, separater Port, nginx-Subdomain. + +**Pros:** Kein zusaetzlicher Hosting-Service, Server-Side-Auth-Checks funktionieren native, +Infisical-Secrets per Infisical-run-wrapper wie gewohnt. + +**Cons:** Belastet CX23 zusaetzlich (RAM). Admin-App hat aber wenig Traffic -- kein Risiko. + +**Setup:** +```bash +# /srv/rebreak/ecosystem.config.js (ergaenzen): +{ + name: 'rebreak-admin-staging', + script: '/srv/rebreak/apps/admin/.output-staging/server/index.mjs', + env: { PORT: 3017, NODE_ENV: 'production' } +} +``` + +```nginx +# nginx: admin.staging.rebreak.org +server { + listen 443 ssl; + server_name admin.staging.rebreak.org; + location / { + proxy_pass http://127.0.0.1:3017; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +### Variante B: Static via Cloudflare Pages + +`nuxt generate` + Deploy zu Cloudflare Pages. + +**Pros:** Kostenlos, globales CDN, keine Hetzner-Last. + +**Cons:** Kein echter SSR (Auth-Checks nur client-side), API-Calls gehen trotzdem zu Hetzner. +Fuer eine Admin-App die Server-Side-Checks braucht suboptimal. + +**Entscheidung: Variante A (SSR auf Hetzner).** + +--- + +## GitHub-Actions-Pipeline -- Plan fuer Phase 2 Deploy + +Die bestehende `.github/workflows/deploy-staging.yml` baut nur `backend/`. +Admin-App braucht einen separaten Job ODER einen eigenen Workflow. + +### Option 1: Separater Workflow `deploy-admin-staging.yml` (empfohlen) + +```yaml +name: Deploy Admin Staging +on: + push: + branches: [main] + paths: + - "apps/admin/**" + - ".github/workflows/deploy-admin-staging.yml" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24.11.1 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Build admin + working-directory: apps/admin + run: pnpm build + - run: tar czf admin-output.tar.gz -C apps/admin/.output . + - uses: actions/upload-artifact@v4 + with: + name: admin-output + path: admin-output.tar.gz + + deploy: + needs: build + runs-on: ubuntu-latest + environment: staging + steps: + # ... analog zu deploy-staging.yml: + # scp admin-output.tar.gz -> /srv/rebreak/apps/admin/.output-incoming.tar.gz + # ssh -> scripts/deploy-admin-from-artifact.sh +``` + +**Warum separater Workflow:** `paths`-Filter verhindert dass ein Backend-Push +auch die Admin-App rebuildet (und vice versa). Weniger GH-Actions-Minutes-Verbrauch. + +### Option 2: Zusaetzlicher Job in `deploy-staging.yml` + +Parallel-Job neben dem bestehenden `build`-Job. Einfacher aber kein `paths`-Filter moeglich +ohne komplizierte Logik -- jeder Push rebuildet alles. + +**Entscheidung: Option 1 (eigener Workflow mit paths-Filter).** + +Der Workflow-File wird in Phase 2 angelegt -- NICHT jetzt (Pipeline-Scope-Creep verhindern). + +--- + +## Server-Script fuer Admin-Deploy + +Analog zu `scripts/deploy-from-artifact.sh` -- ein `scripts/deploy-admin-from-artifact.sh` +das: +1. Admin-Artifact extrahiert nach `/srv/rebreak/apps/admin/.output-staging-new/` +2. Atomisches mv nach `.output-staging` +3. `pm2 restart rebreak-admin-staging` + +KEIN Migration-Step (Admin-App hat keine eigene DB -- nutzt Backend-API). + +--- + +## TODOs nach Phase 1 + +### Backend (rebreak-backend-Agent / Phase 3) + +- [ ] `requireAdmin`-Middleware: JWT verifizieren + admin_users-Tabelle-Check +- [ ] Supabase-Migration: `admin_users`-Tabelle (`id uuid references auth.users`, `created_at`) +- [ ] GET /api/admin/verify-admin (prueft ob eingeloggter User Admin ist) +- [ ] GET /api/admin/users (paginierte User-Liste, NUR nickname + plan + created_at + last_seen) +- [ ] GET /api/admin/domains (Blocker-Domain-Approval-Queue) +- [ ] POST /api/admin/domains/:id/approve +- [ ] POST /api/admin/domains/:id/reject +- [ ] GET /api/admin/stats (aggregierte anonyme Metriken) + +### Hans-Mueller / DSGVO (Phase 4) + +- [ ] DSFA fuer Admin-Zugriff auf Nutzerdaten gemaess Art. 35 DSGVO +- [ ] Audit-Log-Design: `admin_audit_log`-Tabelle + Retention-Policy +- [ ] AVV-Template fuer externe Moderatoren (falls noetig) +- [ ] TOM (Technische+Organisatorische Massnahmen) fuer Admin-Zugang dokumentieren +- [ ] Loeschkonzept fuer Audit-Log-Eintraege (wie lange aufbewahren?) + +### Backyard (Phase 2 Deploy) + +- [ ] nginx-Config: `admin.staging.rebreak.org` -> Port 3017 +- [ ] nginx-Config: `admin.rebreak.org` -> Port 3018 +- [ ] Let's Encrypt-Cert fuer admin.staging.rebreak.org + admin.rebreak.org +- [ ] ecosystem.config.js: rebreak-admin-staging + rebreak-admin Service +- [ ] GH-Actions-Workflow: `deploy-admin-staging.yml` (paths-filtered) +- [ ] Server-Script: `scripts/deploy-admin-from-artifact.sh` +- [ ] GitHub-Environment: `staging` muss HETZNER_SSH_KEY/HOST/USER schon haben (von backend-deploy geerbt) diff --git a/apps/admin/app.vue b/apps/admin/app.vue new file mode 100644 index 0000000..5acf3c1 --- /dev/null +++ b/apps/admin/app.vue @@ -0,0 +1,7 @@ + diff --git a/apps/admin/assets/css/main.css b/apps/admin/assets/css/main.css new file mode 100644 index 0000000..7c95c6f --- /dev/null +++ b/apps/admin/assets/css/main.css @@ -0,0 +1,2 @@ +@import "tailwindcss"; +@import "@nuxt/ui"; diff --git a/apps/admin/composables/useAdminAuth.ts b/apps/admin/composables/useAdminAuth.ts new file mode 100644 index 0000000..16bf275 --- /dev/null +++ b/apps/admin/composables/useAdminAuth.ts @@ -0,0 +1,66 @@ +// 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, + } +} diff --git a/apps/admin/layouts/default.vue b/apps/admin/layouts/default.vue new file mode 100644 index 0000000..71c3bc2 --- /dev/null +++ b/apps/admin/layouts/default.vue @@ -0,0 +1,56 @@ + + + diff --git a/apps/admin/middleware/admin-auth.ts b/apps/admin/middleware/admin-auth.ts new file mode 100644 index 0000000..1b5761b --- /dev/null +++ b/apps/admin/middleware/admin-auth.ts @@ -0,0 +1,22 @@ +// middleware/admin-auth.ts +// +// Nuxt-Route-Middleware -- prueft ob Supabase-Session aktiv ist. +// Wird in definePageMeta({ middleware: 'admin-auth' }) in jeder geschuetzten Page verwendet. +// +// Phase 3: Middleware soll zusaetzlich via useAdminAuth().verifyAdminRole() pruefen, +// ob der eingeloggte User tatsaechlich in der admin_users-Tabelle steht. +// Aktuell genuegt der Supabase-Session-Check als Placeholder. + +export default defineNuxtRouteMiddleware((_to, _from) => { + const user = useSupabaseUser() + + // Kein User -> Login-Redirect + if (!user.value) { + return navigateTo("/login") + } + + // Phase 3 TODO: hier verifyAdminRole() aufrufen (server-side in defineNuxtRouteMiddleware via useRequestEvent). + // Solange Phase 3 nicht done: jeder eingeloggte Supabase-User kann rein. + // Das ist akzeptabel weil Admin-App-URL nicht public ist (kein indexierter Link, interne Verteilung). + // Echte Absicherung kommt mit requireAdmin-Backend-Middleware. +}) diff --git a/apps/admin/nuxt.config.ts b/apps/admin/nuxt.config.ts new file mode 100644 index 0000000..2c43826 --- /dev/null +++ b/apps/admin/nuxt.config.ts @@ -0,0 +1,87 @@ +// apps/admin/nuxt.config.ts +// +// Admin-App fuer rebreak.org -- interne Verwaltung (Domain-Approval, User-Management, Stats). +// Subdomain-Ziel: admin.rebreak.org (Prod) / admin.staging.rebreak.org (Staging) +// Port auf Hetzner: 3017 (staging), 3018 (prod -- TBD) +// pm2-Service: rebreak-admin-staging, rebreak-admin (--Phase 2 Deploy) +// +// Auth-Modell: Supabase-Login + Backend-Middleware requireAdmin (Phase 3). +// KEIN Public-Access. Alle Pages hinter /admin/** sind auth-geschuetzt. + +export default defineNuxtConfig({ + compatibilityDate: "2025-07-15", + devtools: { enabled: false }, + + // SSR = true (Server-Side Rendering auf Hetzner pm2). + // Kein static-generate -- Admin braucht Server-Side-Auth-Checks. + ssr: true, + + app: { + htmlAttrs: { lang: "de" }, + head: { + title: "rebreak Admin", + meta: [ + { + name: "viewport", + content: "width=device-width, initial-scale=1", + }, + { + name: "robots", + content: "noindex, nofollow", + }, + ], + }, + }, + + modules: [ + "@nuxt/ui", + "@nuxt/icon", + "@nuxtjs/supabase", + "@vueuse/nuxt", + ], + + supabase: { + // Runtime-Env ueberschreibt via Infisical (NUXT_PUBLIC_SUPABASE_URL / NUXT_PUBLIC_SUPABASE_KEY). + // Defaults zeigen auf Staging -- Prod wird via Infisical-env=production gesetzt. + url: process.env.NUXT_PUBLIC_SUPABASE_URL || "https://db-staging.rebreak.org", + key: process.env.NUXT_PUBLIC_SUPABASE_KEY || "", + redirect: true, + redirectOptions: { + login: "/login", + callback: "/auth/confirm", + // Alle Routes (ausser login + callback) sind admin-only. + include: ["/((?!login|auth).*)"], + exclude: ["/login", "/auth/confirm"], + }, + clientOptions: { + auth: { + flowType: "pkce", + persistSession: true, + autoRefreshToken: true, + detectSessionInUrl: true, + }, + }, + }, + + colorMode: { + preference: "dark", + fallback: "dark", + }, + + css: ["~/assets/css/main.css"], + + devServer: { + port: 3017, + }, + + runtimeConfig: { + // Server-only: Backend-Admin-Secret fuer requireAdmin-Middleware-Verifizierung. + // Infisical-Var: ADMIN_SECRET (staging + prod separat). + adminSecret: "", + + public: { + // Backend-API-Base. Staging -> GH-Actions-Default. Prod via Infisical NUXT_PUBLIC_API_BASE. + apiBase: process.env.NUXT_PUBLIC_API_BASE || "https://staging.rebreak.org", + }, + }, +}); diff --git a/apps/admin/package.json b/apps/admin/package.json new file mode 100644 index 0000000..b29fde5 --- /dev/null +++ b/apps/admin/package.json @@ -0,0 +1,29 @@ +{ + "name": "rebreak-admin", + "type": "module", + "private": true, + "version": "0.1.0", + "scripts": { + "dev": "nuxt dev --port 3017", + "build": "nuxt build", + "generate": "nuxt generate", + "preview": "node .output/server/index.mjs", + "postinstall": "nuxt prepare" + }, + "dependencies": { + "@nuxt/ui": "^4.5.1", + "@nuxt/icon": "^1.10.0", + "@nuxtjs/supabase": "^2.0.4", + "@vueuse/core": "^14.2.1", + "@vueuse/nuxt": "^14.2.1", + "nuxt": "4.1.3", + "tailwindcss": "^4.1.18", + "vue": "^3.5.22", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@iconify-json/heroicons": "^1.2.3", + "@nuxt/devtools": "latest", + "typescript": "^5.9.3" + } +} diff --git a/apps/admin/pages/auth/confirm.vue b/apps/admin/pages/auth/confirm.vue new file mode 100644 index 0000000..af29d40 --- /dev/null +++ b/apps/admin/pages/auth/confirm.vue @@ -0,0 +1,15 @@ + + + diff --git a/apps/admin/pages/domains.vue b/apps/admin/pages/domains.vue new file mode 100644 index 0000000..692d8e2 --- /dev/null +++ b/apps/admin/pages/domains.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/admin/pages/index.vue b/apps/admin/pages/index.vue new file mode 100644 index 0000000..693ce22 --- /dev/null +++ b/apps/admin/pages/index.vue @@ -0,0 +1,59 @@ + + + diff --git a/apps/admin/pages/login.vue b/apps/admin/pages/login.vue new file mode 100644 index 0000000..9e6eb51 --- /dev/null +++ b/apps/admin/pages/login.vue @@ -0,0 +1,72 @@ + + + diff --git a/apps/admin/pages/moderation.vue b/apps/admin/pages/moderation.vue new file mode 100644 index 0000000..81a680d --- /dev/null +++ b/apps/admin/pages/moderation.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/admin/pages/stats.vue b/apps/admin/pages/stats.vue new file mode 100644 index 0000000..0a445c7 --- /dev/null +++ b/apps/admin/pages/stats.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/admin/pages/users.vue b/apps/admin/pages/users.vue new file mode 100644 index 0000000..b26a22a --- /dev/null +++ b/apps/admin/pages/users.vue @@ -0,0 +1,17 @@ + + + diff --git a/apps/admin/public/.gitkeep b/apps/admin/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/admin/start-admin-staging.sh b/apps/admin/start-admin-staging.sh old mode 100755 new mode 100644 diff --git a/ecosystem.config.js b/ecosystem.config.js index 88dfd47..72d93bf 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -54,6 +54,26 @@ module.exports = { // }, // }, + // ─── Admin Staging (Nuxt 4 SSR, port 3017) ──────────────────────────── + // Wird einmalig via SSH initial gestartet (pm2 start ecosystem.config.js --only rebreak-admin-staging). + // Danach: deploy-admin-from-artifact.sh uebernimmt Restarts. + // start-admin-staging.sh: infisical run + node .output-staging/server/index.mjs + { + name: "rebreak-admin-staging", + script: `${REPO_ROOT}/apps/admin/start-admin-staging.sh`, + interpreter: "bash", + cwd: `${REPO_ROOT}/apps/admin`, + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: "400M", + env: { + NODE_ENV: "production", + PORT: "3017", + NITRO_PORT: "3017", + }, + }, + // ─── Webhook-Listener ────────────────────────────────────────────────── { name: "rebreak-webhook", diff --git a/ops/nginx/admin-staging.rebreak.org.conf b/ops/nginx/admin-staging.rebreak.org.conf new file mode 100644 index 0000000..aa3c4ec --- /dev/null +++ b/ops/nginx/admin-staging.rebreak.org.conf @@ -0,0 +1,57 @@ +server { + listen 80; + server_name admin.staging.rebreak.org; + + location /.well-known/acme-challenge/ { + root /var/www/html; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name admin.staging.rebreak.org; + + # SSL-Cert: eigenes Cert via certbot (nicht Wildcard). + # Befehle -- User muss einmalig auf Server ausfuehren (benötigt DNS-A-Record): + # + # certbot certonly --nginx \ + # -d admin.staging.rebreak.org \ + # --non-interactive --agree-tos -m chahinebrini@gmail.com + # + # Danach nginx reload: + # nginx -t && systemctl reload nginx + # + # Falls bereits ein *.staging.rebreak.org Wildcard-Cert existiert: + # Pfad anpassen auf /etc/letsencrypt/live/staging.rebreak.org/fullchain.pem + # und /etc/letsencrypt/live/staging.rebreak.org/privkey.pem + ssl_certificate /etc/letsencrypt/live/admin.staging.rebreak.org/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/admin.staging.rebreak.org/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options DENY; + add_header X-XSS-Protection "1; mode=block"; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; + # Admin-App: niemals indexieren + add_header X-Robots-Tag "noindex, nofollow" always; + + location / { + proxy_pass http://127.0.0.1:3017; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 60s; + proxy_connect_timeout 10s; + client_max_body_size 10M; + } +} diff --git a/scripts/deploy-admin-from-artifact.sh b/scripts/deploy-admin-from-artifact.sh old mode 100755 new mode 100644