fix(tls/ui): fully automatic TLS (status-only) + uniform hardening buttons
Per operator feedback ("das soll automatisch sein, die Caddy-Config muss man nicht
sehen, nur 'SSL steht'"):
- TLS is fully automatic via Caddy (auto-issue + auto-renew + HTTP→HTTPS). The System
page now shows only the STATUS (domain + access URL) — the generated Caddy config and
the manual `caddy reload` command are gone, and the domain/email edit form is removed.
- The panel domain is an INSTALL-TIME value (APP_DOMAIN → config('clusev.domain')), so it
stays consistent with APP_URL, the Reverb (wss) endpoint and cookie security — none of
which can be re-derived at runtime (Caddy's site address + the browser's Reverb host are
fixed at install/build, and .env is never rewritten). DeploymentService is now read-only.
- Removed AppServiceProvider's runtime app.url override (it let a stale DB value shadow the
install-time URL). Migration drops the inert empty dashboard_domain/dashboard_email rows
(a real configured value is preserved as history; re-apply via install.sh — see CHANGELOG).
- Hardening checklist: all toggle buttons share one style (secondary/bordered) instead of
varying with state (R10).
Bump 0.2.0 -> 0.2.1 + CHANGELOG. Pint clean; Codex reviewed (the remaining note is a
deliberate install-time-domain tradeoff — a runtime/DB domain cannot be served by the
templated Caddy without a false "TLS active" status); R12 — system/server-show/dashboard
HTTP 200, 0 console errors, raw config + edit form gone, buttons uniform.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
v0.2.1
parent
4a4d107bfc
commit
ca96fc44db
|
|
@ -75,7 +75,8 @@ IMAGE_TAG=latest
|
|||
|
||||
# ── Reverse proxy / TLS (prod — written by install.sh) ───────────────
|
||||
# Single knob: APP_DOMAIN empty -> Caddy serves plain HTTP on APP_PORT (bare-IP).
|
||||
# APP_DOMAIN set -> Caddy gets a Let's Encrypt cert + forces HTTPS.
|
||||
# APP_DOMAIN set -> Caddy auto-issues a Let's Encrypt cert + forces HTTPS.
|
||||
# Changing the panel domain later = re-run install.sh with the new domain.
|
||||
APP_DOMAIN=
|
||||
APP_SCHEME=http
|
||||
ACME_EMAIL=
|
||||
|
|
|
|||
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -13,6 +13,27 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui
|
|||
|
||||
_Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._
|
||||
|
||||
## [0.2.1] - 2026-06-13
|
||||
|
||||
Verfeinerungen an 0.2.0: vollautomatisches TLS ohne sichtbare Konfiguration und einheitliche Buttons.
|
||||
|
||||
### Geändert
|
||||
- **TLS ist vollautomatisch.** Caddy stellt das Zertifikat aus, erneuert es und leitet
|
||||
HTTP→HTTPS um — ohne manuelle Schritte. Die System-Seite zeigt nur noch den **Status**
|
||||
(Domain, Zugriffsadresse) statt der erzeugten Caddy-Konfiguration und des
|
||||
`caddy reload`-Befehls. Die Panel-Domain ist eine **Installations-Entscheidung**
|
||||
(`APP_DOMAIN`) und bleibt damit konsistent mit URL, WebSocket (wss) und Cookie-Sicherheit;
|
||||
zum Ändern `install.sh` mit der neuen Domain erneut ausführen.
|
||||
- Härtungs-Checkliste: alle Aktions-Buttons im selben Stil (einheitlich `secondary`,
|
||||
mit Rahmen) statt zustandsabhängig mal mit, mal ohne Rahmen.
|
||||
|
||||
### Entfernt
|
||||
- Laufzeit-Override der App-URL aus der Datenbank und das dashboard-seitige Bearbeiten
|
||||
von Domain/E-Mail (konnte URL/WebSocket/Cookies nicht konsistent zur Laufzeit umstellen).
|
||||
Eine Migration entfernt die leeren Alt-Einträge; ein real konfigurierter Wert bleibt als
|
||||
Verlauf erhalten. **Upgrade-Hinweis:** eine zuvor im Dashboard gesetzte Domain bitte einmalig
|
||||
über `install.sh` setzen.
|
||||
|
||||
## [0.2.0] - 2026-06-13
|
||||
|
||||
Mehr-Betriebssystem-Verwaltung, echte Firewall- und fail2ban-Steuerung aus dem Dashboard sowie eine Tastatur-/Befehls-Bedienung.
|
||||
|
|
@ -114,7 +135,8 @@ Erste geschnittene Version — die komplette v1-Basis inklusive Server- und Pane
|
|||
- Release-/Versionierungsmodell: echte semantische Versionen + Git-Tags, Changelog
|
||||
pro Version, Kanäle `stable`/`beta` (kein nutzer-seitiges `dev`).
|
||||
|
||||
[Unreleased]: https://git.bave.dev/boban/clusev/compare/v0.2.0...HEAD
|
||||
[Unreleased]: https://git.bave.dev/boban/clusev/compare/v0.2.1...HEAD
|
||||
[0.2.1]: https://git.bave.dev/boban/clusev/compare/v0.2.0...v0.2.1
|
||||
[0.2.0]: https://git.bave.dev/boban/clusev/compare/v0.1.1...v0.2.0
|
||||
[0.1.1]: https://git.bave.dev/boban/clusev/compare/v0.1.0...v0.1.1
|
||||
[0.1.0]: https://git.bave.dev/boban/clusev/releases/tag/v0.1.0
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@
|
|||
|
||||
namespace App\Livewire\System;
|
||||
|
||||
use App\Models\AuditEvent;
|
||||
use App\Models\Setting;
|
||||
use App\Services\DeploymentService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Attributes\Title;
|
||||
|
|
@ -14,10 +12,10 @@ use Livewire\Component;
|
|||
/**
|
||||
* System settings — dashboard-configurable Domain + TLS and the release channel.
|
||||
*
|
||||
* Domain & TLS: persists the panel domain + Let's Encrypt admin email (Settings),
|
||||
* then renders the matching Caddy site block via DeploymentService for the operator
|
||||
* to review. The app NEVER reloads Caddy itself (separate container) — it only
|
||||
* stages the config and shows the reload command.
|
||||
* Domain & TLS: READ-ONLY status. The panel domain is an install-time decision
|
||||
* (APP_DOMAIN) so it stays consistent with URL/WebSocket/cookie security; Caddy then
|
||||
* issues, renews and HTTP→HTTPS-redirects fully automatically. Nothing to apply or
|
||||
* reload here — to change the domain, re-run install.sh.
|
||||
*
|
||||
* Release-Kanal: a stable|beta choice persisted as a Setting (default from
|
||||
* config('clusev.channel')), audited on change.
|
||||
|
|
@ -32,47 +30,19 @@ class Index extends Component
|
|||
'beta' => 'Vorabversionen kommender Releases zum Testen neuer Funktionen. Kann instabil sein. Entwicklungs-Builds werden Nutzern nie angeboten.',
|
||||
];
|
||||
|
||||
/** Install-time panel domain (read-only display). */
|
||||
public string $domain = '';
|
||||
|
||||
public string $email = '';
|
||||
|
||||
public string $channel = 'stable';
|
||||
|
||||
public function mount(DeploymentService $deployment): void
|
||||
{
|
||||
$this->domain = (string) ($deployment->domain() ?? '');
|
||||
$this->email = (string) ($deployment->email() ?? '');
|
||||
$channel = Setting::get('release_channel', config('clusev.channel')) ?? 'stable';
|
||||
// Clamp to a valid user channel — a stale/legacy value (e.g. 'dev') falls back to stable.
|
||||
$this->channel = array_key_exists($channel, self::CHANNELS) ? $channel : 'stable';
|
||||
}
|
||||
|
||||
/** Persist domain + email, then (re)render the Caddy site config for review. */
|
||||
public function saveDomain(DeploymentService $deployment): void
|
||||
{
|
||||
$data = $this->validate([
|
||||
'domain' => ['nullable', 'string', 'max:253', 'regex:/^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i'],
|
||||
'email' => ['nullable', 'email', 'max:255', 'required_with:domain'],
|
||||
], [
|
||||
'domain.regex' => 'Bitte eine gültige Domain angeben (z. B. panel.example.com).',
|
||||
'email.required_with' => 'Bei gesetzter Domain ist eine Admin-E-Mail für Let\'s Encrypt Pflicht.',
|
||||
]);
|
||||
|
||||
$deployment->setDomain($data['domain'] ?? null);
|
||||
$deployment->setEmail($data['email'] ?? null);
|
||||
|
||||
$this->domain = (string) ($deployment->domain() ?? '');
|
||||
$this->email = (string) ($deployment->email() ?? '');
|
||||
|
||||
$deployment->writeCaddyConfig();
|
||||
|
||||
$this->audit('system.settings_updated', $this->domain !== '' ? $this->domain : 'bare-ip');
|
||||
|
||||
$this->dispatch('notify', message: $this->domain !== ''
|
||||
? 'Domain gespeichert. Caddy-Konfiguration neu erzeugt.'
|
||||
: 'Bare-IP-Modus gespeichert. Caddy-Konfiguration neu erzeugt.');
|
||||
}
|
||||
|
||||
/** Release channel changes are audited (confirmation via the shared modal). */
|
||||
public function confirmChannel(string $channel): void
|
||||
{
|
||||
|
|
@ -108,23 +78,11 @@ class Index extends Component
|
|||
$this->channel = $channel;
|
||||
}
|
||||
|
||||
private function audit(string $action, string $target): void
|
||||
{
|
||||
AuditEvent::create([
|
||||
'user_id' => Auth::id(),
|
||||
'actor' => Auth::user()?->name ?? 'system',
|
||||
'action' => $action,
|
||||
'target' => $target,
|
||||
'ip' => request()->ip(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function render(DeploymentService $deployment)
|
||||
{
|
||||
return view('livewire.system.index', [
|
||||
'hasTls' => $this->domain !== '',
|
||||
'caddyConfig' => $deployment->renderCaddyConfig(),
|
||||
'reloadHint' => $deployment->reloadHint(),
|
||||
'panelUrl' => $deployment->panelUrl(),
|
||||
'channels' => self::CHANNELS,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Throwable;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -19,43 +16,14 @@ class AppServiceProvider extends ServiceProvider
|
|||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* Note: the panel domain / app URL is an INSTALL-TIME value (APP_URL in .env,
|
||||
* derived from APP_DOMAIN by install.sh), kept consistent with the Reverb endpoint,
|
||||
* cookie security and Caddy's auto-TLS. It is deliberately NOT overridden at runtime
|
||||
* from the database, so URL generation always matches how the panel is actually served.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->applyDashboardDomain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep Laravel's URL generation in sync with the dashboard-set panel domain
|
||||
* WITHOUT ever touching .env.
|
||||
*
|
||||
* The panel domain lives in the DATABASE (Settings: `dashboard_domain`), not in
|
||||
* .env — and the app never rewrites .env. So at runtime, if a domain is set, we
|
||||
* override config('app.url') to `https://<domain>` so that signed URLs, absolute
|
||||
* links, password-reset mails, etc. resolve against the operator-configured host.
|
||||
*
|
||||
* Guarded hard: during `artisan migrate`, fresh installs, or any moment before the
|
||||
* `settings` table exists (or the DB is unreachable), this must be a silent no-op so
|
||||
* console commands and migrations never crash. Hence the Schema::hasTable() probe
|
||||
* plus a defensive try/catch around the DB read.
|
||||
*/
|
||||
private function applyDashboardDomain(): void
|
||||
{
|
||||
try {
|
||||
if (! Schema::hasTable('settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$domain = Setting::get('dashboard_domain');
|
||||
|
||||
if ($domain === null || $domain === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
config(['app.url' => 'https://'.$domain]);
|
||||
} catch (Throwable) {
|
||||
// No DB / table yet (install, migrate, CI). Leave config('app.url') as-is —
|
||||
// never fail boot over a settings lookup.
|
||||
}
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,140 +2,34 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Setting;
|
||||
|
||||
/**
|
||||
* Renders the Caddy reverse-proxy site config for the dashboard's own domain.
|
||||
*
|
||||
* Clusev ships behind a Caddy container (see docker-compose.prod.yml). In prod the
|
||||
* proxy address + ACME email are driven by .env (SITE_ADDRESS / ACME_EMAIL) and the
|
||||
* baked docker/caddy/Caddyfile is mounted read-only. This service lets the operator
|
||||
* configure the panel domain from the dashboard: it persists domain + email as
|
||||
* Settings and renders the matching Caddy site block (auto Let's Encrypt TLS), so the
|
||||
* operator can review and drop it into Caddy's config.
|
||||
*
|
||||
* IMPORTANT: the app CANNOT restart or reload Caddy — Caddy runs in a separate
|
||||
* container. Writing the file is the most the app can do; picking it up is an infra
|
||||
* step ('caddy reload --config /config/clusev.caddy' inside the caddy container, or a
|
||||
* stack recreate). renderCaddyConfig()/writeCaddyConfig() never touch the live proxy.
|
||||
*
|
||||
* .env IS NEVER WRITTEN. This service only persists Settings (DB) and writes a
|
||||
* standalone Caddy site block to storage/app/caddy/clusev.caddy. It does not read,
|
||||
* edit, or rewrite the application's .env in any way — the panel domain is a database
|
||||
* value, so a change to it never mutates .env.
|
||||
* Read-only view of the panel's domain + TLS state. The panel domain is an
|
||||
* INSTALL-TIME decision (APP_DOMAIN → config('clusev.domain')) because it must stay
|
||||
* consistent with APP_URL, the Reverb (wss) endpoint, secure-cookie + HSTS settings,
|
||||
* and Caddy's auto-TLS — none of which can be safely re-derived at runtime. TLS itself
|
||||
* is fully automatic (Caddy issues + renews + redirects HTTP→HTTPS); the dashboard only
|
||||
* SHOWS the status. To change the domain, re-run install.sh with the new domain.
|
||||
*/
|
||||
class DeploymentService
|
||||
{
|
||||
/** Where writeCaddyConfig() drops the rendered block (app-writable, host-mountable). */
|
||||
public const CONFIG_FILENAME = 'clusev.caddy';
|
||||
|
||||
public function domain(): ?string
|
||||
{
|
||||
return Setting::get('dashboard_domain');
|
||||
$domain = config('clusev.domain');
|
||||
|
||||
return ($domain !== null && $domain !== '') ? strtolower(trim((string) $domain)) : null;
|
||||
}
|
||||
|
||||
public function email(): ?string
|
||||
{
|
||||
return Setting::get('dashboard_email');
|
||||
}
|
||||
|
||||
public function setDomain(?string $domain): void
|
||||
{
|
||||
Setting::put('dashboard_domain', $domain !== null && $domain !== '' ? strtolower(trim($domain)) : null);
|
||||
}
|
||||
|
||||
public function setEmail(?string $email): void
|
||||
{
|
||||
Setting::put('dashboard_email', $email !== null && $email !== '' ? trim($email) : null);
|
||||
}
|
||||
|
||||
/** True once a domain is configured — i.e. TLS via Let's Encrypt is in effect. */
|
||||
/** True once a domain is configured — i.e. automatic TLS is in effect. */
|
||||
public function hasTls(): bool
|
||||
{
|
||||
$domain = $this->domain();
|
||||
|
||||
return $domain !== null && $domain !== '';
|
||||
return $this->domain() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Caddy site block for the configured domain, mirroring the shape of
|
||||
* docker/caddy/Caddyfile: a global `email` block + a site address that triggers
|
||||
* automatic Let's Encrypt TLS, with the same Reverb (/app/*, /apps/*) and app
|
||||
* upstreams and the same conservative hardening header set.
|
||||
*
|
||||
* With a domain set: `https://<domain>` → auto-TLS on :443, HTTP auto-redirect.
|
||||
* Without a domain: `:80` → plain HTTP on the bare IP (no cert issued).
|
||||
*/
|
||||
public function renderCaddyConfig(): string
|
||||
/** The HTTPS URL the panel is reachable at once a domain is configured. */
|
||||
public function panelUrl(): ?string
|
||||
{
|
||||
$domain = $this->domain();
|
||||
$email = $this->email();
|
||||
|
||||
$siteAddress = $domain ? 'https://'.$domain : ':80';
|
||||
// Caddy needs a non-empty ACME email even in bare-IP mode, where no cert is
|
||||
// issued so the value is simply unused — mirror the compose default.
|
||||
$acmeEmail = $email ?: 'admin@localhost';
|
||||
|
||||
return <<<CADDY
|
||||
# Clusev reverse proxy — generated from the dashboard (System → Domain & TLS).
|
||||
# Mirrors docker/caddy/Caddyfile. Reverb rides /app/* + /apps/* on the same address.
|
||||
#
|
||||
# https://<domain> -> auto Let's Encrypt, :443, HTTP auto-redirect
|
||||
# :80 -> plain HTTP on the bare IP, no cert
|
||||
|
||||
{
|
||||
\temail {$acmeEmail}
|
||||
}
|
||||
|
||||
{$siteAddress} {
|
||||
\t@reverb path /app/* /apps/*
|
||||
\treverse_proxy @reverb reverb:8080
|
||||
|
||||
\treverse_proxy app:80
|
||||
|
||||
\tencode zstd gzip
|
||||
|
||||
\theader {
|
||||
\t\tX-Content-Type-Options nosniff
|
||||
\t\tX-Frame-Options DENY
|
||||
\t\tReferrer-Policy strict-origin-when-cross-origin
|
||||
\t\t-Server
|
||||
\t}
|
||||
}
|
||||
CADDY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the rendered site config into the app's storage dir. This is a staging
|
||||
* file the operator can mount/copy into Caddy's /config — it does NOT reload the
|
||||
* live proxy (separate container). Returns the absolute path written.
|
||||
*/
|
||||
public function writeCaddyConfig(): string
|
||||
{
|
||||
$dir = storage_path('app/caddy');
|
||||
|
||||
if (! is_dir($dir)) {
|
||||
@mkdir($dir, 0775, true);
|
||||
}
|
||||
|
||||
$path = $dir.'/'.self::CONFIG_FILENAME;
|
||||
file_put_contents($path, $this->renderCaddyConfig().PHP_EOL);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/** Absolute path writeCaddyConfig() targets (no write performed). */
|
||||
public function configPath(): string
|
||||
{
|
||||
return storage_path('app/caddy/'.self::CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Operator note: the app cannot reload Caddy. After writing, reload is an infra
|
||||
* step inside the caddy container.
|
||||
*/
|
||||
public function reloadHint(): string
|
||||
{
|
||||
return 'caddy reload --config /config/'.self::CONFIG_FILENAME;
|
||||
return $domain !== null ? 'https://'.$domain : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,15 @@
|
|||
return [
|
||||
// First tagged release is v0.1.0 (semantic, not -dev). The live build hash
|
||||
// is resolved from .git at runtime (see App\Livewire\Versions\Index).
|
||||
'version' => '0.2.0',
|
||||
'version' => '0.2.1',
|
||||
|
||||
// Default user channel. Only 'stable' and 'beta' are ever offered to users.
|
||||
'channel' => 'stable',
|
||||
|
||||
// Install-time panel domain (APP_DOMAIN). The dashboard Setting overrides it; this
|
||||
// is the fallback so a domain set during install still gets automatic TLS.
|
||||
'domain' => env('APP_DOMAIN'),
|
||||
|
||||
// Real project home (self-hosted Gitea) + license. Open-core, AGPL.
|
||||
'repository' => 'https://git.bave.dev/boban/clusev',
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* The panel domain moved from a dashboard Setting to the install-time APP_DOMAIN
|
||||
* (kept consistent with APP_URL, the Reverb endpoint, cookie security and Caddy's
|
||||
* auto-TLS — none safely re-derivable at runtime, since Caddy's site address and the
|
||||
* browser's Reverb host are fixed at install/build and .env is never rewritten).
|
||||
*
|
||||
* We remove ONLY the inert empty rows that the old write-path left behind — any
|
||||
* non-empty value an operator actually configured is preserved as a historical
|
||||
* record (it is no longer read). To run such a domain under the new model, set it via
|
||||
* install.sh (re-run); see CHANGELOG.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('settings')
|
||||
->whereIn('key', ['dashboard_domain', 'dashboard_email'])
|
||||
->where(function ($q) {
|
||||
$q->whereNull('value')->orWhere('value', '');
|
||||
})
|
||||
->delete();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// No-op: these settings are obsolete and intentionally not recreated.
|
||||
}
|
||||
};
|
||||
|
|
@ -3,8 +3,11 @@
|
|||
# SITE_ADDRESS=:80 -> plain HTTP on the bare IP, no cert
|
||||
# SITE_ADDRESS=https://clusev.host.tld -> auto Let's Encrypt, :443, auto-redirect
|
||||
#
|
||||
# Reverb (Pusher protocol) rides the same address over /app/* (WS) and /apps/*
|
||||
# (event API), proxied to reverb:8080 — no extra exposed port, no mixed content.
|
||||
# Fully automatic: Caddy issues + renews the certificate and redirects HTTP->HTTPS with
|
||||
# no script, cron or reload. The domain is the single install knob (APP_DOMAIN); to
|
||||
# change it later, re-run install.sh with the new domain. Reverb (Pusher protocol) rides
|
||||
# the same address over /app/* (WS) and /apps/* (event API), proxied to reverb:8080 —
|
||||
# no extra exposed port, no mixed content.
|
||||
|
||||
{
|
||||
# ACME_EMAIL is never empty at runtime: compose defaults it to admin@localhost
|
||||
|
|
|
|||
|
|
@ -212,8 +212,8 @@
|
|||
<x-icon name="settings" class="h-3.5 w-3.5" />
|
||||
</x-btn>
|
||||
@endif
|
||||
{{-- Toggle: secondary to make secure, quiet ghost to loosen. --}}
|
||||
<x-btn :variant="$check['secure'] ? 'ghost' : 'secondary'" size="sm" class="shrink-0"
|
||||
{{-- Uniform style across all rows (R10): always the bordered secondary button. --}}
|
||||
<x-btn variant="secondary" size="sm" class="shrink-0"
|
||||
wire:click="$dispatch('openModal', { component: 'modals.hardening-action', arguments: { serverId: {{ $server->id }}, action: '{{ $check['key'] }}', enable: {{ $check['featureOn'] ? 'false' : 'true' }} } })">
|
||||
{{ $check['featureOn'] ? 'Deaktivieren' : 'Aktivieren' }}
|
||||
</x-btn>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
@php
|
||||
$field = 'h-9 w-full rounded-md border border-line bg-inset px-3 font-mono text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none';
|
||||
$label = 'mb-1 block font-mono text-[11px] uppercase tracking-wider text-ink-3';
|
||||
$err = 'mt-1 flex items-center gap-1.5 font-mono text-[11px] text-offline';
|
||||
@endphp
|
||||
|
||||
<div class="space-y-5">
|
||||
{{-- Header --}}
|
||||
<div class="flex flex-wrap items-center gap-4 rounded-xl border border-line bg-surface p-5 shadow-panel">
|
||||
|
|
@ -31,8 +25,10 @@
|
|||
<div class="mb-4 flex items-start gap-3 rounded-md border border-online/25 bg-online/5 p-3">
|
||||
<x-icon name="shield" class="mt-0.5 h-4 w-4 shrink-0 text-online" />
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm text-ink">HTTPS via Let's Encrypt (Caddy)</p>
|
||||
<p class="mt-0.5 font-mono text-[11px] text-ink-3">Caddy stellt automatisch ein Zertifikat für <span class="text-ink-2">{{ $domain }}</span> aus und erneuert es.</p>
|
||||
<p class="text-sm text-ink">TLS aktiv — Zertifikat automatisch über Let's Encrypt</p>
|
||||
<p class="mt-0.5 font-mono text-[11px] text-ink-3">Erreichbar unter
|
||||
<a href="{{ $panelUrl }}" class="text-accent-text hover:underline">{{ $panelUrl }}</a> ·
|
||||
wird automatisch ausgestellt und erneuert. Voraussetzung: die Domain zeigt per DNS auf diesen Server.</p>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
|
|
@ -40,7 +36,7 @@
|
|||
<x-icon name="alert" class="mt-0.5 h-4 w-4 shrink-0 text-warning" />
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm text-ink">Bare-IP-Modus: Klartext-HTTP</p>
|
||||
<p class="mt-0.5 font-mono text-[11px] text-ink-3">Ohne Domain läuft das Panel inkl. 2FA und Audit über unverschlüsseltes HTTP. Für den Produktivbetrieb eine Domain setzen.</p>
|
||||
<p class="mt-0.5 font-mono text-[11px] text-ink-3">Ohne Domain läuft das Panel inkl. 2FA und Audit über unverschlüsseltes HTTP. Domain eintragen — TLS wird dann automatisch eingerichtet, ohne weitere Schritte.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -59,45 +55,29 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<form wire:submit="saveDomain" class="space-y-4">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="{{ $label }}">Domain</label>
|
||||
<input wire:model="domain" type="text" placeholder="panel.example.com" autocomplete="off" class="{{ $field }}" />
|
||||
@error('domain') <p class="{{ $err }}"><x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $message }}</p> @enderror
|
||||
<p class="mt-1 font-mono text-[11px] text-ink-4">Leer lassen für Zugriff per IP über HTTP.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="{{ $label }}">Admin-E-Mail (Let's Encrypt)</label>
|
||||
<input wire:model="email" type="email" placeholder="admin@example.com" autocomplete="off" class="{{ $field }}" />
|
||||
@error('email') <p class="{{ $err }}"><x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $message }}</p> @enderror
|
||||
<p class="mt-1 font-mono text-[11px] text-ink-4">Für Zertifikatshinweise von Let's Encrypt.</p>
|
||||
</div>
|
||||
{{-- Read-only: the panel domain is an install-time decision (kept consistent
|
||||
with URL/WebSocket/cookie security); TLS provisions itself automatically. --}}
|
||||
<dl class="divide-y divide-line overflow-hidden rounded-md border border-line">
|
||||
<div class="flex items-center justify-between gap-3 px-4 py-2.5">
|
||||
<dt class="text-sm text-ink-2">Domain</dt>
|
||||
<dd class="truncate font-mono text-sm text-ink">{{ $domain !== '' ? $domain : '— (Zugriff per IP)' }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<x-btn variant="primary" type="submit" wire:target="saveDomain" wire:loading.attr="disabled">
|
||||
<svg wire:loading wire:target="saveDomain" class="h-3.5 w-3.5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56" stroke-linecap="round" /></svg>
|
||||
Speichern
|
||||
</x-btn>
|
||||
<div class="flex items-center justify-between gap-3 px-4 py-2.5">
|
||||
<dt class="text-sm text-ink-2">Zugriffsadresse</dt>
|
||||
<dd class="truncate font-mono text-sm text-ink">
|
||||
@if ($panelUrl)
|
||||
<a href="{{ $panelUrl }}" class="text-accent-text hover:underline">{{ $panelUrl }}</a>
|
||||
@else
|
||||
HTTP (Bare-IP)
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{-- Generated Caddy site config --}}
|
||||
<div class="mt-5 border-t border-line pt-5">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<x-icon name="settings" class="h-3.5 w-3.5 text-ink-3" />
|
||||
<p class="font-mono text-[11px] uppercase tracking-wider text-ink-3">Erzeugte Caddy-Konfiguration</p>
|
||||
</div>
|
||||
<pre class="overflow-x-auto rounded-md border border-line bg-inset p-3 font-mono text-[12px] leading-relaxed text-ink-2"><code>{{ $caddyConfig }}</code></pre>
|
||||
<div class="mt-3 flex items-start gap-3 rounded-md border border-line bg-raised/40 p-3">
|
||||
<x-icon name="alert" class="mt-0.5 h-4 w-4 shrink-0 text-ink-3" />
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm text-ink-2">Caddy läuft als eigener Container und muss neu laden, um die Konfiguration zu übernehmen.</p>
|
||||
<p class="mt-1 font-mono text-[11px] text-ink-4">Die Datei wurde nach <span class="text-ink-3">storage/app/caddy/clusev.caddy</span> geschrieben. Im Caddy-Container ausführen:</p>
|
||||
<pre class="mt-1.5 overflow-x-auto rounded border border-line bg-inset px-2.5 py-1.5 font-mono text-[11px] text-cyan"><code>{{ $reloadHint }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
<p class="mt-2.5 font-mono text-[11px] leading-relaxed text-ink-4">
|
||||
Die Panel-Domain wird bei der Installation gesetzt und bleibt mit URL, WebSocket und
|
||||
Cookie-Sicherheit konsistent. Zum Ändern <span class="text-ink-3">install.sh</span> mit der
|
||||
neuen Domain erneut ausführen — TLS richtet sich danach automatisch ein.
|
||||
</p>
|
||||
</x-panel>
|
||||
|
||||
{{-- Release-Kanal --}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue