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

View File

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

View File

@ -10,6 +10,19 @@
<i class="ph ph-list"></i>
</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"--}}
{{-- 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"--}}

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="text-slate-200">{{ $r['name'] }}</span>
</div>
{{ $recordColors[$r['type']] }}
<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']" />
</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>
@endforeach
</div>

View File

@ -1,5 +1,6 @@
<?php
use App\Jobs\RunHealthChecks;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
@ -8,7 +9,8 @@ Artisan::command('inspire', function () {
$this->comment(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')
->everyFiveMinutes()