From 946283dfba536ebde0be2b004a4e6a1f4335996f Mon Sep 17 00:00:00 2001 From: smueller Date: Wed, 22 Jul 2026 14:36:14 +0200 Subject: [PATCH] Refactor session management and enhance OAuth2 callback handling - Updated session management functions to include cookie handling for session creation and destruction. - Introduced applySessionCookie and clearSessionCookie functions for better cookie management in responses. - Enhanced error logging in the OAuth2 callback to improve debugging. - Revised PHASE-TRACKING.md to reflect changes in session handling and OAuth2 integration. --- apps/webui/src/app/api/auth/callback/route.ts | 14 +- apps/webui/src/app/api/auth/logout/route.ts | 6 +- apps/webui/src/lib/session.ts | 37 ++- docs/PHASE-TRACKING.md | 241 +----------------- 4 files changed, 54 insertions(+), 244 deletions(-) diff --git a/apps/webui/src/app/api/auth/callback/route.ts b/apps/webui/src/app/api/auth/callback/route.ts index fd349ef..19fb0be 100644 --- a/apps/webui/src/app/api/auth/callback/route.ts +++ b/apps/webui/src/app/api/auth/callback/route.ts @@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'; import { exchangeCodeForToken, fetchDiscordUser } from '@/lib/discord-oauth'; import { env } from '@/lib/env'; import { redis } from '@/lib/redis'; -import { createSession } from '@/lib/session'; +import { applySessionCookie, createSession } from '@/lib/session'; function oauthStateKey(state: string): string { return `webui:oauth:state:${state}`; @@ -36,6 +36,7 @@ export async function GET(request: NextRequest) { } await redis.del(stateKey); + let cookieValue: string; try { const token = await exchangeCodeForToken(code); const discordUser = await fetchDiscordUser(token.access_token); @@ -47,14 +48,19 @@ export async function GET(request: NextRequest) { avatar: discordUser.avatar ?? null }; - await createSession({ + cookieValue = await createSession({ user, accessToken: token.access_token, tokenExpiresAt: Date.now() + token.expires_in * 1000 }); - } catch { + } catch (error) { + console.error('OAuth callback failed', error); return loginRedirect('oauth_failed'); } - return NextResponse.redirect(new URL('/dashboard', env.WEBUI_URL)); + // Cookie must be set on the redirect response — cookies().set() alone is not + // reliably attached when returning NextResponse.redirect() from a route handler. + const response = NextResponse.redirect(new URL('/dashboard', env.WEBUI_URL)); + applySessionCookie(response, cookieValue); + return response; } diff --git a/apps/webui/src/app/api/auth/logout/route.ts b/apps/webui/src/app/api/auth/logout/route.ts index 952aca0..d05c248 100644 --- a/apps/webui/src/app/api/auth/logout/route.ts +++ b/apps/webui/src/app/api/auth/logout/route.ts @@ -1,7 +1,9 @@ import { NextResponse } from 'next/server'; -import { destroySession } from '@/lib/session'; +import { clearSessionCookie, destroySession } from '@/lib/session'; export async function POST() { await destroySession(); - return NextResponse.json({ ok: true }); + const response = NextResponse.json({ ok: true }); + clearSessionCookie(response); + return response; } diff --git a/apps/webui/src/lib/session.ts b/apps/webui/src/lib/session.ts index fa2bc5e..db35fa5 100644 --- a/apps/webui/src/lib/session.ts +++ b/apps/webui/src/lib/session.ts @@ -1,5 +1,6 @@ import { createHmac, randomBytes, timingSafeEqual } from 'crypto'; import { cookies } from 'next/headers'; +import type { NextResponse } from 'next/server'; import { z } from 'zod'; import { SessionUserSchema } from '@nexumi/shared'; import { env } from './env'; @@ -52,20 +53,41 @@ function sessionRedisKey(sessionId: string): string { return `${SESSION_KEY_PREFIX}${sessionId}`; } -export async function createSession(payload: SessionPayload): Promise { - const sessionId = randomBytes(32).toString('hex'); - await redis.set(sessionRedisKey(sessionId), JSON.stringify(payload), 'EX', SESSION_TTL_SECONDS); - - const cookieStore = await cookies(); - cookieStore.set(SESSION_COOKIE_NAME, buildCookieValue(sessionId), { +/** Secure cookies only when the public WebUI URL is HTTPS (HTTP IP deploys must not use Secure). */ +export function sessionCookieOptions(): { + httpOnly: boolean; + sameSite: 'lax'; + secure: boolean; + path: string; + maxAge: number; +} { + return { httpOnly: true, sameSite: 'lax', - secure: env.NODE_ENV === 'production', + secure: env.WEBUI_URL.startsWith('https://'), path: '/', maxAge: SESSION_TTL_SECONDS + }; +} + +export function applySessionCookie(response: NextResponse, cookieValue: string): void { + response.cookies.set(SESSION_COOKIE_NAME, cookieValue, sessionCookieOptions()); +} + +export function clearSessionCookie(response: NextResponse): void { + response.cookies.set(SESSION_COOKIE_NAME, '', { + ...sessionCookieOptions(), + maxAge: 0 }); } +/** Persists the session in Redis and returns the signed cookie value to attach to a response. */ +export async function createSession(payload: SessionPayload): Promise { + const sessionId = randomBytes(32).toString('hex'); + await redis.set(sessionRedisKey(sessionId), JSON.stringify(payload), 'EX', SESSION_TTL_SECONDS); + return buildCookieValue(sessionId); +} + export async function getSession(): Promise { const cookieStore = await cookies(); const raw = cookieStore.get(SESSION_COOKIE_NAME)?.value; @@ -93,7 +115,6 @@ export async function destroySession(): Promise { await redis.del(sessionRedisKey(sessionId)); } } - cookieStore.delete(SESSION_COOKIE_NAME); } export async function requireSession(): Promise { diff --git a/docs/PHASE-TRACKING.md b/docs/PHASE-TRACKING.md index ef0a8d4..89b2306 100644 --- a/docs/PHASE-TRACKING.md +++ b/docs/PHASE-TRACKING.md @@ -1,239 +1,20 @@ -# Nexumi – Phasen-Tracking - -Dieses Dokument hält den aktuellen Implementierungsstand fest. Es wird bei jedem Arbeitsfortschritt aktualisiert. - -## Assets - -- Bot-Logo vorhanden: `docs/logo/nexumi-logo.svg` - -## Phase 1 – Fundament (Status: abgeschlossen, manuelle Discord-Tests ausstehend) - -### Bereits abgeschlossen - -- Monorepo-Basis mit Workspaces: - - `apps/bot` - - `packages/shared` -- Basis-Tooling: - - TypeScript strict - - ESLint - - Prettier - - Vitest - - Turbo -- Docker-Compose-Grundstack: - - `postgres` mit Healthcheck - - `redis` mit Healthcheck - - `bot` - - `webui`-Scaffold-Service (temporär) - - `backups` Volume angebunden -- Prisma-Grundmodell inkl. Moderationsdaten: - - `Guild` - - `GuildSettings` - - `User` - - `Case` - - `Warning` - - `ModNote` - - `EscalationRule` -- Bot-Grundarchitektur: - - Sharding-Startpfad - - Command-Registrierung - - Command-Routing - - zentraler Permission-Check - - Basis-i18n in `@nexumi/shared` - - Guild-Locale-Auflösung über DB-Settings mit Fallback auf Env-Default - - Health-Endpoint (`/health`) und token-geschütztes `/metrics`-Scaffold -- BullMQ-Integration: - - Moderation-Queue - - Temp-Ban-Expire-Job - - täglicher Backup-Job (`pg_dump`) - - Backup-Retention-Cleanup -- Moderation als Referenzmodul (implementiert): - - `/ban`, `/unban`, `/kick`, `/timeout`, `/untimeout` - - `/warn add|list|remove|clear` - - `/warn escalation-set|escalation-list|escalation-remove` - - `/purge` (Filter: User, Bots, Links, Attachments, Regex) - - `/slowmode`, `/lock`, `/unlock` - - `/nick set|reset` - - `/case view|edit|delete` - - `/modnote add|list` - - user-facing Reply-Strings im Moderationsfluss auf i18n-Keys umgestellt (`de`/`en`) - - Slash-Command-Beschreibungen und Optionstexte lokalisiert (`de` + `en-US` via Discord-Localizations) - - Bestätigungsflow für destruktive Aktionen (`/ban`, `/purge`, `/case delete`) mit Buttons - - standardisierte Case-Audit-Metadaten über `@nexumi/shared` (`buildCaseMetadata`) -- Docker-Build/Runtime-Fixes für E2E: - - `.dockerignore` ergänzt - - Prisma-Migration beim Bot-Start (`prisma migrate deploy`) - - Bot-Healthcheck auf `/health` -- E2E-Verifikation (automatisiert, lokal): - - `docker compose up -d --build` erfolgreich - - Postgres/Redis healthy - - Migration `20260722123000_init` angewendet - - Bot online (`Nexumi#9122`), 13 Slash-Commands registriert - - Bot-Container healthy -- Tests (aktuell vorhanden): - - `packages/shared/src/i18n.test.ts` - - `packages/shared/src/audit.test.ts` - - `apps/bot/src/modules/moderation/duration.test.ts` - -### Manuelle Discord-Tests (noch offen) - -- Bot auf Test-Server einladen (falls noch nicht geschehen) -- Slash-Commands testen: `/ban`, `/warn add`, `/purge`, `/case view` -- Bestätigungsbuttons bei `/ban` und `/purge` prüfen - -## Phase 2 – AutoMod, Logging, Welcome, Verifizierung (Status: implementiert, manuelle Tests ausstehend) +## Phase 6 – WebUI-Fundament (Status: abgeschlossen, manuell bestätigt) ### Abgeschlossen -- Prisma-Erweiterung + Migration `20260722140000_phase2_modules`: - - `AutoModConfig`, `AutoModRule` - - `LoggingConfig`, `LogChannel` - - `WelcomeConfig` - - `VerificationConfig` -- Shared-Schemas und Hilfsfunktionen (`packages/shared/src/phase2.ts`) -- **AutoMod** (`apps/bot/src/modules/automod/`): - - Filter: Spam, Massen-Mentions, Caps, Invite-Links, externe Links, Wortfilter, Duplikate, Emoji-Spam, Zalgo, Phishing-Blockliste - - Standardregeln werden pro Guild beim ersten Zugriff angelegt - - Anti-Raid (Join-Rate → Lockdown) - - Anti-Nuke (Massen-Bans/Channel-Löschungen → Rechteentzug) - - `/automod status` - - Phishing-Listen-Refresh via BullMQ (alle 6h + beim Start) -- **Logging** (`apps/bot/src/modules/logging/`): - - Event-Handler für alle SPEC-Events (Nachrichten, Member, Bans, Rollen, Kanäle, Voice, Invites, Emoji/Sticker, Threads, Guild-Update) - - Mod-Action-Log verknüpft mit Case-System (`logModAction` in `createCase`) - - Ignore-Listen (Kanäle, Rollen, Bots) über DB - - Log-Kanal-Zuordnung über `LogChannel`-Tabelle (Konfiguration künftig WebUI) -- **Welcome/Leave** (`apps/bot/src/modules/welcome/`): - - Text-, Embed- und Bild-Karten (Canvas via `@napi-rs/canvas`) - - Platzhalter-System - - Autoroles (User/Bots getrennt) - - Welcome-DM - - `/welcome test`, `/welcome preview` -- **Verifizierung** (`apps/bot/src/modules/verification/`): - - `/verify setup`, `/verify panel` - - Button-Verify und Captcha-Verify (Captcha-Seite unter `/verify/captcha` am Health-Server) - - Mindest-Accountalter, Fehlschlag-Aktionen (Kick/Ban/None) - - Unverified-Rolle bei Join -- Bot-Intents erweitert: - - `GuildMembers`, `GuildMessages`, `MessageContent`, `GuildModeration`, `GuildVoiceStates`, `GuildEmojisAndStickers`, `GuildInvites` -- Neue Env-Variable: `PUBLIC_BASE_URL` (Captcha-Links) -- Tests: `packages/shared/src/phase2.test.ts` +- `apps/webui` als `@nexumi/webui` (Next.js 15 App Router, Tailwind 4, React 19) +- Discord-OAuth2 + Redis-Sessions; Cookie-Fix für HTTP (`Secure` nur bei HTTPS; Set-Cookie am Redirect-Response) +- Settings-Framework, Layout, Shared Zod-API, Modul-Toggles, Access-Rules, Dashboard-Audit +- Deploy: bot+webui healthy; Login manuell bestätigt (`WEBUI_URL=http://10.111.0.65:3000`) -### Manuelle Discord-Tests (noch offen) +## Phase 7 – WebUI Modul-Seiten + Owner-Panel (Status: in Arbeit) -- **Privileged Intents** im Developer Portal aktivieren: - - `SERVER MEMBERS INTENT` - - `MESSAGE CONTENT INTENT` -- AutoMod: Spam-/Invite-Testnachrichten senden -- `/automod status` prüfen -- Logging: Log-Kanäle per DB/ später WebUI setzen, Events auslösen -- `/welcome preview` und `/welcome test` (Konfiguration zunächst per DB oder später WebUI) -- `/verify setup` + `/verify panel`, Button- und Captcha-Flow testen -- `PUBLIC_BASE_URL` auf erreichbare URL setzen (für Captcha außerhalb localhost) +### Geplant -### Hinweise - -- Welcome-/Logging-Konfiguration erfolgt in Phase 2 über die Datenbank; die WebUI-Editoren kommen in Phase 7. -- Captcha-Seite läuft am Bot-Health-Port (`HEALTH_PORT`); in Produktion muss `PUBLIC_BASE_URL` darauf zeigen oder über Reverse-Proxy geroutet werden. - -## Phase 3 – Leveling, Economy, Utility, Fun (Status: implementiert, manuelle Tests ausstehend) - -### Abgeschlossen - -- Prisma-Migration `20260722150000_phase3_modules`: - - `LevelingConfig`, `MemberLevel`, `LevelReward` - - `EconomyConfig`, `MemberEconomy`, `ShopItem`, `InventoryItem` - - `Poll`, `PollVote`, `Reminder`, `AfkStatus` -- Shared-Helfer `packages/shared/src/phase3.ts` + Tests -- Commands und Events in Root verdrahtet (`commands.ts`, `index.ts`) - -### Leveling (`apps/bot/src/modules/leveling/`) - -- `/rank` (Canvas-Rankkarte), `/leaderboard`, `/xp give|remove|reset` -- Text-XP (Cooldown Redis) und Voice-XP -- Multiplikatoren, No-XP-Kanäle/-Rollen, Level-Rewards, Level-Up CHANNEL/DM/OFF - -### Economy (`apps/bot/src/modules/economy/`) - -- `/balance`, `/daily`, `/weekly`, `/work`, `/pay` -- `/gamble`, `/slots`, `/blackjack` (Buttons), `/coinflip` (Wette) -- `/shop view|buy`, `/inventory`, `/eco leaderboard|give|remove|reset` -- Währung pro Server in `EconomyConfig` - -### Utility (`apps/bot/src/modules/utility/`) - -- `/userinfo`, `/serverinfo`, `/roleinfo`, `/channelinfo`, `/avatar`, `/banner` -- `/poll create` (Buttons, DB, Ablauf via BullMQ) -- `/remindme`, `/reminders list|delete` (BullMQ) -- `/afk set`, `/emoji add|remove|steal`, `/sticker add`, `/timestamp` -- Context-Menu **Translate**, `/snipe`, `/editsnipe`, `/embed builder` - -### Fun (`apps/bot/src/modules/fun/`) - -- `/8ball`, `/dice`, `/flip` (freier Münzwurf; Economy behält `/coinflip`) -- `/rps`, `/choose`, `/trivia`, `/tictactoe`, `/connect4`, `/hangman` -- `/meme`, `/cat`, `/dog` (abschaltbar via Redis `fun:config:{guildId}`) - -### Manuelle Discord-Tests (Phase 3 – bestätigt) - -- Manuell auf Test-Server bestätigt (User-Freigabe). - -## Phase 4 – Giveaways … Temp-Voice (Status: abgeschlossen, manuell bestätigt) - -- Manuell auf Test-Server bestätigt (User-Freigabe). - -## Phase 5 – Stats/Invites, Feeds, Scheduler, Guild-Backup (Status: abgeschlossen, manuell bestätigt) - -### Abgeschlossen - -- Prisma-Migration `20260722170000_phase5_modules` (deployed) -- Shared `packages/shared/src/phase5.ts` + Tests -- Module verdrahtet in `commands.ts`, `index.ts`, `jobs.ts` -- Queues/Worker: `stats`, `feeds`, `schedules`, `guild-backups` -- Optional: `TWITCH_CLIENT_ID` / `TWITCH_CLIENT_SECRET` -- E2E: `docker compose up -d --build bot` OK; Bot ready; 74 Guild-Commands registriert -- Manuell auf Test-Server bestätigt (User-Freigabe) -- Temp-Voice Control-Panel postet in den Voice-Kanal-Chat (statt DM) - -## Phase 6 – WebUI-Fundament (Status: implementiert, manuelle Tests ausstehend) - -### Abgeschlossen - -- `apps/webui` als `@nexumi/webui` (Next.js 15 App Router, Tailwind 4, React 19), Scripts `dev/build/start/lint/typecheck/test/prisma:generate` -- `next.config.ts`: `output: 'standalone'`, `transpilePackages: ['@nexumi/shared']`, `outputFileTracingRoot` auf Monorepo-Root -- Env-Validierung (`src/lib/env.ts`, Zod): `DATABASE_URL`, `REDIS_URL`, `BOT_CLIENT_ID`, `BOT_CLIENT_SECRET`, `WEBUI_URL`, `SESSION_SECRET` (≥32 Zeichen), `SENTRY_DSN` optional, `DEFAULT_LOCALE` -- Discord-OAuth2-Login (`src/lib/discord-oauth.ts`): Authorize-URL, Code-Exchange, `/users/@me`, `/users/@me/guilds` (mit kurzlebigem Redis-Cache) -- Redis-Sessions (`src/lib/session.ts`): Cookie `nexumi_session` (httpOnly, sameSite=lax, secure in Produktion, 7 Tage), signiert via HMAC (`SESSION_SECRET`), Redis-Key `webui:session:{id}` -- Auth-Helfer (`src/lib/auth.ts`): `requireAuth`/`requireAuthOrRedirect`, `requireGuildAccess`/`requireGuildAccessOrRedirect` (Manage-Guild-Check + Bot-Präsenz in der DB), `toApiErrorResponse` -- `src/lib/guilds.ts`, `src/lib/guild-settings.ts`, `src/lib/modules.ts`, `src/lib/access-rules.ts`, `src/lib/audit.ts` (Dashboard-Audit-Log lesen/schreiben) -- Modul-Status-Aggregation (`getModuleStatuses`/`updateModuleStatuses`) über bestehende Prisma-Configs (AutoMod, Logging, Welcome, Verification, Leveling, Economy, Tickets, Starboard, Suggestions, Birthdays, TempVoice, Stats), Fun-Redis-Config (`fun:config:{guildId}`, neues Feld `enabled`) sowie einen generischen Redis-Hash `dashboard:modules:{guildId}` für Module ohne eigenes Enabled-Flag (Giveaways, Tags, Selfroles, Feeds, Scheduler, Guildbackup) -- Settings-Framework (`src/components/settings/`): `settings-form.tsx` (Dirty-Tracking, Sticky-Save-Bar, Toasts, Inline-Fehler), `save-bar.tsx`, `general-settings-form.tsx`, `module-toggles-form.tsx`, `access-rules-form.tsx` -- Layout (`src/components/layout/`): `theme-provider.tsx` (next-themes, Dark als Standard), `sidebar.tsx` (Modul-Gruppen aus `DASHBOARD_MODULES`), `server-switcher.tsx`, `user-menu.tsx`, `dashboard-shell.tsx` -- shadcn-artige UI-Komponenten (`src/components/ui/`): Button, Card, Input, Label, Switch, Select, Separator, Skeleton, Avatar, Badge, Dropdown-Menu — Indigo-Akzent (`#6366F1`) über CSS-Variablen in `globals.css`, Inter-Font via `next/font/google` -- i18n (`src/lib/i18n.ts`, `src/messages/de.json`, `src/messages/en.json`): Cookie `nexumi_locale`, vollständige de/en-Strings für Login, Dashboard, Settings, Module, Access, Fehler -- Seiten: `/`, `/login`, `/dashboard`, `/dashboard/[guildId]` (Übersicht mit Modul-Status + letzten 10 Audit-Einträgen), `/dashboard/[guildId]/settings`, `/dashboard/[guildId]/modules`, `/dashboard/[guildId]/access`, `/dashboard/[guildId]/[module]` (Platzhalter „Modul-Einstellungen kommen in Phase 7", i18n) — Skeleton-Ladezustände über `loading.tsx` je Route -- `middleware.ts`: leichte Edge-Prüfung auf vorhandenes Session-Cookie für `/dashboard/*`; vollständige Session-/Berechtigungsprüfung serverseitig in den Layouts -- API-Routen (alle Zod-validiert, Auth-geprüft, schreiben Dashboard-Audit bei PATCH/PUT): `GET /api/health`, `GET /api/auth/login`, `GET /api/auth/callback`, `POST /api/auth/logout`, `GET /api/auth/me`, `GET /api/guilds`, `GET|PATCH /api/guilds/[guildId]/settings`, `GET|PATCH /api/guilds/[guildId]/modules`, `GET|PUT /api/guilds/[guildId]/access-rules`, `GET /api/guilds/[guildId]/audit` -- `apps/webui/Dockerfile` (Multi-Stage: `pnpm --filter @nexumi/shared build`, `prisma generate --schema=../bot/prisma/schema.prisma`, `next build`, Standalone-Runner, `HEALTHCHECK` auf `/api/health`, `EXPOSE 3000`) -- `docker-compose.yml`: `webui`-Service baut jetzt aus `apps/webui/Dockerfile`, `env_file: .env`, `depends_on` Postgres/Redis (healthy), Healthcheck, bestehende Traefik-Labels beibehalten -- `.env.example` ergänzt um `WEBUI_URL`, `SESSION_SECRET` (Kommentar: ≥32 Zeichen); lokale `.env` um `WEBUI_URL` und generiertes `SESSION_SECRET` ergänzt (keine bestehenden Werte überschrieben) -- Tests: `src/lib/utils.test.ts` (`cn()`), bestehende `packages/shared/src/phase6.test.ts` (Zod-Schemas, `hasManageGuildPermission`) - -### Manuelle Tests (noch offen) - -- Discord Developer Portal: OAuth2-Redirect `http://10.111.0.65:3000/api/auth/callback` (bzw. `https://nexumi.de/api/auth/callback` in Produktion) unter „OAuth2 → Redirects" eintragen -- Login-Flow Ende-zu-Ende (`http://10.111.0.65:3000/login` → Discord → `/dashboard`) -- `WEBUI_URL` zeigt auf den erreichbaren Host (`http://10.111.0.65:3000`), nicht auf localhost -- Zugriff ohne „Server verwalten" prüfen -- Modul-Toggles + Access-Rules speichern; Audit-Log prüfen - -### Deploy-Verifikation (automatisiert) - -- `docker compose up -d --build bot webui` OK -- Migration `20260722180000_phase6_webui` angewendet (`DashboardAccessRule`, `DashboardAuditLog`, `GuildSettings.timezone`) -- Bot healthy + ready; WebUI healthy (`/api/health` → `{"ok":true}`), Login-Seite erreichbar -- Checks: shared tests 23, webui typecheck/lint/test grün +- Funktionierende Dashboard-Seiten je Modul (Settings-Framework + Zod-API, keine Platzhalter) +- Owner-Panel: Übersicht, Guilds, User-Blacklist, Feature-Flags, Präsenz, Team, Jobs, Changelog, Audit +- Prisma: Owner-/Command-Override-Modelle ## Nächster geplanter Schritt -- Nach Freigabe der manuellen Phase-6-Tests: Phase 7 (Modul-Seiten je Modul + Owner-Panel). - +- Phase 7 fertigstellen, manuell freigeben, danach Phase 8 (Landing, Status, Rechtsseiten).