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>
This commit is contained in:
parent
e12da5385c
commit
d3dfa74cf8
120
.github/workflows/deploy-admin-staging.yml
vendored
Normal file
120
.github/workflows/deploy-admin-staging.yml
vendored
Normal file
@ -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
|
||||
258
apps/admin/README.md
Normal file
258
apps/admin/README.md
Normal file
@ -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 <token>)
|
||||
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)
|
||||
7
apps/admin/app.vue
Normal file
7
apps/admin/app.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<UApp>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</UApp>
|
||||
</template>
|
||||
2
apps/admin/assets/css/main.css
Normal file
2
apps/admin/assets/css/main.css
Normal file
@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
66
apps/admin/composables/useAdminAuth.ts
Normal file
66
apps/admin/composables/useAdminAuth.ts
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
56
apps/admin/layouts/default.vue
Normal file
56
apps/admin/layouts/default.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-950 text-gray-100">
|
||||
<!-- Topbar -->
|
||||
<header class="border-b border-gray-800 bg-gray-900 px-6 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold tracking-wide text-gray-400 uppercase">rebreak</span>
|
||||
<span class="text-gray-600">/</span>
|
||||
<span class="text-sm font-medium text-white">Admin</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-xs text-gray-500">{{ adminEmail }}</span>
|
||||
<UButton
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
label="Logout"
|
||||
@click="logout"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Sidebar + Content -->
|
||||
<div class="flex">
|
||||
<aside class="w-56 min-h-[calc(100vh-49px)] border-r border-gray-800 bg-gray-900 py-4">
|
||||
<nav class="flex flex-col gap-1 px-3">
|
||||
<NuxtLink
|
||||
v-for="item in nav"
|
||||
:key="item.to"
|
||||
:to="item.to"
|
||||
class="flex items-center gap-2 rounded px-3 py-2 text-sm text-gray-400 hover:bg-gray-800 hover:text-white transition-colors"
|
||||
active-class="bg-gray-800 text-white"
|
||||
>
|
||||
<UIcon :name="item.icon" class="h-4 w-4 shrink-0" />
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 p-6">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { adminEmail, logout } = useAdminAuth()
|
||||
|
||||
const nav = [
|
||||
{ to: "/", label: "Dashboard", icon: "heroicons:home" },
|
||||
{ to: "/domains", label: "Domain-Approval", icon: "heroicons:globe-alt" },
|
||||
{ to: "/users", label: "User-Management", icon: "heroicons:users" },
|
||||
{ to: "/stats", label: "Statistiken", icon: "heroicons:chart-bar" },
|
||||
{ to: "/moderation", label: "Moderation", icon: "heroicons:shield-check" },
|
||||
]
|
||||
</script>
|
||||
22
apps/admin/middleware/admin-auth.ts
Normal file
22
apps/admin/middleware/admin-auth.ts
Normal file
@ -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.
|
||||
})
|
||||
87
apps/admin/nuxt.config.ts
Normal file
87
apps/admin/nuxt.config.ts
Normal file
@ -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",
|
||||
},
|
||||
},
|
||||
});
|
||||
29
apps/admin/package.json
Normal file
29
apps/admin/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
15
apps/admin/pages/auth/confirm.vue
Normal file
15
apps/admin/pages/auth/confirm.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-950 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-400">Auth-Callback wird verarbeitet...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Supabase-PKCE-Callback-Route. @nuxtjs/supabase verarbeitet den Token automatisch.
|
||||
// Nach Verarbeitung wird auf / weitergeleitet.
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
})
|
||||
</script>
|
||||
14
apps/admin/pages/domains.vue
Normal file
14
apps/admin/pages/domains.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-white mb-1">Domain-Approval</h1>
|
||||
<p class="text-sm text-gray-500 mb-8">Ausstehende Blocker-Domain-Anfragen genehmigen oder ablehnen.</p>
|
||||
<div class="rounded-lg border border-dashed border-gray-700 bg-gray-900 p-12 text-center">
|
||||
<UIcon name="heroicons:globe-alt" class="h-8 w-8 text-gray-600 mx-auto mb-3" />
|
||||
<p class="text-sm text-gray-500">Phase 2 -- Backend-API /api/admin/domains pending</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ middleware: "admin-auth" })
|
||||
</script>
|
||||
59
apps/admin/pages/index.vue
Normal file
59
apps/admin/pages/index.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-white mb-1">Dashboard</h1>
|
||||
<p class="text-sm text-gray-500 mb-8">rebreak Admin -- internes Verwaltungspanel</p>
|
||||
|
||||
<!-- Stat-Cards (Placeholder bis Phase 2 echte API-Calls hat) -->
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4 mb-8">
|
||||
<div
|
||||
v-for="card in statCards"
|
||||
:key="card.label"
|
||||
class="rounded-lg border border-gray-800 bg-gray-900 p-4"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<UIcon :name="card.icon" class="h-4 w-4 text-gray-500" />
|
||||
<span class="text-xs text-gray-500">{{ card.label }}</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-white">{{ card.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 2 Preview -->
|
||||
<div class="rounded-lg border border-gray-800 bg-gray-900 p-6">
|
||||
<h2 class="text-sm font-medium text-gray-400 mb-3">Phase 2 -- ausstehende Features</h2>
|
||||
<ul class="space-y-2 text-sm text-gray-500">
|
||||
<li class="flex items-center gap-2">
|
||||
<UIcon name="heroicons:clock" class="h-4 w-4 text-gray-600" />
|
||||
Domain-Approval-Queue (wartende Anfragen)
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<UIcon name="heroicons:clock" class="h-4 w-4 text-gray-600" />
|
||||
User-Liste mit Plan-Status + letztem Login
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<UIcon name="heroicons:clock" class="h-4 w-4 text-gray-600" />
|
||||
SOS-Session-Statistiken (aggregiert, anonym)
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<UIcon name="heroicons:clock" class="h-4 w-4 text-gray-600" />
|
||||
Content-Moderation-Queue (gemeldete Nachrichten)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Auth-Guard via Nuxt-Middleware (siehe middleware/admin-auth.ts)
|
||||
definePageMeta({
|
||||
middleware: "admin-auth",
|
||||
})
|
||||
|
||||
// Placeholder-Werte -- Phase 2 ersetzt mit echten API-Calls gegen backend /api/admin/*
|
||||
const statCards = [
|
||||
{ label: "Aktive User (30d)", value: "—", icon: "heroicons:users" },
|
||||
{ label: "SOS-Sessions heute", value: "—", icon: "heroicons:chat-bubble-left-ellipsis" },
|
||||
{ label: "Domains pending", value: "—", icon: "heroicons:globe-alt" },
|
||||
{ label: "Free / Pro / Legend", value: "—", icon: "heroicons:star" },
|
||||
]
|
||||
</script>
|
||||
72
apps/admin/pages/login.vue
Normal file
72
apps/admin/pages/login.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<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>
|
||||
14
apps/admin/pages/moderation.vue
Normal file
14
apps/admin/pages/moderation.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-white mb-1">Moderation</h1>
|
||||
<p class="text-sm text-gray-500 mb-8">Gemeldete Inhalte pruefen und behandeln.</p>
|
||||
<div class="rounded-lg border border-dashed border-gray-700 bg-gray-900 p-12 text-center">
|
||||
<UIcon name="heroicons:shield-check" class="h-8 w-8 text-gray-600 mx-auto mb-3" />
|
||||
<p class="text-sm text-gray-500">Phase 2 -- Backend-API /api/admin/moderation pending</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ middleware: "admin-auth" })
|
||||
</script>
|
||||
14
apps/admin/pages/stats.vue
Normal file
14
apps/admin/pages/stats.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-white mb-1">Statistiken</h1>
|
||||
<p class="text-sm text-gray-500 mb-8">Aggregierte, anonyme Nutzungsmetriken.</p>
|
||||
<div class="rounded-lg border border-dashed border-gray-700 bg-gray-900 p-12 text-center">
|
||||
<UIcon name="heroicons:chart-bar" class="h-8 w-8 text-gray-600 mx-auto mb-3" />
|
||||
<p class="text-sm text-gray-500">Phase 2 -- Backend-API /api/admin/stats pending</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ middleware: "admin-auth" })
|
||||
</script>
|
||||
17
apps/admin/pages/users.vue
Normal file
17
apps/admin/pages/users.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-white mb-1">User-Management</h1>
|
||||
<p class="text-sm text-gray-500 mb-8">
|
||||
User-Liste, Plan-Status, letzte Aktivitaet. Anonymisiert -- keine echten Namen, nur Nicknames.
|
||||
</p>
|
||||
<div class="rounded-lg border border-dashed border-gray-700 bg-gray-900 p-12 text-center">
|
||||
<UIcon name="heroicons:users" class="h-8 w-8 text-gray-600 mx-auto mb-3" />
|
||||
<p class="text-sm text-gray-500">Phase 2 -- Backend-API /api/admin/users pending</p>
|
||||
<p class="text-xs text-gray-600 mt-2">DSGVO: Nur Nickname sichtbar, niemals E-Mail oder Name</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ middleware: "admin-auth" })
|
||||
</script>
|
||||
0
apps/admin/public/.gitkeep
Normal file
0
apps/admin/public/.gitkeep
Normal file
0
apps/admin/start-admin-staging.sh
Executable file → Normal file
0
apps/admin/start-admin-staging.sh
Executable file → Normal file
@ -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",
|
||||
|
||||
57
ops/nginx/admin-staging.rebreak.org.conf
Normal file
57
ops/nginx/admin-staging.rebreak.org.conf
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
0
scripts/deploy-admin-from-artifact.sh
Executable file → Normal file
0
scripts/deploy-admin-from-artifact.sh
Executable file → Normal file
Loading…
x
Reference in New Issue
Block a user