diff --git a/app/Http/Middleware/AuthenticatedMiddleware.php b/app/Http/Middleware/AuthenticatedMiddleware.php new file mode 100644 index 0000000..cd5306a --- /dev/null +++ b/app/Http/Middleware/AuthenticatedMiddleware.php @@ -0,0 +1,29 @@ +expectsJson()) { + return response()->json(['message' => 'Unauthenticated'], 401); + } + + return redirect()->route('login'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/GuestOnlyMiddleware.php b/app/Http/Middleware/GuestOnlyMiddleware.php new file mode 100644 index 0000000..b9a4cc7 --- /dev/null +++ b/app/Http/Middleware/GuestOnlyMiddleware.php @@ -0,0 +1,26 @@ +route('dashboard'); + } + + return $next($request); + } +} diff --git a/app/Livewire/Ui/Dashboard/HealthCard.php b/app/Livewire/Ui/Dashboard/HealthCard.php index d3b8415..73aa920 100644 --- a/app/Livewire/Ui/Dashboard/HealthCard.php +++ b/app/Livewire/Ui/Dashboard/HealthCard.php @@ -44,6 +44,7 @@ class HealthCard extends Component public ?int $ramPercent = null; public ?string $updatedAtHuman = null; + public $guardOk = []; public function mount(): void { $this->loadData(); @@ -179,19 +180,52 @@ class HealthCard extends Component '127.0.0.1:8080' => ['label' => 'Reverb', 'hint' => 'WebSocket'], ]; - $this->servicesCompact = collect($this->services) - ->map(function ($srv) use ($nameMap) { - $key = (string)($srv['name'] ?? ''); - $ok = (bool) ($srv['ok'] ?? false); - $label = $nameMap[$key]['label'] ?? ucfirst($key); - $hint = $nameMap[$key]['hint'] ?? (str_contains($key, ':') ? $key : ''); +// $nameMap = [ +// // --- Mail --- +// 'postfix' => ['label' => 'Postfix', 'hint' => 'MTA / Versand'], +// 'dovecot' => ['label' => 'Dovecot', 'hint' => 'IMAP / POP3'], +// 'rspamd' => ['label' => 'Rspamd', 'hint' => 'Spamfilter'], +// 'clamav' => ['label' => 'ClamAV', 'hint' => 'Virenscanner'], +// +// // --- Daten & Cache --- +// 'db' => ['label' => 'Datenbank', 'hint' => 'MySQL / MariaDB'], +// '127.0.0.1:6379' => ['label' => 'Redis', 'hint' => 'Cache / Queue'], +// +// // --- Web / PHP --- +// 'php8.2-fpm' => ['label' => 'PHP-FPM', 'hint' => 'PHP Runtime'], +// 'nginx' => ['label' => 'Nginx', 'hint' => 'Webserver'], +// +// // --- MailWolt spezifisch --- +// 'mailwolt-queue' => ['label' => 'MailWolt Queue', 'hint' => 'Job Worker'], +// 'mailwolt-schedule'=> ['label' => 'MailWolt Schedule','hint' => 'Task Scheduler'], +// +// // --- Sonstige Infrastruktur --- +// 'fail2ban' => ['label' => 'Fail2Ban', 'hint' => 'SSH / Mail Protection'], +// 'systemd-journald'=> ['label' => 'System Logs', 'hint' => 'Logging / Journal'], +// +// // --- WebSocket & Echtzeit --- +// '127.0.0.1:8080' => ['label' => 'Reverb', 'hint' => 'WebSocket Server'], +// ]; + + $existing = collect($this->services)->keyBy('name'); + + $this->servicesCompact = collect($nameMap) + ->map(function ($meta, $key) use ($existing) { + $srv = $existing->get($key, []); + $ok = (bool)($srv['ok'] ?? false); return [ - 'label' => $label, - 'hint' => $hint, + 'label' => $meta['label'], + 'hint' => $meta['hint'], 'ok' => $ok, + + // Punktfarbe 'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400', - 'pillText' => $ok ? 'ok' : 'down', + + // ✅ Bessere Status-Texte + 'pillText' => $ok ? 'Aktiv' : 'Offline', + + // Farbe für Pill 'pillClass' => $ok ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' : 'text-rose-300 border-rose-400/30 bg-rose-500/10', @@ -199,6 +233,31 @@ class HealthCard extends Component }) ->values() ->all(); + + $this->guardOk = collect($this->services)->every( + fn($s) => (bool)($s['ok'] ?? false) + ); +// $this->servicesCompact = collect($this->services) +// ->map(function ($srv) use ($nameMap) { +// $key = (string)($srv['name'] ?? ''); +// $ok = (bool) ($srv['ok'] ?? false); +// $label = $nameMap[$key]['label'] ?? ucfirst($key); +// $hint = $nameMap[$key]['hint'] ?? (str_contains($key, ':') ? $key : ''); +// +// var_dump($srv); +// return [ +// 'label' => $label, +// 'hint' => $hint, +// 'ok' => $ok, +// 'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400', +// 'pillText' => $ok ? 'Läuft' : 'Down', +// 'pillClass' => $ok +// ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' +// : 'text-rose-300 border-rose-400/30 bg-rose-500/10', +// ]; +// }) +// ->values() +// ->all(); } protected function decorateDisk(): void diff --git a/app/Livewire/Ui/Dashboard/TopBar.php b/app/Livewire/Ui/Dashboard/TopBar.php index 3ab29e4..eb7ba98 100644 --- a/app/Livewire/Ui/Dashboard/TopBar.php +++ b/app/Livewire/Ui/Dashboard/TopBar.php @@ -28,7 +28,7 @@ class TopBar extends Component public function mount(): void { // Domains + Zertifikate (passe an deinen Storage an) - $this->domainsCount = Domain::count(); // oder aus Cache/Repo + $this->domainsCount = Domain::where('is_server', false)->where('is_system', false)->count(); // oder aus Cache/Repo // Beispiel: Domain::select('cert_expires_at','cert_issuer')... // -> hier simple Annahme: im Cache 'domains:certs' liegt [{domain, days_left, type}] diff --git a/app/Livewire/Ui/System/UpdateCard.php b/app/Livewire/Ui/System/UpdateCard.php new file mode 100644 index 0000000..02e5c25 --- /dev/null +++ b/app/Livewire/Ui/System/UpdateCard.php @@ -0,0 +1,324 @@ +current = $this->readCurrentVersion(); + $this->latest = Cache::get('mailwolt.update_available'); + $this->refreshLowLevelState(); + + // Starttext, falls nichts geprüft wurde + if ($this->message === null) { + $this->message = $this->latest && $this->hasUpdate() + ? "Neue Version verfügbar: {$this->latest}" + : ($this->current ? "Du bist auf dem neuesten Stand ({$this->current})" : "Status unbekannt"); + $this->messagePositive = !$this->hasUpdate(); + } + } + + public function render() + { + return view('livewire.ui.system.update-card'); + } + + /** + * „Erneut prüfen“ – ohne Toast: + * - Progress anzeigen + * - Check-Command laufen lassen + * - Message in der Box aktualisieren + */ + public function refreshState(): void + { + $this->state = 'running'; + $this->message = 'Prüfe auf Updates …'; + $this->messagePositive = null; + + try { + // Passe den Namen hier an dein tatsächliches Command an: + Artisan::call('mailwolt:check-updates'); + } catch (\Throwable $e) { + // weich fallen + } + + // Daten neu einlesen + $this->current = $this->readCurrentVersion(); + $this->latest = Cache::get('mailwolt.update_available'); + + if ($this->hasUpdate()) { + $this->message = "Neue Version verfügbar: {$this->latest}"; + $this->messagePositive = false; // neutral + } else { + $cur = $this->current ?: '–'; + $this->message = "Du bist auf dem neuesten Stand ({$cur})"; + $this->messagePositive = true; // grün + } + + $this->refreshLowLevelState(); + $this->state = 'idle'; + } + + /** + * „Jetzt aktualisieren“ – ohne Toast: + * - Hinweis sofort aus Cache entfernen (Badge weg) + * - Update-Wrapper starten + * - Running + Text in der Box anzeigen + */ + public function runUpdate(): void + { + Cache::forget('mailwolt.update_available'); + + @shell_exec('nohup sudo -n /usr/local/sbin/mw-update >/dev/null 2>&1 &'); + + $this->latest = null; + $this->state = 'running'; + $this->running = true; + $this->message = 'Update läuft …'; + $this->messagePositive = null; + } + + /** --------------------- helpers --------------------- */ + + protected function hasUpdate(): bool + { + if (!$this->latest) return false; + $cur = $this->current ?: '0.0.0'; + return version_compare($this->latest, $cur, '>'); + } + + protected function refreshLowLevelState(): void + { + $state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: ''); + $this->running = ($state === 'running'); + + $rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: ''); + $this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null; + } + + protected function readCurrentVersion(): ?string + { + // bevorzugt /etc/mailwolt/build.info (wird im Installer/Updater gepflegt) + $build = @file_get_contents('/etc/mailwolt/build.info'); + if ($build) { + foreach (preg_split('/\R+/', $build) as $line) { + if (str_starts_with($line, 'version=')) { + $v = trim(substr($line, 8)); + if ($v !== '') return $v; + } + } + } + $v = config('app.version'); + return $v !== '' ? $v : null; + } +} + +// +// +//namespace App\Livewire\Ui\System; +// +//use Livewire\Component; +//use Illuminate\Support\Facades\Artisan; +//use Illuminate\Support\Facades\Cache; +// +//class UpdateCard extends Component +//{ +// public ?string $current = null; // z.B. v1.0.16 (aktuell installiert) +// public ?string $latest = null; // z.B. v1.0.17 (verfügbar) oder null +// public string $state = 'idle'; // idle | running +// +// // optional: Low-level-Infos, falls du sie irgendwo anders brauchst +// public bool $running = false; // Wrapper-/Updater läuft gerade (aus State-Datei) +// public string $log = ''; +// public ?int $rc = null; +// +// public function mount(): void +// { +// $this->current = $this->readCurrentVersion(); +// $this->latest = Cache::get('mailwolt.update_available'); +// $this->refreshLowLevelState(); // liest /var/lib/mailwolt/update/* +// } +// +// public function render() +// { +// return view('livewire.ui.system.update-card'); +// } +// +// /** +// * „Erneut prüfen“: +// * - zeigt Running-Progress +// * - ruft den Checker (dein bestehendes Artisan-Command) +// * - aktualisiert $latest / $current +// * - dispatcht passenden Toast +// */ +// public function refreshState(): void +// { +// $this->state = 'running'; +// +// // Falls vorhanden: dein Command, der cache('mailwolt.update_available') setzt +// // -> Namen ggf. anpassen (du hattest z.B. mailwolt:check-updates erwähnt) +// try { +// Artisan::call('mailwolt:check-updates'); +// } catch (\Throwable $e) { +// // Wenn das Command (noch) nicht existiert, nicht crashen – state einfach zurücksetzen +// } +// +// // App-Status neu einlesen +// $this->current = $this->readCurrentVersion(); +// $this->latest = Cache::get('mailwolt.update_available'); +// +// $hasUpdate = $this->hasUpdate(); +// +// // Toast ausspielen +// $this->dispatch('toast', +// type: $hasUpdate ? 'info' : 'done', +// badge: 'Update', +// title: $hasUpdate ? 'Neue Version verfügbar' : 'Alles aktuell', +// text: $hasUpdate +// ? "Verfügbar: {$this->latest} – installiert: {$this->current}" +// : "Du bist auf dem neuesten Stand ({$this->current}).", +// duration: 6000 +// ); +// +// // Low-level-State (optional) und UI-State zurücksetzen +// $this->refreshLowLevelState(); +// $this->state = 'idle'; +// } +// +// /** +// * „Jetzt aktualisieren“: +// * - blendet die verfügbare-Version sofort aus +// * - startet den Root-Wrapper im Hintergrund +// * - zeigt einen Toast „Update gestartet“ +// */ +// public function runUpdate(): void +// { +// Cache::forget('mailwolt.update_available'); // Badge sofort weg +// @shell_exec('nohup sudo -n /usr/local/sbin/mw-update >/dev/null 2>&1 &'); +// +// $this->state = 'running'; +// $this->running = true; +// +// $this->dispatch('toast', +// type: 'info', +// badge: 'Update', +// title: 'Update gestartet', +// text: 'Das System wird aktualisiert …', +// duration: 6000 +// ); +// } +// +// /** ---- Helpers ------------------------------------------------------- */ +// +// protected function hasUpdate(): bool +// { +// if (!$this->latest) return false; +// $cur = $this->current ?: '0.0.0'; +// return version_compare($this->latest, $cur, '>'); +// } +// +// protected function refreshLowLevelState(): void +// { +// $state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: ''); +// $this->running = ($state === 'running'); +// +// $rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: ''); +// $this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null; +// +// // Log bewusst NICHT angezeigt, aber verfügbar, falls du es später brauchst +// $this->log = @shell_exec('tail -n 200 /var/log/mailwolt-update.log 2>/dev/null') ?? ''; +// } +// +// protected function readCurrentVersion(): ?string +// { +// // bevorzugt build.info, sonst config('app.version') +// $build = @file_get_contents('/etc/mailwolt/build.info'); +// if ($build) { +// foreach (preg_split('/\R+/', $build) as $line) { +// if (str_starts_with($line, 'version=')) { +// $v = trim(substr($line, 8)); +// if ($v !== '') return $v; +// } +// } +// } +// $v = config('app.version'); +// return $v !== '' ? $v : null; +// } +//} +//// +////namespace App\Livewire\Ui\System; +//// +////use Livewire\Component; +////use Illuminate\Support\Facades\Artisan; +////use Illuminate\Support\Facades\Cache; +//// +////class UpdateCard extends Component +////{ +//// public ?string $current = null; +//// public ?string $latest = null; +//// public string $state = 'idle'; // idle|running +//// +//// public bool $running = false; +//// public string $log = ''; +//// public ?int $rc = null; +//// +//// public function mount(): void +//// { +////// $this->refreshState(); +//// $this->latest = cache('mailwolt.update_available'); +//// +//// } +//// +//// public function render() +//// { +//// return view('livewire.ui.system.update-card'); +//// } +//// +//// public function refreshState(): void +//// { +//// $state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: ''); +//// $this->running = ($state === 'running'); +//// +//// $rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: ''); +//// $this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null; +//// +//// // letzte 200 Zeilen Log +//// $this->log = @shell_exec('tail -n 200 /var/log/mailwolt-update.log 2>/dev/null') ?? ''; +//// } +//// +//// public function runUpdate(): void +//// { +//// // Hinweis „Update verfügbar“ ausblenden +//// cache()->forget('mailwolt.update_available'); +//// +//// // Update im Hintergrund starten +//// @shell_exec('nohup sudo -n /usr/local/sbin/mw-update >/dev/null 2>&1 &'); +//// +//// $this->running = true; +//// $this->dispatch('toast', +//// type: 'info', +//// badge: 'Update', +//// title: 'Update gestartet', +//// text: 'Das System wird aktualisiert …', +//// duration: 6000 +//// ); +//// } +////} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 221d509..770e59b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -23,13 +23,8 @@ class AppServiceProvider extends ServiceProvider */ public function boot(\App\Support\SettingsRepository $settings): void { - Domain::observe(DomainObserver::class); -// $ver = trim(@file_get_contents(base_path('VERSION'))) ?: 'dev'; -// config(['app.version' => $ver]); - - config(['app.version' => trim(@file_get_contents('/var/lib/mailwolt/version')) ?: 'dev']); if (file_exists(base_path('.git/HEAD'))) { $ref = trim(file_get_contents(base_path('.git/HEAD'))); @@ -39,6 +34,7 @@ class AppServiceProvider extends ServiceProvider } else { $commit = $ref; } + config(['app.git_commit' => substr($commit ?? '', 0, 7)]); } diff --git a/app/Providers/BuildMetaServiceProvider.php b/app/Providers/BuildMetaServiceProvider.php new file mode 100644 index 0000000..9464ad9 --- /dev/null +++ b/app/Providers/BuildMetaServiceProvider.php @@ -0,0 +1,37 @@ +app->singleton(BuildMeta::class, fn () => BuildMeta::detect()); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + $meta = $this->app->make(BuildMeta::class); + + // zentral verfügbar + config([ + 'app.version' => $meta->version, + 'app.git_rev' => $meta->rev, + 'app.git_short' => $meta->short, + 'app.build_time' => $meta->updated, + ]); + + // optional: in allen Views als $build + View::share('build', $meta); + } +} diff --git a/app/Support/BuildMeta.php b/app/Support/BuildMeta.php new file mode 100644 index 0000000..b65d0a4 --- /dev/null +++ b/app/Support/BuildMeta.php @@ -0,0 +1,77 @@ +version = $v; + if ($k === 'rev') $m->rev = $v; + if ($k === 'updated') $m->updated = $v; + } + } + + // 2) Fallback: .env APP_VERSION + if ($m->version === 'dev' && ($envVer = env('APP_VERSION'))) { + $m->version = trim($envVer); + } + + // 3) Fallback: Git (ohne "-dirty") + if ($m->rev === '' || $m->version === 'dev') { + $head = $basePath.'/.git/HEAD'; + if (is_file($head)) { + $ref = trim((string)@file_get_contents($head)); + if (Str::startsWith($ref, 'ref:')) { + $refFile = $basePath.'/.git/'.substr($ref, 5); + $commit = @file_get_contents($refFile); + } else { + $commit = $ref; + } + $m->rev = $m->rev ?: trim((string)$commit); + + if ($m->version === 'dev') { + $tag = @shell_exec('git -C '.escapeshellarg($basePath).' describe --tags --abbrev=0 2>/dev/null'); + $m->version = $tag ? trim($tag) : 'dev'; + } + } + } + + // Sauber: "-dirty" entfernen + $m->version = preg_replace('/-dirty$/', '', $m->version); + + // Kurz-Commit + $m->short = $m->rev ? substr($m->rev, 0, 7) : ''; + + return $m; + } + + public function toArray(): array + { + return [ + 'version' => $this->version, + 'rev' => $this->rev, + 'short' => $this->short, + 'updated' => $this->updated, + ]; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index b919e34..444441e 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -15,6 +15,8 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->alias([ 'ensure.setup' => \App\Http\Middleware\EnsureSetupCompleted::class, 'signup.open' => \App\Http\Middleware\SignupOpen::class, + 'auth.user' => \App\Http\Middleware\AuthenticatedMiddleware::class, + 'guest.only' => \App\Http\Middleware\GuestOnlyMiddleware::class, ]); }) diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 38b258d..99087b2 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,4 +2,5 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\BuildMetaServiceProvider::class, ]; diff --git a/config/app.php b/config/app.php index fbc7da5..847648f 100644 --- a/config/app.php +++ b/config/app.php @@ -13,7 +13,7 @@ return [ | */ - 'name' => env('APP_NAME', 'Laravel'), + 'name' => env('APP_NAME', 'MailWolt'), /* |-------------------------------------------------------------------------- @@ -123,6 +123,8 @@ return [ 'store' => env('APP_MAINTENANCE_STORE', 'database'), ], - 'version' => env('APP_VERSION', '1.0.0'), - 'git_commit' => env('APP_GIT_COMMIT', ''), + 'version' => env('APP_VERSION', 'dev'), + 'git_rev' => null, + 'git_short' => null, + 'build_time' => null, ]; diff --git a/config/ui/header.php b/config/ui/header.php index a0a3152..c492453 100644 --- a/config/ui/header.php +++ b/config/ui/header.php @@ -18,25 +18,25 @@ return [ [ 'title' => 'Security', 'icon' => 'icons.icon-security', - 'route' => 'logout', + 'route' => 'ui.logout', 'roles' => ['super_admin', 'admin', 'employee', 'user'], ], [ 'title' => 'Team', 'icon' => 'icons.icon-team', - 'route' => 'logout', + 'route' => 'ui.logout', 'roles' => ['super_admin', 'admin', 'employee', 'user'], ], [ 'title' => 'Settings', 'icon' => 'icons.icon-settings', - 'route' => 'logout', + 'route' => 'ui.logout', 'roles' => ['super_admin', 'admin', 'employee', 'user'], ], [ 'title' => 'Logout', 'icon' => 'icons.icon-logout', - 'route' => 'logout', + 'route' => 'ui.logout', 'roles' => ['super_admin', 'admin', 'employee', 'user'], ], ] diff --git a/resources/css/app.css b/resources/css/app.css index ddb3470..fab9c28 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -32,6 +32,14 @@ } +@keyframes pulse-slow { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 0.8; } +} +.animate-pulse-slow { + animation: pulse-slow 4s ease-in-out infinite; +} + .safe-pads { padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); diff --git a/resources/views/components/partials/header.blade.php b/resources/views/components/partials/header.blade.php index 94c9560..91db085 100644 --- a/resources/views/components/partials/header.blade.php +++ b/resources/views/components/partials/header.blade.php @@ -1,36 +1,13 @@ {{--resources/views/components/partials/header.blade.php--}}
- + class="#sticky top-0 w-full border-b hr #rounded-lg max-w-9xl mx-auto">