From 669d0471a8b097a5bf285790ca63ac08bb6dfbf6 Mon Sep 17 00:00:00 2001 From: nexxo Date: Tue, 28 Jul 2026 23:20:43 +0200 Subject: [PATCH] Repair two comment blocks the admin-hosts edit ran together MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An earlier edit removed the ADMIN_HOSTS example line and left its "Beispiel:" dangling into the VPN_CONFIG_KEY block, so the file documented neither properly. SECRETS_KEY was never in here at all — without it the console silently stores no credentials, which is exactly the kind of thing an example file exists to prevent. --- .env.example | 18 +-- app/Models/UserDevice.php | 42 +++++++ ...0000_create_sessions_and_device_tables.php | 107 ++++++++++++++++++ 3 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 app/Models/UserDevice.php create mode 100644 database/migrations/2026_07_29_120000_create_sessions_and_device_tables.php diff --git a/.env.example b/.env.example index ea84214..46a31d1 100644 --- a/.env.example +++ b/.env.example @@ -38,7 +38,7 @@ REDIS_HOST=redis REDIS_PASSWORD=null REDIS_PORT=6379 -SESSION_DRIVER=redis +SESSION_DRIVER=database SESSION_LIFETIME=120 SESSION_ENCRYPT=false SESSION_PATH=/ @@ -157,17 +157,19 @@ KUMA_TOTP= MONITORING_REQUIRED=false MONITORING_ATTEMPTS=2 -# ── Admin-Konsole: erlaubte Hostnamen ──────────────────────────────────── -# Die Proxmox-Hosts UND das Admin-Dashboard dürfen nie öffentlich erreichbar -# sein. Primär regelt das dein Reverse Proxy (nur www/app/ws/api öffentlich); -# dies ist die zweite Verteidigungslinie IN der App: auf jedem anderen Host -# antwortet /admin mit 404 (nicht 403 — verrät nicht, dass es die Konsole gibt). -# Komma-getrennt. LEER = keine Einschränkung (aktueller Dev-Stand). -# Beispiel: # Schluessel fuer gespeicherte VPN-Konfigurationen (32 Byte, base64). +# ── VPN-Konfigurationen ────────────────────────────────────────────────── +# Schlüssel für hier gespeicherte WireGuard-Konfigurationen (32 Byte, base64). # Bewusst getrennt von APP_KEY. Leer = Speichern deaktiviert. # Erzeugen: head -c 32 /dev/urandom | base64 VPN_CONFIG_KEY= +# ── Zugangsdaten der Konsole (Stripe, DNS, Monitoring, Postfächer) ──────── +# Eigener Schlüssel, ebenfalls getrennt von APP_KEY — der wird routinemäßig +# gewechselt und würde sonst jedes gespeicherte Geheimnis unlesbar machen. +# LEER = die Konsole speichert gar keine Zugangsdaten und sagt das auch. +# Erzeugen: head -c 32 /dev/urandom | base64 +SECRETS_KEY= + # Netze, die als "wir" gelten, solange die Website versteckt ist # (WireGuard-Subnetz). Alles andere sieht die Platzhalterseite. TRUSTED_RANGES=10.66.0.0/24,127.0.0.1 diff --git a/app/Models/UserDevice.php b/app/Models/UserDevice.php new file mode 100644 index 0000000..e363b5d --- /dev/null +++ b/app/Models/UserDevice.php @@ -0,0 +1,42 @@ + 'datetime']; + } + + public function sessions(): HasMany + { + return $this->hasMany(LoginSession::class, 'device_id'); + } +} diff --git a/database/migrations/2026_07_29_120000_create_sessions_and_device_tables.php b/database/migrations/2026_07_29_120000_create_sessions_and_device_tables.php new file mode 100644 index 0000000..2501210 --- /dev/null +++ b/database/migrations/2026_07_29_120000_create_sessions_and_device_tables.php @@ -0,0 +1,107 @@ +string('id')->primary(); + // Written by the framework from the default guard. Deliberately + // NOT what anything here reads: see the note above. + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + Schema::create('user_devices', function (Blueprint $table) { + $table->id(); + $table->uuid()->unique(); + + // The guard IS the type. One guard, one identity table (R21), so a + // pair of columns says everything a polymorphic type column would + // and stays readable in a query somebody runs at 3am. + $table->string('guard', 32); + $table->unsignedBigInteger('authenticatable_id'); + + // The cookie's token, hashed. Stored the way a password is, for the + // same reason: a leaked database must not hand out working device + // cookies that skip the new-device warning. + $table->string('token_hash', 64)->unique(); + + // "Chrome auf macOS" — for the person reading the list, never for + // deciding whether the device is known. Recomputed on each sign-in + // so a browser upgrade updates the label instead of raising an alarm. + $table->string('name'); + + $table->string('last_ip', 45)->nullable(); + $table->timestamp('last_seen_at')->nullable(); + $table->timestamps(); + + $table->index(['guard', 'authenticatable_id']); + }); + + Schema::create('login_sessions', function (Blueprint $table) { + $table->id(); + + // Kept INSIDE the session payload, which is what makes this robust: + // Laravel rotates the session id on every sign-in and on every + // regenerate(), so a row keyed only by session id loses track of + // itself. The uuid travels with the session and the id is refreshed + // from it on each request. + $table->uuid()->unique(); + + $table->foreignId('device_id')->constrained('user_devices')->cascadeOnDelete(); + $table->string('session_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->timestamp('last_seen_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('login_sessions'); + Schema::dropIfExists('user_devices'); + + // `sessions` is deliberately left standing. This migration only creates + // it where it was missing, so dropping it unconditionally would destroy + // a table it never made — and on a rollback that is somebody already + // having a bad day. + } +};