parent
a4f3990ce4
commit
c16fb74cd1
|
|
@ -1,14 +0,0 @@
|
|||
[90m= [39m[34;4mApp\Models\TlsaRecord[39;24m {#6794
|
||||
[34mid[39m: [35m1[39m,
|
||||
[34mdomain_id[39m: [35m9[39m,
|
||||
[34mservice[39m: "[32m_25._tcp[39m",
|
||||
[34mhost[39m: "[32mmx.nexlab.at[39m",
|
||||
[34musage[39m: [35m3[39m,
|
||||
[34mselector[39m: [35m1[39m,
|
||||
[34mmatching[39m: [35m1[39m,
|
||||
[34mhash[39m: "[32m0922eee5f6090b241a3f8554a366b3c1adc4088eb1cdffa94ae838c6e580b983[39m",
|
||||
[34mcert_path[39m: "[32m/etc/ssl/mail/fullchain.pem[39m",
|
||||
[34mcreated_at[39m: "[32m2025-10-06 15:24:56[39m",
|
||||
[34mupdated_at[39m: "[32m2025-10-06 15:24:56[39m",
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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('Let’s 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 Let’s 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('Let’s 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('Let’s 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 Let’s 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('Let’s 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;
|
||||
//// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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"--}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue