Rechtebechebung für User mit Sudorechte

main 1.0.0
boban 2025-10-20 21:21:25 +02:00
parent a4f3990ce4
commit c16fb74cd1
8 changed files with 205 additions and 201 deletions

View File

@ -1,14 +0,0 @@
= App\Models\TlsaRecord {#6794
id: 1,
domain_id: 9,
service: "_25._tcp",
host: "mx.nexlab.at",
usage: 3,
selector: 1,
matching: 1,
hash: "0922eee5f6090b241a3f8554a366b3c1adc4088eb1cdffa94ae838c6e580b983",
cert_path: "/etc/ssl/mail/fullchain.pem",
created_at: "2025-10-06 15:24:56",
updated_at: "2025-10-06 15:24:56",
}

View File

@ -1,30 +0,0 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R

View File

@ -0,0 +1,39 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class CheckUpdates extends Command
{
protected $signature = 'mailwolt:check-updates';
protected $description = 'Check for newer MailWolt releases via git tags';
public function handle(): int
{
$appPath = base_path();
$current = trim(@file_get_contents(base_path('VERSION'))) ?: '0.0.0';
// newest tag from origin (sorted semver-friendly)
$latest = trim(shell_exec(
"cd {$appPath} && git fetch --tags --quiet origin && git tag --list | sort -V | tail -n1"
) ?? '');
// Tags haben usually ein 'v' Prefix entfernen
$latest = ltrim($latest, 'v');
if (!$latest) {
$this->warn('Keine Release-Tags gefunden.');
cache()->forget('mailwolt.update_available');
return 0;
}
if (version_compare($latest, $current, '>')) {
cache()->forever('mailwolt.update_available', $latest);
$this->info("Update verfügbar: {$latest} (installiert: {$current})");
} else {
cache()->forget('mailwolt.update_available');
$this->info("Aktuell (installiert: {$current}).");
}
return 0;
}
}

View File

@ -1,147 +1,147 @@
<?php <?php
//
namespace App\Console\Commands; //namespace App\Console\Commands;
//
use Illuminate\Console\Command; //use Illuminate\Console\Command;
use Symfony\Component\Process\Process; //use Symfony\Component\Process\Process;
//
class ProvisionCert extends Command //class ProvisionCert extends Command
{ //{
protected $signature = 'mailwolt:provision-cert //// protected $signature = 'mailwolt:provision-cert
{domain : z.B. mail.example.com} //// {domain : z.B. mail.example.com}
{--email= : E-Mail für Let\'s Encrypt} //// {--email= : E-Mail für Let\'s Encrypt}
{--self-signed : Statt LE ein self-signed Zertifikat erzeugen}'; //// {--self-signed : Statt LE ein self-signed Zertifikat erzeugen}';
////
protected $description = 'Beschafft ein Zertifikat (LE oder self-signed) und setzt Nginx darauf.'; //// protected $description = 'Beschafft ein Zertifikat (LE oder self-signed) und setzt Nginx darauf.';
////
private string $sslDir = '/etc/mailwolt/ssl'; //// private string $sslDir = '/etc/mailwolt/ssl';
private string $certPath; //// private string $certPath;
private string $keyPath; //// private string $keyPath;
//
public function handle(): int //// public function handle(): int
{ //// {
$domain = $this->argument('domain'); //// $domain = $this->argument('domain');
$email = (string)$this->option('email'); //// $email = (string)$this->option('email');
$self = (bool)$this->option('self-signed'); //// $self = (bool)$this->option('self-signed');
////
$this->certPath = "{$this->sslDir}/cert.pem"; //// $this->certPath = "{$this->sslDir}/cert.pem";
$this->keyPath = "{$this->sslDir}/key.pem"; //// $this->keyPath = "{$this->sslDir}/key.pem";
////
if (!is_dir($this->sslDir)) { //// if (!is_dir($this->sslDir)) {
@mkdir($this->sslDir, 0750, true); //// @mkdir($this->sslDir, 0750, true);
@chgrp($this->sslDir, 'www-data'); //// @chgrp($this->sslDir, 'www-data');
} //// }
////
if ($self) { //// if ($self) {
return $this->issueSelfSigned($domain); //// return $this->issueSelfSigned($domain);
} //// }
////
// Versuche Let's Encrypt bei Fehler fallback self-signed //// // Versuche Let's Encrypt bei Fehler fallback self-signed
$rc = $this->issueLetsEncrypt($domain, $email); //// $rc = $this->issueLetsEncrypt($domain, $email);
if ($rc !== 0) { //// if ($rc !== 0) {
$this->warn('Lets Encrypt fehlgeschlagen erstelle Self-Signed als Fallback…'); //// $this->warn('Lets Encrypt fehlgeschlagen erstelle Self-Signed als Fallback…');
return $this->issueSelfSigned($domain); //// return $this->issueSelfSigned($domain);
} //// }
return $rc; //// return $rc;
} //// }
////
private function issueLetsEncrypt(string $domain, string $email): int //// private function issueLetsEncrypt(string $domain, string $email): int
{ //// {
if (empty($email)) { //// if (empty($email)) {
$this->error('Für Lets Encrypt ist --email erforderlich.'); //// $this->error('Für Lets Encrypt ist --email erforderlich.');
return 2; //// return 2;
} //// }
////
// Webroot sicherstellen (Nginx-Standort ist im Installer bereits konfiguriert) //// // Webroot sicherstellen (Nginx-Standort ist im Installer bereits konfiguriert)
@mkdir('/var/www/letsencrypt', 0755, true); //// @mkdir('/var/www/letsencrypt', 0755, true);
////
$cmd = [ //// $cmd = [
'bash','-lc', //// 'bash','-lc',
// non-interactive, webroot challenge //// // non-interactive, webroot challenge
"certbot certonly --webroot -w /var/www/letsencrypt -d {$domain} ". //// "certbot certonly --webroot -w /var/www/letsencrypt -d {$domain} ".
"--email ".escapeshellarg($email)." --agree-tos --no-eff-email --non-interactive --rsa-key-size 2048" //// "--email ".escapeshellarg($email)." --agree-tos --no-eff-email --non-interactive --rsa-key-size 2048"
]; //// ];
$p = new Process($cmd, null, ['PATH' => getenv('PATH') ?: '/usr/bin:/bin']); //// $p = new Process($cmd, null, ['PATH' => getenv('PATH') ?: '/usr/bin:/bin']);
$p->setTimeout(600); //// $p->setTimeout(600);
$p->run(function($type,$buff){ $this->output->write($buff); }); //// $p->run(function($type,$buff){ $this->output->write($buff); });
////
if (!$p->isSuccessful()) { //// if (!$p->isSuccessful()) {
$this->error('Certbot-Fehler.'); //// $this->error('Certbot-Fehler.');
return 1; //// return 1;
} //// }
////
// Pfade vom Certbot-Store //// // Pfade vom Certbot-Store
$leBase = "/etc/letsencrypt/live/{$domain}"; //// $leBase = "/etc/letsencrypt/live/{$domain}";
$fullchain = "{$leBase}/fullchain.pem"; //// $fullchain = "{$leBase}/fullchain.pem";
$privkey = "{$leBase}/privkey.pem"; //// $privkey = "{$leBase}/privkey.pem";
////
if (!is_file($fullchain) || !is_file($privkey)) { //// if (!is_file($fullchain) || !is_file($privkey)) {
$this->error("LE-Dateien fehlen unter {$leBase}"); //// $this->error("LE-Dateien fehlen unter {$leBase}");
return 1; //// return 1;
} //// }
////
// In unsere Standard-Pfade kopieren (Nginx zeigt bereits darauf) //// // In unsere Standard-Pfade kopieren (Nginx zeigt bereits darauf)
if (!@copy($fullchain, $this->certPath) || !@copy($privkey, $this->keyPath)) { //// if (!@copy($fullchain, $this->certPath) || !@copy($privkey, $this->keyPath)) {
$this->error('Konnte Zertifikate nicht in /etc/mailwolt/ssl kopieren.'); //// $this->error('Konnte Zertifikate nicht in /etc/mailwolt/ssl kopieren.');
return 1; //// return 1;
} //// }
////
@chown($this->certPath, 'root'); @chgrp($this->certPath, 'www-data'); @chmod($this->certPath, 0640); //// @chown($this->certPath, 'root'); @chgrp($this->certPath, 'www-data'); @chmod($this->certPath, 0640);
@chown($this->keyPath, 'root'); @chgrp($this->keyPath, 'www-data'); @chmod($this->keyPath, 0640); //// @chown($this->keyPath, 'root'); @chgrp($this->keyPath, 'www-data'); @chmod($this->keyPath, 0640);
////
// Nginx reload //// // Nginx reload
$reload = new Process(['bash','-lc','systemctl reload nginx']); //// $reload = new Process(['bash','-lc','systemctl reload nginx']);
$reload->run(); //// $reload->run();
////
$this->info('Lets Encrypt Zertifikat gesetzt und Nginx neu geladen.'); //// $this->info('Lets Encrypt Zertifikat gesetzt und Nginx neu geladen.');
return 0; //// return 0;
} //// }
////
private function issueSelfSigned(string $domain): int //// private function issueSelfSigned(string $domain): int
{ //// {
$cfgPath = "{$this->sslDir}/openssl.cnf"; //// $cfgPath = "{$this->sslDir}/openssl.cnf";
$cfg = <<<CFG //// $cfg = <<<CFG
[req] ////[req]
default_bits = 2048 ////default_bits = 2048
prompt = no ////prompt = no
default_md = sha256 ////default_md = sha256
req_extensions = req_ext ////req_extensions = req_ext
distinguished_name = dn ////distinguished_name = dn
////
[dn] ////[dn]
CN = {$domain} ////CN = {$domain}
O = MailWolt ////O = MailWolt
C = DE ////C = DE
////
[req_ext] ////[req_ext]
subjectAltName = @alt_names ////subjectAltName = @alt_names
////
[alt_names] ////[alt_names]
DNS.1 = {$domain} ////DNS.1 = {$domain}
CFG; ////CFG;
@file_put_contents($cfgPath, $cfg); //// @file_put_contents($cfgPath, $cfg);
////
$cmd = [ //// $cmd = [
'bash','-lc', //// 'bash','-lc',
"openssl req -x509 -newkey rsa:2048 -days 825 -nodes -keyout {$this->keyPath} -out {$this->certPath} -config {$cfgPath}" //// "openssl req -x509 -newkey rsa:2048 -days 825 -nodes -keyout {$this->keyPath} -out {$this->certPath} -config {$cfgPath}"
]; //// ];
$p = new Process($cmd); //// $p = new Process($cmd);
$p->setTimeout(60); //// $p->setTimeout(60);
$p->run(function($t,$b){ $this->output->write($b); }); //// $p->run(function($t,$b){ $this->output->write($b); });
////
if (!$p->isSuccessful()) { //// if (!$p->isSuccessful()) {
$this->error('Self-Signed Erstellung fehlgeschlagen.'); //// $this->error('Self-Signed Erstellung fehlgeschlagen.');
return 1; //// return 1;
} //// }
////
@chown($this->certPath, 'root'); @chgrp($this->certPath, 'www-data'); @chmod($this->certPath, 0640); //// @chown($this->certPath, 'root'); @chgrp($this->certPath, 'www-data'); @chmod($this->certPath, 0640);
@chown($this->keyPath, 'root'); @chgrp($this->keyPath, 'www-data'); @chmod($this->keyPath, 0640); //// @chown($this->keyPath, 'root'); @chgrp($this->keyPath, 'www-data'); @chmod($this->keyPath, 0640);
@chmod($this->sslDir, 0750); //// @chmod($this->sslDir, 0750);
////
$reload = new Process(['bash','-lc','systemctl reload nginx']); //// $reload = new Process(['bash','-lc','systemctl reload nginx']);
$reload->run(); //// $reload->run();
////
$this->info('Self-Signed Zertifikat erstellt und Nginx neu geladen.'); //// $this->info('Self-Signed Zertifikat erstellt und Nginx neu geladen.');
return 0; //// return 0;
} //// }
} //}

View File

@ -26,6 +26,9 @@ class AppServiceProvider extends ServiceProvider
Domain::observe(DomainObserver::class); Domain::observe(DomainObserver::class);
$ver = trim(@file_get_contents(base_path('VERSION'))) ?: 'dev';
config(['app.version' => $ver]);
try { try {
$S = app(\App\Support\SettingsRepository::class); $S = app(\App\Support\SettingsRepository::class);

View File

@ -10,6 +10,19 @@
<i class="ph ph-list"></i> <i class="ph ph-list"></i>
</button> </button>
@if ($latest = cache('mailwolt.update_available'))
<div class="bg-blue-900/40 text-blue-100 p-4 rounded-xl border border-blue-800">
<div class="flex justify-between items-center">
<div>
<strong>Neue Version verfügbar:</strong> {{ $latest }}
</div>
<button wire:click="runUpdate"
class="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded">
Jetzt aktualisieren
</button>
</div>
</div>
@endif
{{-- <button id="sidebar-toggle-btn"--}} {{-- <button id="sidebar-toggle-btn"--}}
{{-- class="action-button sidebar-toggle flex items-center justify-center p-1.5 shadow-2xl rounded">--}} {{-- class="action-button sidebar-toggle flex items-center justify-center p-1.5 shadow-2xl rounded">--}}
{{-- <svg class="size-5 group-[.expanded]/side:rotate-180"--}} {{-- <svg class="size-5 group-[.expanded]/side:rotate-180"--}}

View File

@ -60,20 +60,11 @@
<span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span> <span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>
<span class="text-slate-200">{{ $r['name'] }}</span> <span class="text-slate-200">{{ $r['name'] }}</span>
</div> </div>
{{ $recordColors[$r['type']] }}
<div class="flex items-center gap-2 text-slate-300/70"> <div class="flex items-center gap-2 text-slate-300/70">
<span class="text-[11px] px-2 py-0.5 rounded {{ $recordColors['OPTIONAL'] ?? 'bg-slate-700/50 text-slate-300' }}">Optional</span>
<x-button.copy-btn :text="$r['value']" /> <x-button.copy-btn :text="$r['value']" />
</div> </div>
</div> </div>
<div class="px-4 pb-3">
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm opacity-70 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
@if(!empty($r['helpUrl']))
<a href="{{ $r['helpUrl'] }}" target="_blank" rel="noopener"
class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200">
<i class="ph ph-arrow-square-out"></i>{{ $r['helpLabel'] }}
</a>
@endif
</div>
</div> </div>
@endforeach @endforeach
</div> </div>

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Jobs\RunHealthChecks;
use Illuminate\Foundation\Inspiring; use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule; use Illuminate\Support\Facades\Schedule;
@ -8,7 +9,8 @@ Artisan::command('inspire', function () {
$this->comment(Inspiring::quote()); $this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote'); })->purpose('Display an inspiring quote');
Schedule::job(\App\Jobs\RunHealthChecks::class)->everytenSeconds()->withoutOverlapping(); Schedule::job(RunHealthChecks::class)->everytenSeconds()->withoutOverlapping();
Schedule::command('mailwolt:check-updates')->dailyAt('04:10');
Schedule::command('mail:update-stats') Schedule::command('mail:update-stats')
->everyFiveMinutes() ->everyFiveMinutes()