Compare commits
No commits in common. "main" and "v1.1.147" have entirely different histories.
|
|
@ -33,7 +33,7 @@ SESSION_ENCRYPT=false
|
||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
# For cross-subdomain session sharing (e.g. webmail on mail.example.com):
|
# For cross-subdomain session sharing (e.g. webmail on mail.example.com):
|
||||||
# SESSION_DOMAIN=.example.com
|
# SESSION_DOMAIN=.example.com
|
||||||
SESSION_DOMAIN=
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
#BROADCAST_CONNECTION=log
|
#BROADCAST_CONNECTION=log
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/storage
|
/public/storage
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
/storage/backups
|
|
||||||
/storage/pail
|
/storage/pail
|
||||||
/vendor
|
/vendor
|
||||||
Homestead.json
|
Homestead.json
|
||||||
|
|
|
||||||
102
CLAUDE.md
102
CLAUDE.md
|
|
@ -1,102 +0,0 @@
|
||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development (starts PHP server + queue + logs + Vite concurrently)
|
|
||||||
composer dev
|
|
||||||
|
|
||||||
# Build assets for production
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Vite dev server only
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# Run all tests
|
|
||||||
composer test
|
|
||||||
php artisan test
|
|
||||||
|
|
||||||
# Run a single test file
|
|
||||||
php artisan test tests/Feature/ExampleTest.php
|
|
||||||
|
|
||||||
# Run a single test by name
|
|
||||||
php artisan test --filter=TestName
|
|
||||||
|
|
||||||
# Migrations
|
|
||||||
php artisan migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Design
|
|
||||||
- Tailwind CSS v4
|
|
||||||
- Modals sind immer vom Livewire Modals
|
|
||||||
- Kein Inline-Style nur wenn wirklich notwendig.
|
|
||||||
- Immer prüfen das das Style nicht kaputt ist und wenn Icons in Buttons mit Text sind, sind diese immer nebeneinadner Nie übereinander.
|
|
||||||
|
|
||||||
### What this is
|
|
||||||
Mailwolt is a self-hosted mail server administration panel (German UI). It manages domains, mailboxes, aliases, DNS records (DKIM/DMARC/SPF/TLSA), TLS, Fail2ban, IMAP, and email quarantine/queues. It also has a mail sandbox for testing Postfix transports.
|
|
||||||
|
|
||||||
### Layout & Routing
|
|
||||||
- **`resources/views/layouts/dvx.blade.php`** is the actual app layout (not `app.blade.php`). Livewire full-page components use `#[Layout('layouts.dvx')]`.
|
|
||||||
- Routes are in `routes/web.php` — each page maps directly to a Livewire full-page component via `->name()`.
|
|
||||||
- Navigation structure is driven by `config/ui-menu.php`.
|
|
||||||
- Für den Style wird Tailwind CSS v4
|
|
||||||
|
|
||||||
### Livewire component structure
|
|
||||||
```
|
|
||||||
app/Livewire/Ui/
|
|
||||||
├── Nx/ — Current production UI (Dashboard, DomainList, MailboxList, AliasList, etc.)
|
|
||||||
├── Domain/ — Domain modals and DKIM/DNS views
|
|
||||||
├── Mail/ — Mailbox/alias modals, queue, quarantine
|
|
||||||
├── Security/ — Fail2ban, SSL, RSpamd, TLS, audit logs
|
|
||||||
├── System/ — Settings, users, API keys, webhooks, sandbox, backups
|
|
||||||
├── Search/ — Global search palette modal
|
|
||||||
└── Webmail/ — Webmail-specific components
|
|
||||||
```
|
|
||||||
|
|
||||||
Full-page components live in `Ui/Nx/` and `Ui/System/`, `Ui/Security/`, etc. Modals are always in a `Modal/` subfolder and extend `LivewireUI\Modal\ModalComponent`.
|
|
||||||
|
|
||||||
### Modals (wire-elements/modal v2)
|
|
||||||
- Open from **outside** a Livewire component: `onclick="Livewire.dispatch('openModal', {component:'ui.system.modal.my-modal', arguments:{key:value}})"`
|
|
||||||
- Open from **inside** a Livewire component: `$this->dispatch('openModal', component: '...', arguments: [...])`
|
|
||||||
- **Never** use `wire:click="$dispatch('openModal',...)"` outside a Livewire component context — it won't work.
|
|
||||||
- Modal argument keys must match the `mount(int $keyName)` parameter names exactly.
|
|
||||||
- To prevent closing on backdrop/Escape, override in the modal class:
|
|
||||||
```php
|
|
||||||
public static function closeModalOnClickAway(): bool { return false; }
|
|
||||||
public static function closeModalOnEscape(): bool { return false; }
|
|
||||||
public static function closeModalOnEscapeIsForceful(): bool { return false; }
|
|
||||||
```
|
|
||||||
- To force-close the entire modal stack: `$this->forceClose()->closeModal()`
|
|
||||||
|
|
||||||
### Livewire dependency injection
|
|
||||||
- **Never** use constructor injection in Livewire components — Livewire calls `new Component()` with no args.
|
|
||||||
- Use `boot(MyService $service)` instead: this is called on every request and supports DI.
|
|
||||||
|
|
||||||
### CSS design system
|
|
||||||
The app uses a custom `mw-*` variable and class system defined in `resources/css/app.css` (Tailwind CSS v4, no `tailwind.config.js`):
|
|
||||||
|
|
||||||
**CSS variables:**
|
|
||||||
- `--mw-bg`, `--mw-bg3`, `--mw-bg4` — background layers
|
|
||||||
- `--mw-b1`, `--mw-b2`, `--mw-b3` — border shades
|
|
||||||
- `--mw-t1` through `--mw-t5` — text shades (t1 = primary, t4/t5 = muted)
|
|
||||||
- `--mw-v`, `--mw-v2`, `--mw-vbg` — purple accent (primary brand color)
|
|
||||||
- `--mw-gr` — green (success)
|
|
||||||
|
|
||||||
**Reusable component classes:** `.mw-btn-primary`, `.mw-btn-secondary`, `.mw-btn-cancel`, `.mw-btn-save`, `.mw-btn-del`, `.mbx-act-btn`, `.mbx-act-danger`, `.mw-modal-frame`, `.mw-modal-head`, `.mw-modal-body`, `.mw-modal-foot`, `.mw-modal-label`, `.mw-modal-input`, `.mw-modal-error`, `.mbx-badge-mute`, `.mbx-badge-ok`, `.mbx-badge-warn`.
|
|
||||||
|
|
||||||
### Services
|
|
||||||
`app/Services/` contains: `DkimService`, `DnsRecordService`, `ImapService`, `MailStorage`, `SandboxMailParser`, `SandboxService`, `TlsaService`, `TotpService`, `WebhookService`.
|
|
||||||
|
|
||||||
### API
|
|
||||||
REST API under `/api/v1/` uses Laravel Sanctum. Token abilities map to scopes like `mailboxes:read`, `domains:write`, etc. Defined in `routes/api.php`.
|
|
||||||
|
|
||||||
### Sandbox mail system
|
|
||||||
The mail sandbox intercepts Postfix mail via a pipe transport (`php artisan sandbox:receive`). `SandboxRoute` model controls which domains/addresses are intercepted. `SandboxService::syncTransportFile()` writes `/etc/postfix/transport.sandbox` and runs `postmap`.
|
|
||||||
|
|
||||||
### Queue & real-time
|
|
||||||
- Queue driver: database (configurable). Jobs in `app/Jobs/`.
|
|
||||||
- Real-time updates use Laravel Reverb + Pusher.js. Livewire polls (`wire:poll.5s`) are used as fallback on some pages.
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Setting;
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class WizardDomains extends Command
|
class WizardDomains extends Command
|
||||||
|
|
@ -26,6 +25,7 @@ class WizardDomains extends Command
|
||||||
|
|
||||||
@mkdir(self::STATE_DIR, 0755, true);
|
@mkdir(self::STATE_DIR, 0755, true);
|
||||||
|
|
||||||
|
// Start: alle auf pending
|
||||||
foreach (['ui', 'mail', 'webmail'] as $key) {
|
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'pending');
|
file_put_contents(self::STATE_DIR . "/{$key}", 'pending');
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +33,6 @@ class WizardDomains extends Command
|
||||||
$domains = ['ui' => $ui, 'mail' => $mail, 'webmail' => $webmail];
|
$domains = ['ui' => $ui, 'mail' => $mail, 'webmail' => $webmail];
|
||||||
$allOk = true;
|
$allOk = true;
|
||||||
|
|
||||||
// DNS prüfen
|
|
||||||
foreach ($domains as $key => $domain) {
|
foreach ($domains as $key => $domain) {
|
||||||
if (!$domain) {
|
if (!$domain) {
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'skip');
|
file_put_contents(self::STATE_DIR . "/{$key}", 'skip');
|
||||||
|
|
@ -42,75 +41,46 @@ class WizardDomains extends Command
|
||||||
|
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'running');
|
file_put_contents(self::STATE_DIR . "/{$key}", 'running');
|
||||||
|
|
||||||
|
// DNS prüfen
|
||||||
$hasDns = checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA');
|
$hasDns = checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA');
|
||||||
if (!$hasDns) {
|
if (!$hasDns) {
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'nodns');
|
file_put_contents(self::STATE_DIR . "/{$key}", 'nodns');
|
||||||
$allOk = false;
|
$allOk = false;
|
||||||
}
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$allOk) {
|
// SSL-Zertifikat anfordern
|
||||||
file_put_contents(self::STATE_DIR . '/done', '0');
|
if ($ssl) {
|
||||||
Setting::set('ssl_configured', '0');
|
|
||||||
return self::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nginx-Vhosts + optionales SSL via mailwolt-apply-domains
|
|
||||||
// Das Script erstellt erst die Vhosts (mit ACME-Location), dann certbot --webroot
|
|
||||||
$helper = '/usr/local/sbin/mailwolt-apply-domains';
|
|
||||||
$out = shell_exec(sprintf(
|
$out = shell_exec(sprintf(
|
||||||
'sudo -n %s --ui-host %s --webmail-host %s --mail-host %s --ssl-auto %d',
|
'sudo -n certbot certonly --nginx --non-interactive --agree-tos -m root@%s -d %s 2>&1',
|
||||||
|
escapeshellarg($domain),
|
||||||
|
escapeshellarg($domain)
|
||||||
|
));
|
||||||
|
$certOk = str_contains((string) $out, 'Successfully') || str_contains((string) $out, 'Certificate not yet due for renewal');
|
||||||
|
if (!$certOk) {
|
||||||
|
file_put_contents(self::STATE_DIR . "/{$key}", 'error');
|
||||||
|
$allOk = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents(self::STATE_DIR . "/{$key}", 'done');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nginx neu konfigurieren (alle Domains auf einmal)
|
||||||
|
if ($allOk) {
|
||||||
|
$helper = '/usr/local/sbin/mailwolt-apply-domains';
|
||||||
|
shell_exec(sprintf(
|
||||||
|
'sudo -n %s --ui-host %s --webmail-host %s --mail-host %s --ssl-auto %d 2>&1',
|
||||||
escapeshellarg($helper),
|
escapeshellarg($helper),
|
||||||
escapeshellarg($ui),
|
escapeshellarg($ui),
|
||||||
escapeshellarg($webmail),
|
escapeshellarg($webmail),
|
||||||
escapeshellarg($mail),
|
escapeshellarg($mail),
|
||||||
$ssl ? 1 : 0,
|
$ssl ? 1 : 0,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Shell-Script schreibt per-Domain-Status selbst in die State-Dateien.
|
|
||||||
// Fallback: Domains die noch auf running/pending stehen auf error setzen.
|
|
||||||
foreach (['ui', 'mail', 'webmail'] as $key) {
|
|
||||||
$status = trim((string) @file_get_contents(self::STATE_DIR . "/{$key}"));
|
|
||||||
if ($status === 'running' || $status === 'pending') {
|
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'error');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// done-Datei: Shell-Script schreibt "1"/"0"; Fallback wenn Script abstürzte.
|
file_put_contents(self::STATE_DIR . '/done', $allOk ? '1' : '0');
|
||||||
$doneVal = trim((string) @file_get_contents(self::STATE_DIR . '/done'));
|
|
||||||
if ($doneVal === '') {
|
|
||||||
file_put_contents(self::STATE_DIR . '/done', '0');
|
|
||||||
$doneVal = '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ssl_configured anhand tatsächlich ausgestellter LE-Zertifikate bestimmen
|
|
||||||
$hasAnyCert = false;
|
|
||||||
foreach ($domains as $domain) {
|
|
||||||
if ($domain && is_dir("/etc/letsencrypt/live/{$domain}")) {
|
|
||||||
$hasAnyCert = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Setting::set('ssl_configured', $hasAnyCert ? '1' : '0');
|
|
||||||
|
|
||||||
// SESSION_SECURE_COOKIE wird nicht automatisch gesetzt —
|
|
||||||
// nginx leitet HTTP→HTTPS weiter, Secure-Flag wird im Admin gesetzt
|
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateEnv(string $path, string $key, string $value): void
|
|
||||||
{
|
|
||||||
$content = @file_get_contents($path) ?: '';
|
|
||||||
$pattern = '/^' . preg_quote($key, '/') . '=[^\r\n]*/m';
|
|
||||||
$line = $key . '=' . $value;
|
|
||||||
|
|
||||||
if (preg_match($pattern, $content)) {
|
|
||||||
$content = preg_replace($pattern, $line, $content);
|
|
||||||
} else {
|
|
||||||
$content .= "\n{$line}";
|
|
||||||
}
|
|
||||||
|
|
||||||
file_put_contents($path, $content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ class LoginForm extends Component
|
||||||
|
|
||||||
public string $name = '';
|
public string $name = '';
|
||||||
public string $password = '';
|
public string $password = '';
|
||||||
public bool $remember = false;
|
|
||||||
public ?string $error = null;
|
public ?string $error = null;
|
||||||
public bool $show = false;
|
public bool $show = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ class Wizard extends Component
|
||||||
public string $mail_domain = '';
|
public string $mail_domain = '';
|
||||||
public string $webmail_domain = '';
|
public string $webmail_domain = '';
|
||||||
|
|
||||||
// Schritt 4 — Option
|
|
||||||
public bool $skipSsl = false;
|
|
||||||
|
|
||||||
// Schritt 3 — Admin-Account
|
// Schritt 3 — Admin-Account
|
||||||
public string $admin_name = '';
|
public string $admin_name = '';
|
||||||
public string $admin_email = '';
|
public string $admin_email = '';
|
||||||
|
|
@ -45,30 +42,14 @@ class Wizard extends Component
|
||||||
|
|
||||||
private const STATE_DIR = '/var/lib/mailwolt/wizard';
|
private const STATE_DIR = '/var/lib/mailwolt/wizard';
|
||||||
|
|
||||||
public function mount()
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->instance_name = config('app.name', 'Mailwolt');
|
$this->instance_name = config('app.name', 'Mailwolt');
|
||||||
try {
|
|
||||||
$this->timezone = Setting::get('timezone', 'Europe/Berlin');
|
$this->timezone = Setting::get('timezone', 'Europe/Berlin');
|
||||||
$this->locale = Setting::get('locale', 'de');
|
$this->locale = Setting::get('locale', 'de');
|
||||||
$this->ui_domain = Setting::get('ui_domain', '');
|
$this->ui_domain = Setting::get('ui_domain', '');
|
||||||
$this->mail_domain = Setting::get('mail_domain', '');
|
$this->mail_domain = Setting::get('mail_domain', '');
|
||||||
$this->webmail_domain = Setting::get('webmail_domain', '');
|
$this->webmail_domain = Setting::get('webmail_domain', '');
|
||||||
} catch (\Throwable) {
|
|
||||||
// DB noch nicht migriert — Standardwerte bleiben
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updatedUiDomain(): void { $this->fillEmptyDomains($this->ui_domain); }
|
|
||||||
public function updatedMailDomain(): void { $this->fillEmptyDomains($this->mail_domain); }
|
|
||||||
public function updatedWebmailDomain(): void { $this->fillEmptyDomains($this->webmail_domain); }
|
|
||||||
|
|
||||||
private function fillEmptyDomains(string $value): void
|
|
||||||
{
|
|
||||||
if ($value === '') return;
|
|
||||||
if ($this->ui_domain === '') $this->ui_domain = $value;
|
|
||||||
if ($this->mail_domain === '') $this->mail_domain = $value;
|
|
||||||
if ($this->webmail_domain === '') $this->webmail_domain = $value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function next(): void
|
public function next(): void
|
||||||
|
|
@ -94,10 +75,10 @@ class Wizard extends Component
|
||||||
3 => $this->validate([
|
3 => $this->validate([
|
||||||
'admin_name' => 'required|string|min:2|max:64',
|
'admin_name' => 'required|string|min:2|max:64',
|
||||||
'admin_email' => 'required|email|max:190',
|
'admin_email' => 'required|email|max:190',
|
||||||
'admin_password' => 'required|string|min:6|same:admin_password_confirmation',
|
'admin_password' => 'required|string|min:10|same:admin_password_confirmation',
|
||||||
'admin_password_confirmation' => 'required',
|
'admin_password_confirmation' => 'required',
|
||||||
], [
|
], [
|
||||||
'admin_password.min' => 'Mindestens 6 Zeichen.',
|
'admin_password.min' => 'Mindestens 10 Zeichen.',
|
||||||
'admin_password.same' => 'Passwörter stimmen nicht überein.',
|
'admin_password.same' => 'Passwörter stimmen nicht überein.',
|
||||||
]),
|
]),
|
||||||
default => null,
|
default => null,
|
||||||
|
|
@ -116,7 +97,7 @@ class Wizard extends Component
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'admin_name' => 'required|string|min:2|max:64',
|
'admin_name' => 'required|string|min:2|max:64',
|
||||||
'admin_email' => 'required|email|max:190',
|
'admin_email' => 'required|email|max:190',
|
||||||
'admin_password' => 'required|string|min:6|same:admin_password_confirmation',
|
'admin_password' => 'required|string|min:10|same:admin_password_confirmation',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Settings + .env speichern
|
// Settings + .env speichern
|
||||||
|
|
@ -152,7 +133,7 @@ class Wizard extends Component
|
||||||
file_put_contents(self::STATE_DIR . "/{$k}", 'pending');
|
file_put_contents(self::STATE_DIR . "/{$k}", 'pending');
|
||||||
}
|
}
|
||||||
|
|
||||||
$ssl = $this->skipSsl ? 0 : 1;
|
$ssl = app()->isProduction() ? 1 : 0;
|
||||||
$artisan = base_path('artisan');
|
$artisan = base_path('artisan');
|
||||||
$cmd = sprintf(
|
$cmd = sprintf(
|
||||||
'nohup php %s mailwolt:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=%d > /dev/null 2>&1 &',
|
'nohup php %s mailwolt:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=%d > /dev/null 2>&1 &',
|
||||||
|
|
@ -169,8 +150,6 @@ class Wizard extends Component
|
||||||
|
|
||||||
public function pollSetup(): void
|
public function pollSetup(): void
|
||||||
{
|
{
|
||||||
if ($this->setupDone) return;
|
|
||||||
|
|
||||||
foreach (['ui', 'mail', 'webmail'] as $key) {
|
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||||
$file = self::STATE_DIR . "/{$key}";
|
$file = self::STATE_DIR . "/{$key}";
|
||||||
$this->domainStatus[$key] = is_readable($file)
|
$this->domainStatus[$key] = is_readable($file)
|
||||||
|
|
@ -184,36 +163,9 @@ class Wizard extends Component
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function retryDomains(): void
|
|
||||||
{
|
|
||||||
@unlink(self::STATE_DIR . '/done');
|
|
||||||
foreach (['ui', 'mail', 'webmail'] as $k) {
|
|
||||||
file_put_contents(self::STATE_DIR . "/{$k}", 'pending');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->domainStatus = ['ui' => 'pending', 'mail' => 'pending', 'webmail' => 'pending'];
|
|
||||||
$this->setupDone = false;
|
|
||||||
|
|
||||||
$ssl = $this->skipSsl ? 0 : 1;
|
|
||||||
$artisan = base_path('artisan');
|
|
||||||
$cmd = sprintf(
|
|
||||||
'nohup php %s mailwolt:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=%d > /dev/null 2>&1 &',
|
|
||||||
escapeshellarg($artisan),
|
|
||||||
escapeshellarg($this->ui_domain),
|
|
||||||
escapeshellarg($this->mail_domain),
|
|
||||||
escapeshellarg($this->webmail_domain),
|
|
||||||
$ssl,
|
|
||||||
);
|
|
||||||
@shell_exec($cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function goToLogin(): mixed
|
public function goToLogin(): mixed
|
||||||
{
|
{
|
||||||
$sslOk = Setting::get('ssl_configured', '0') === '1' && $this->ui_domain;
|
return redirect()->route('login')->with('setup_done', true);
|
||||||
$url = $sslOk
|
|
||||||
? 'https://' . $this->ui_domain . '/login'
|
|
||||||
: '/login';
|
|
||||||
return redirect()->to($url)->with('setup_done', true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function writeEnv(array $values): void
|
private function writeEnv(array $values): void
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ use App\Models\BackupJob;
|
||||||
use App\Models\BackupPolicy;
|
use App\Models\BackupPolicy;
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Models\MailUser;
|
use App\Models\MailUser;
|
||||||
use App\Models\SandboxRoute;
|
|
||||||
use App\Models\Setting as SettingModel;
|
use App\Models\Setting as SettingModel;
|
||||||
use App\Support\CacheVer;
|
use App\Support\CacheVer;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
@ -36,16 +35,12 @@ class Dashboard extends Component
|
||||||
|
|
||||||
$servicesActive = count(array_filter($services, fn($s) => $s['status'] === 'online'));
|
$servicesActive = count(array_filter($services, fn($s) => $s['status'] === 'online'));
|
||||||
|
|
||||||
$sslConfigured = SettingModel::get('ssl_configured', '1') === '1';
|
|
||||||
|
|
||||||
return view('livewire.ui.nx.dashboard', [
|
return view('livewire.ui.nx.dashboard', [
|
||||||
'sslConfigured' => $sslConfigured,
|
|
||||||
'domainCount' => Domain::where('is_system', false)->where('is_server', false)->count(),
|
'domainCount' => Domain::where('is_system', false)->where('is_server', false)->count(),
|
||||||
'mailboxCount' => MailUser::where('is_system', false)->where('is_active', true)->count(),
|
'mailboxCount' => MailUser::where('is_system', false)->where('is_active', true)->count(),
|
||||||
'servicesActive' => $servicesActive,
|
'servicesActive' => $servicesActive,
|
||||||
'servicesTotal' => count($services),
|
'servicesTotal' => count($services),
|
||||||
'alertCount' => SandboxRoute::where('is_active', true)->count(),
|
'alertCount' => 0,
|
||||||
'sandboxAlerts' => SandboxRoute::activeRoutes(),
|
|
||||||
'backup' => $this->backupData(),
|
'backup' => $this->backupData(),
|
||||||
'mailHostname' => gethostname() ?: 'mailserver',
|
'mailHostname' => gethostname() ?: 'mailserver',
|
||||||
'services' => $services,
|
'services' => $services,
|
||||||
|
|
|
||||||
|
|
@ -126,20 +126,21 @@ class SearchPaletteModal extends ModalComponent
|
||||||
|
|
||||||
public function go(string $type, int $id): void
|
public function go(string $type, int $id): void
|
||||||
{
|
{
|
||||||
if ($type === 'domain') {
|
// Schließe die Palette …
|
||||||
$component = 'ui.domain.modal.domain-edit-modal';
|
$this->dispatch('closeModal');
|
||||||
$arguments = ['domainId' => $id];
|
|
||||||
} elseif ($type === 'mailbox') {
|
|
||||||
$component = 'ui.mail.modal.mailbox-edit-modal';
|
|
||||||
$arguments = ['mailboxId' => $id];
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search palette schließen, dann nach dem State-Reset (300 ms) das Ziel-Modal öffnen
|
// … und navigiere / öffne Kontext:
|
||||||
$this->forceClose()->closeModal();
|
// - Domain → scrolle/markiere Domainkarte
|
||||||
$payload = json_encode(['component' => $component, 'arguments' => $arguments]);
|
// - Mailbox → öffne Bearbeiten-Modal
|
||||||
$this->js("setTimeout(()=>Livewire.dispatch('openModal',{$payload}),350)");
|
// Passe an, was du bevorzugst:
|
||||||
|
if ($type === 'domain') {
|
||||||
|
$this->dispatch('focus:domain', id: $id);
|
||||||
|
} elseif ($type === 'mailbox') {
|
||||||
|
// direkt Edit-Modal auf
|
||||||
|
$this->dispatch('openModal', component:'ui.mail.modal.mailbox-edit-modal', arguments: [$id]);
|
||||||
|
} elseif ($type === 'user') {
|
||||||
|
$this->dispatch('focus:user', id: $id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function modalMaxWidth(): string
|
public static function modalMaxWidth(): string
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,6 @@ class ApiKeyCreateModal extends ModalComponent
|
||||||
'domains:write' => 'Domains schreiben',
|
'domains:write' => 'Domains schreiben',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static function closeModalOnClickAway(): bool { return false; }
|
|
||||||
public static function closeModalOnEscape(): bool { return false; }
|
|
||||||
public static function closeModalOnEscapeIsForceful(): bool { return false; }
|
|
||||||
|
|
||||||
public function create(): void
|
public function create(): void
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
|
|
@ -35,7 +31,10 @@ class ApiKeyCreateModal extends ModalComponent
|
||||||
'selected.min' => 'Bitte mindestens einen Scope auswählen.',
|
'selected.min' => 'Bitte mindestens einen Scope auswählen.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$token = Auth::user()->createToken($this->name, $this->selected);
|
$token = Auth::user()->createToken(
|
||||||
|
$this->name,
|
||||||
|
$this->selected,
|
||||||
|
);
|
||||||
|
|
||||||
$pat = $token->accessToken;
|
$pat = $token->accessToken;
|
||||||
if ($this->sandbox) {
|
if ($this->sandbox) {
|
||||||
|
|
@ -44,10 +43,7 @@ class ApiKeyCreateModal extends ModalComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->dispatch('token-created', plainText: $token->plainTextToken);
|
$this->dispatch('token-created', plainText: $token->plainTextToken);
|
||||||
$this->dispatch('openModal',
|
$this->closeModal();
|
||||||
component: 'ui.system.modal.api-key-show-modal',
|
|
||||||
arguments: ['plainText' => $token->plainTextToken],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleAll(): void
|
public function toggleAll(): void
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\Ui\System\Modal;
|
|
||||||
|
|
||||||
use App\Models\PersonalAccessToken;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use LivewireUI\Modal\ModalComponent;
|
|
||||||
|
|
||||||
class ApiKeyDeleteModal extends ModalComponent
|
|
||||||
{
|
|
||||||
public int $tokenId;
|
|
||||||
public string $tokenName = '';
|
|
||||||
|
|
||||||
public function mount(int $tokenId): void
|
|
||||||
{
|
|
||||||
$token = PersonalAccessToken::where('tokenable_id', Auth::id())
|
|
||||||
->where('tokenable_type', Auth::user()::class)
|
|
||||||
->findOrFail($tokenId);
|
|
||||||
|
|
||||||
$this->tokenId = $tokenId;
|
|
||||||
$this->tokenName = $token->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(): void
|
|
||||||
{
|
|
||||||
PersonalAccessToken::where('tokenable_id', Auth::id())
|
|
||||||
->where('tokenable_type', Auth::user()::class)
|
|
||||||
->findOrFail($this->tokenId)
|
|
||||||
->delete();
|
|
||||||
|
|
||||||
$this->dispatch('toast', type: 'done', badge: 'API Key',
|
|
||||||
title: 'Gelöscht', text: "Key <b>{$this->tokenName}</b> wurde entfernt.", duration: 4000);
|
|
||||||
|
|
||||||
$this->dispatch('token-deleted');
|
|
||||||
$this->closeModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
return view('livewire.ui.system.modal.api-key-delete-modal');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\Ui\System\Modal;
|
|
||||||
|
|
||||||
use App\Models\PersonalAccessToken;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use LivewireUI\Modal\ModalComponent;
|
|
||||||
|
|
||||||
class ApiKeyScopesModal extends ModalComponent
|
|
||||||
{
|
|
||||||
public string $tokenName = '';
|
|
||||||
public array $scopes = [];
|
|
||||||
|
|
||||||
public function mount(int $tokenId): void
|
|
||||||
{
|
|
||||||
$token = PersonalAccessToken::where('tokenable_id', Auth::id())
|
|
||||||
->where('tokenable_type', Auth::user()::class)
|
|
||||||
->findOrFail($tokenId);
|
|
||||||
|
|
||||||
$this->tokenName = $token->name;
|
|
||||||
$this->scopes = $token->abilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
return view('livewire.ui.system.modal.api-key-scopes-modal');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,18 +13,9 @@ class ApiKeyShowModal extends ModalComponent
|
||||||
$this->plainText = $plainText;
|
$this->plainText = $plainText;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function closeModalOnClickAway(): bool { return false; }
|
|
||||||
public static function closeModalOnEscape(): bool { return false; }
|
|
||||||
public static function closeModalOnEscapeIsForceful(): bool { return false; }
|
|
||||||
|
|
||||||
public function dismiss(): void
|
|
||||||
{
|
|
||||||
$this->forceClose()->closeModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function modalMaxWidth(): string
|
public static function modalMaxWidth(): string
|
||||||
{
|
{
|
||||||
return '2xl';
|
return 'md';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\Ui\System;
|
|
||||||
|
|
||||||
use App\Models\SandboxRoute;
|
|
||||||
use App\Services\SandboxService;
|
|
||||||
use Livewire\Attributes\Computed;
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
class SandboxRoutes extends Component
|
|
||||||
{
|
|
||||||
public string $newType = 'domain';
|
|
||||||
public string $newTarget = '';
|
|
||||||
public ?string $syncMessage = null;
|
|
||||||
public bool $syncOk = false;
|
|
||||||
|
|
||||||
public function boot(SandboxService $sandboxService): void
|
|
||||||
{
|
|
||||||
$this->sandboxService = $sandboxService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addRoute(): void
|
|
||||||
{
|
|
||||||
if ($this->newType !== 'global') {
|
|
||||||
$this->validate(['newTarget' => 'required|string|max:255']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$target = $this->newType === 'global' ? null : trim($this->newTarget);
|
|
||||||
$this->sandboxService->enable($this->newType, $target);
|
|
||||||
$this->sandboxService->syncTransportFile();
|
|
||||||
$this->dispatch('sandbox-route-changed');
|
|
||||||
$this->newTarget = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function removeRoute(int $id): void
|
|
||||||
{
|
|
||||||
$this->sandboxService->delete($id);
|
|
||||||
$this->sandboxService->syncTransportFile();
|
|
||||||
$this->dispatch('sandbox-route-changed');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toggleRoute(int $id): void
|
|
||||||
{
|
|
||||||
$route = SandboxRoute::findOrFail($id);
|
|
||||||
$route->is_active = !$route->is_active;
|
|
||||||
$route->save();
|
|
||||||
$this->sandboxService->syncTransportFile();
|
|
||||||
$this->dispatch('sandbox-route-changed');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function syncPostfix(): void
|
|
||||||
{
|
|
||||||
$result = $this->sandboxService->syncTransportFile();
|
|
||||||
$this->syncOk = $result['ok'];
|
|
||||||
$this->syncMessage = $result['message'];
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Computed]
|
|
||||||
public function routes()
|
|
||||||
{
|
|
||||||
return SandboxRoute::orderBy('type')->orderBy('target')->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
return view('livewire.ui.system.sandbox-routes');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -306,7 +306,6 @@ class SettingsForm extends Component
|
||||||
\Illuminate\Support\Facades\Log::info('mailwolt-apply-domains', ['output' => $output]);
|
\Illuminate\Support\Facades\Log::info('mailwolt-apply-domains', ['output' => $output]);
|
||||||
|
|
||||||
if ($ok) {
|
if ($ok) {
|
||||||
Setting::set('ssl_configured', '1');
|
|
||||||
$this->dispatch('toast', type: 'done', badge: 'Nginx',
|
$this->dispatch('toast', type: 'done', badge: 'Nginx',
|
||||||
title: 'Nginx aktualisiert',
|
title: 'Nginx aktualisiert',
|
||||||
text: 'Nginx-Konfiguration wurde neu geladen.',
|
text: 'Nginx-Konfiguration wurde neu geladen.',
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ class UpdatePage extends Component
|
||||||
$rcRaw = @trim(@file_get_contents(self::STATE_DIR . '/rc') ?: '');
|
$rcRaw = @trim(@file_get_contents(self::STATE_DIR . '/rc') ?: '');
|
||||||
|
|
||||||
$this->lowState = $state !== '' ? $state : null;
|
$this->lowState = $state !== '' ? $state : null;
|
||||||
$this->running = ($this->lowState === 'running');
|
$this->running = ($this->lowState !== 'done');
|
||||||
$this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int) $rcRaw : null;
|
$this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int) $rcRaw : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,13 +255,15 @@ class UpdatePage extends Component
|
||||||
|
|
||||||
protected function readCurrentVersion(): ?string
|
protected function readCurrentVersion(): ?string
|
||||||
{
|
{
|
||||||
$v = @trim(@file_get_contents(self::VERSION_FILE) ?: '');
|
// Lokal: direkt aus git describe lesen damit Entwicklungsumgebung immer aktuell ist
|
||||||
if ($v !== '') return $v;
|
if (app()->isLocal()) {
|
||||||
|
|
||||||
// Fallback: git tag (lokal immer, production wenn Datei fehlt)
|
|
||||||
$tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null'));
|
$tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null'));
|
||||||
$v = $this->normalizeVersion($tag);
|
$v = $this->normalizeVersion($tag);
|
||||||
if ($v) return $v;
|
if ($v) return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = @trim(@file_get_contents(self::VERSION_FILE) ?: '');
|
||||||
|
if ($v !== '') return $v;
|
||||||
|
|
||||||
$raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: '');
|
$raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: '');
|
||||||
if ($raw !== '') return $this->normalizeVersion($raw);
|
if ($raw !== '') return $this->normalizeVersion($raw);
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
|
|
||||||
class SandboxRoute extends Model
|
|
||||||
{
|
|
||||||
protected $fillable = ['type', 'target', 'is_active'];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All active routes ordered by type and target.
|
|
||||||
*/
|
|
||||||
public static function activeRoutes(): Collection
|
|
||||||
{
|
|
||||||
return static::where('is_active', true)
|
|
||||||
->orderBy('type')
|
|
||||||
->orderBy('target')
|
|
||||||
->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether sandbox is active for a given domain.
|
|
||||||
* True if global sandbox is active OR domain-specific rule is active.
|
|
||||||
*/
|
|
||||||
public static function isActiveForDomain(string $domain): bool
|
|
||||||
{
|
|
||||||
return static::where('is_active', true)
|
|
||||||
->where(function ($q) use ($domain) {
|
|
||||||
$q->where('type', 'global')
|
|
||||||
->orWhere(function ($q2) use ($domain) {
|
|
||||||
$q2->where('type', 'domain')->where('target', $domain);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
->exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether sandbox is active for a given address.
|
|
||||||
* True if global is active OR domain matches OR address-specific rule exists.
|
|
||||||
*/
|
|
||||||
public static function isActiveForAddress(string $address): bool
|
|
||||||
{
|
|
||||||
$domain = substr(strrchr($address, '@'), 1) ?: '';
|
|
||||||
|
|
||||||
return static::where('is_active', true)
|
|
||||||
->where(function ($q) use ($address, $domain) {
|
|
||||||
$q->where('type', 'global')
|
|
||||||
->orWhere(function ($q2) use ($domain) {
|
|
||||||
$q2->where('type', 'domain')->where('target', $domain);
|
|
||||||
})
|
|
||||||
->orWhere(function ($q3) use ($address) {
|
|
||||||
$q3->where('type', 'address')->where('target', $address);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
->exists();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use App\Models\SandboxRoute;
|
|
||||||
|
|
||||||
class SandboxService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Enable (or create) a sandbox route.
|
|
||||||
*/
|
|
||||||
public function enable(string $type, ?string $target): SandboxRoute
|
|
||||||
{
|
|
||||||
return SandboxRoute::updateOrCreate(
|
|
||||||
['type' => $type, 'target' => $target],
|
|
||||||
['is_active' => true]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable a sandbox route by id.
|
|
||||||
*/
|
|
||||||
public function disable(int $id): void
|
|
||||||
{
|
|
||||||
SandboxRoute::findOrFail($id)->update(['is_active' => false]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a sandbox route by id.
|
|
||||||
*/
|
|
||||||
public function delete(int $id): void
|
|
||||||
{
|
|
||||||
SandboxRoute::findOrFail($id)->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write /etc/postfix/transport.sandbox and run postmap.
|
|
||||||
* Returns ['ok' => bool, 'message' => string].
|
|
||||||
*/
|
|
||||||
public function syncTransportFile(): array
|
|
||||||
{
|
|
||||||
$file = config('sandbox.transport_file', '/etc/postfix/transport.sandbox');
|
|
||||||
|
|
||||||
$routes = SandboxRoute::where('is_active', true)
|
|
||||||
->orderBy('type')
|
|
||||||
->orderBy('target')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$lines = [];
|
|
||||||
$hasGlobal = false;
|
|
||||||
|
|
||||||
foreach ($routes as $route) {
|
|
||||||
if ($route->type === 'global') {
|
|
||||||
$hasGlobal = true;
|
|
||||||
} elseif ($route->type === 'domain') {
|
|
||||||
$lines[] = $route->target . ' sandbox:';
|
|
||||||
} elseif ($route->type === 'address') {
|
|
||||||
$lines[] = $route->target . ' sandbox:';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global catch-all goes at the end
|
|
||||||
if ($hasGlobal) {
|
|
||||||
$lines[] = '* sandbox:';
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = implode("\n", $lines);
|
|
||||||
if ($lines) {
|
|
||||||
$content .= "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
file_put_contents($file, $content);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return ['ok' => false, 'message' => 'Fehler beim Schreiben: ' . $e->getMessage()];
|
|
||||||
}
|
|
||||||
|
|
||||||
$out = [];
|
|
||||||
$code = 0;
|
|
||||||
exec('postmap ' . escapeshellarg($file) . ' 2>&1', $out, $code);
|
|
||||||
|
|
||||||
if ($code !== 0) {
|
|
||||||
return ['ok' => false, 'message' => 'postmap Fehler: ' . implode(' ', $out)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['ok' => true, 'message' => 'Transport-Datei synchronisiert (' . count($lines) . ' Einträge).'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,10 +22,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||||
->name('ui.webmail.')
|
->name('ui.webmail.')
|
||||||
->group(base_path('routes/webmail.php'));
|
->group(base_path('routes/webmail.php'));
|
||||||
|
|
||||||
// Path-based fallback mit eigenem Namen-Prefix — kein Konflikt mit web.php 'login'
|
// Path-based fallback (no names — avoids duplicate-name conflict)
|
||||||
Route::middleware('web')
|
Route::middleware('web')
|
||||||
->prefix('webmail')
|
->prefix('webmail')
|
||||||
->name('webmail.')
|
|
||||||
->group(base_path('routes/webmail.php'));
|
->group(base_path('routes/webmail.php'));
|
||||||
} else {
|
} else {
|
||||||
Route::middleware('web')
|
Route::middleware('web')
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('sandbox_routes', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->enum('type', ['global', 'domain', 'address'])->default('domain');
|
|
||||||
$table->string('target')->nullable(); // domain or email, null = global
|
|
||||||
$table->boolean('is_active')->default(true);
|
|
||||||
$table->timestamps();
|
|
||||||
$table->unique(['type', 'target']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('sandbox_routes');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
724
installer.sh
724
installer.sh
|
|
@ -44,10 +44,6 @@ NODE_SETUP="${NODE_SETUP:-deb}"
|
||||||
GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; CYAN="\033[1;36m"; GREY="\033[0;90m"; NC="\033[0m"
|
GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; CYAN="\033[1;36m"; GREY="\033[0;90m"; NC="\033[0m"
|
||||||
BAR="──────────────────────────────────────────────────────────────────────────────"
|
BAR="──────────────────────────────────────────────────────────────────────────────"
|
||||||
|
|
||||||
# ===== Install-Log =====
|
|
||||||
LOG_FILE="/var/log/mailwolt-install.log"
|
|
||||||
> "$LOG_FILE"
|
|
||||||
|
|
||||||
header() {
|
header() {
|
||||||
echo -e "${CYAN}${BAR}${NC}"
|
echo -e "${CYAN}${BAR}${NC}"
|
||||||
echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}"
|
echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}"
|
||||||
|
|
@ -67,72 +63,27 @@ footer_ok() {
|
||||||
local ip="$1"
|
local ip="$1"
|
||||||
local app_name="${2:-$APP_NAME}"
|
local app_name="${2:-$APP_NAME}"
|
||||||
local app_dir="${3:-$APP_DIR}"
|
local app_dir="${3:-$APP_DIR}"
|
||||||
local cert_dir="${4:-$CERT_DIR}"
|
local nginx_site="${4:-$NGINX_SITE}"
|
||||||
|
local cert_dir="${5:-$CERT_DIR}"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo -e "${GREEN}${BAR}${NC}"
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
echo -e "${GREEN} ✔ ${app_name} Installation erfolgreich abgeschlossen${NC}"
|
echo -e "${GREEN} ✔ ${app_name} Bootstrap erfolgreich abgeschlossen${NC}"
|
||||||
echo -e "${GREEN}${BAR}${NC}"
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
echo -e ""
|
echo -e " Aufruf: ${CYAN}http://${ip}${NC} ${GREY}| https://${ip}${NC}"
|
||||||
echo -e " ${CYAN}➜ Setup-Wizard jetzt öffnen:${NC}"
|
|
||||||
echo -e " ${CYAN}http://${ip}/setup${NC}"
|
|
||||||
echo -e ""
|
|
||||||
echo -e " Laravel Root: ${GREY}${app_dir}${NC}"
|
echo -e " Laravel Root: ${GREY}${app_dir}${NC}"
|
||||||
echo -e " Mail-TLS Cert: ${GREY}${cert_dir}/{cert.pem,key.pem}${NC} (Postfix/Dovecot)"
|
echo -e " Nginx Site: ${GREY}${nginx_site}${NC}"
|
||||||
echo -e " Postfix/Dovecot: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}"
|
echo -e " Self-signed Cert: ${GREY}${cert_dir}/{cert.pem,key.pem}${NC}"
|
||||||
|
echo -e " Postfix/Dovecot Ports aktiv: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}"
|
||||||
|
echo -e " Rspamd/OpenDKIM: ${GREY}aktiv (DKIM-Keys später im Wizard)${NC}"
|
||||||
|
echo -e " Monit (Watchdog): ${GREY}installiert, NICHT aktiviert${NC}"
|
||||||
echo -e "${GREEN}${BAR}${NC}"
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
_STEP_T=0
|
log() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||||
_SPIN_PID=
|
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||||
|
err() { echo -e "${RED}[x]${NC} $*"; }
|
||||||
_spin_bg() {
|
|
||||||
local chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
|
||||||
local n=${#chars} i=0
|
|
||||||
while true; do
|
|
||||||
printf "\r \033[0;90m${chars:$i:1}\033[0m"
|
|
||||||
i=$(( (i + 1) % n ))
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
}
|
|
||||||
start_spin() { _spin_bg & _SPIN_PID=$!; }
|
|
||||||
stop_spin() {
|
|
||||||
[[ -n "${_SPIN_PID:-}" ]] || return 0
|
|
||||||
kill "$_SPIN_PID" 2>/dev/null || true
|
|
||||||
wait "$_SPIN_PID" 2>/dev/null || true
|
|
||||||
_SPIN_PID=
|
|
||||||
printf "\r\033[K"
|
|
||||||
}
|
|
||||||
trap 'stop_spin' EXIT INT TERM
|
|
||||||
|
|
||||||
step() {
|
|
||||||
local msg="$1" dur="${2:-}"
|
|
||||||
[ -n "$dur" ] && dur=" ${GREY}(~${dur})${NC}" || dur=""
|
|
||||||
printf "\n${CYAN} ▸${NC} %-42s%b\n" "$msg" "$dur"
|
|
||||||
_STEP_T=$SECONDS
|
|
||||||
}
|
|
||||||
ok() {
|
|
||||||
stop_spin
|
|
||||||
local t=$(( SECONDS - _STEP_T ))
|
|
||||||
[ $t -gt 1 ] && printf " ${GREEN}✔${NC} ${GREY}%ds${NC}\n" $t || printf " ${GREEN}✔${NC}\n"
|
|
||||||
}
|
|
||||||
warn() { stop_spin; printf " ${YELLOW}⚠${NC} %s\n" "$*"; }
|
|
||||||
err() { stop_spin; printf " ${RED}✗${NC} %s\n" "$*"; }
|
|
||||||
|
|
||||||
quietly() {
|
|
||||||
start_spin
|
|
||||||
if ! "$@" >> "$LOG_FILE" 2>&1; then
|
|
||||||
stop_spin
|
|
||||||
printf " ${RED}✗${NC} Fehlgeschlagen. Letzte Log-Zeilen:\n\n"
|
|
||||||
tail -20 "$LOG_FILE" | sed 's/^/ /'
|
|
||||||
printf "\n ${GREY}Vollständiges Log: %s${NC}\n\n" "$LOG_FILE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
stop_spin
|
|
||||||
}
|
|
||||||
try_quiet() { "$@" >> "$LOG_FILE" 2>&1 || true; }
|
|
||||||
log() { :; }
|
|
||||||
require_root() { [ "$(id -u)" -eq 0 ] || { err "Bitte als root ausführen."; exit 1; }; }
|
require_root() { [ "$(id -u)" -eq 0 ] || { err "Bitte als root ausführen."; exit 1; }; }
|
||||||
|
|
||||||
# ===== IP ermitteln =====
|
# ===== IP ermitteln =====
|
||||||
|
|
@ -149,37 +100,30 @@ gen() { head -c 512 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c "${1:-28}" ||
|
||||||
pw() { gen 28; }
|
pw() { gen 28; }
|
||||||
short() { gen 16; }
|
short() { gen 16; }
|
||||||
|
|
||||||
# ===== Argument-Parsing =====
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
-dev) APP_ENV="local"; APP_DEBUG="true" ;;
|
|
||||||
-stag|-staging) APP_ENV="staging"; APP_DEBUG="false" ;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
# ===== Start =====
|
# ===== Start =====
|
||||||
require_root
|
require_root
|
||||||
header
|
header
|
||||||
|
|
||||||
SERVER_IP="$(detect_ip)"
|
SERVER_IP="$(detect_ip)"
|
||||||
APP_PW="${APP_PW:-$(pw)}"
|
|
||||||
MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}" # Wizard setzt später FQDN
|
MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}" # Wizard setzt später FQDN
|
||||||
TZ="${TZ:-""}" # leer; Wizard setzt final
|
TZ="${TZ:-""}" # leer; Wizard setzt final
|
||||||
|
|
||||||
echo -e "\n ${GREY}Server-IP: ${SERVER_IP} Log: ${LOG_FILE}${NC}"
|
echo -e "${GREY}Server-IP erkannt: ${SERVER_IP}${NC}"
|
||||||
[ -n "$TZ" ] && { ln -fs "/usr/share/zoneinfo/${TZ}" /etc/localtime || true; }
|
[ -n "$TZ" ] && { ln -fs "/usr/share/zoneinfo/${TZ}" /etc/localtime || true; }
|
||||||
|
|
||||||
step "Paketquellen aktualisieren" "10 Sek"
|
log "Paketquellen aktualisieren…"
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
quietly apt-get update -y
|
apt-get update -y
|
||||||
ok
|
|
||||||
|
|
||||||
|
# ---- MariaDB-Workaround (fix für mariadb-common prompt) ----
|
||||||
|
log "MariaDB-Workaround vorbereiten…"
|
||||||
mkdir -p /etc/mysql /etc/mysql/mariadb.conf.d
|
mkdir -p /etc/mysql /etc/mysql/mariadb.conf.d
|
||||||
[ -f /etc/mysql/mariadb.cnf ] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf
|
[ -f /etc/mysql/mariadb.cnf ] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf
|
||||||
|
|
||||||
step "Pakete installieren" "2–5 Min"
|
# ---- Basis-Pakete installieren ----
|
||||||
quietly apt-get -y -o Dpkg::Options::="--force-confdef" \
|
log "Pakete installieren… (dies kann einige Minuten dauern)"
|
||||||
|
#apt-get install -y \
|
||||||
|
apt-get -y -o Dpkg::Options::="--force-confdef" \
|
||||||
-o Dpkg::Options::="--force-confold" install \
|
-o Dpkg::Options::="--force-confold" install \
|
||||||
postfix postfix-mysql \
|
postfix postfix-mysql \
|
||||||
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
|
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
|
||||||
|
|
@ -188,20 +132,20 @@ quietly apt-get -y -o Dpkg::Options::="--force-confdef" \
|
||||||
rspamd \
|
rspamd \
|
||||||
opendkim opendkim-tools \
|
opendkim opendkim-tools \
|
||||||
nginx \
|
nginx \
|
||||||
php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql php-redis php-gd php-sqlite3 unzip curl acl \
|
php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql php-redis php-gd unzip curl \
|
||||||
composer \
|
composer \
|
||||||
certbot python3-certbot-nginx \
|
certbot python3-certbot-nginx \
|
||||||
fail2ban \
|
fail2ban \
|
||||||
ca-certificates rsyslog sudo openssl netcat-openbsd monit git
|
ca-certificates rsyslog sudo openssl netcat-openbsd monit
|
||||||
ok
|
|
||||||
|
|
||||||
step "Benutzer & Verzeichnisse anlegen" "5 Sek"
|
# ===== Verzeichnisse / User =====
|
||||||
|
log "Verzeichnisse und Benutzer anlegen…"
|
||||||
mkdir -p ${CERT_DIR} /etc/postfix /etc/dovecot/conf.d /etc/rspamd/local.d /var/mail/vhosts
|
mkdir -p ${CERT_DIR} /etc/postfix /etc/dovecot/conf.d /etc/rspamd/local.d /var/mail/vhosts
|
||||||
quietly bash -c "id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail"
|
id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
|
||||||
chown -R vmail:vmail /var/mail
|
chown -R vmail:vmail /var/mail
|
||||||
quietly bash -c "id '$APP_USER' >/dev/null 2>&1 || adduser --disabled-password --gecos '' '$APP_USER'"
|
|
||||||
quietly usermod -a -G www-data "$APP_USER"
|
id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
|
||||||
ok
|
usermod -a -G www-data "$APP_USER"
|
||||||
|
|
||||||
# ===== Self-signed TLS (SAN = IP) =====
|
# ===== Self-signed TLS (SAN = IP) =====
|
||||||
CERT="${CERT_DIR}/cert.pem"
|
CERT="${CERT_DIR}/cert.pem"
|
||||||
|
|
@ -209,7 +153,7 @@ KEY="${CERT_DIR}/key.pem"
|
||||||
OSSL_CFG="${CERT_DIR}/openssl.cnf"
|
OSSL_CFG="${CERT_DIR}/openssl.cnf"
|
||||||
|
|
||||||
if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then
|
if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then
|
||||||
step "Self-Signed Zertifikat erstellen" "5 Sek"
|
log "Erzeuge Self-Signed TLS Zertifikat (SAN=IP:${SERVER_IP})…"
|
||||||
cat > "$OSSL_CFG" <<CFG
|
cat > "$OSSL_CFG" <<CFG
|
||||||
[req]
|
[req]
|
||||||
default_bits = 2048
|
default_bits = 2048
|
||||||
|
|
@ -229,26 +173,26 @@ subjectAltName = @alt_names
|
||||||
[alt_names]
|
[alt_names]
|
||||||
IP.1 = ${SERVER_IP}
|
IP.1 = ${SERVER_IP}
|
||||||
CFG
|
CFG
|
||||||
quietly openssl req -x509 -newkey rsa:2048 -days 825 -nodes \
|
openssl req -x509 -newkey rsa:2048 -days 825 -nodes \
|
||||||
-keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
|
-keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
|
||||||
chmod 600 "$KEY"; chmod 644 "$CERT"
|
chmod 600 "$KEY"; chmod 644 "$CERT"
|
||||||
ok
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ===== MariaDB vorbereiten =====
|
# ===== MariaDB vorbereiten =====
|
||||||
step "Datenbank einrichten" "10 Sek"
|
log "MariaDB vorbereiten…"
|
||||||
quietly systemctl enable --now mariadb
|
systemctl enable --now mariadb
|
||||||
|
DB_NAME="${DB_USER}"
|
||||||
|
DB_USER="${DB_USER}"
|
||||||
DB_PASS="$(pw)"
|
DB_PASS="$(pw)"
|
||||||
quietly mysql -uroot <<SQL
|
mysql -uroot <<SQL
|
||||||
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
||||||
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
|
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
|
||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
SQL
|
SQL
|
||||||
ok
|
|
||||||
|
|
||||||
# ===== Postfix konfigurieren (25/465/587) =====
|
# ===== Postfix konfigurieren (25/465/587) =====
|
||||||
step "Mailserver konfigurieren (Postfix / Dovecot / Rspamd)" "15 Sek"
|
log "Postfix konfigurieren…"
|
||||||
postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
||||||
postconf -e "myorigin = \$myhostname"
|
postconf -e "myorigin = \$myhostname"
|
||||||
postconf -e "mydestination = "
|
postconf -e "mydestination = "
|
||||||
|
|
@ -327,9 +271,10 @@ CONF
|
||||||
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
|
||||||
try_quiet systemctl enable --now postfix
|
systemctl enable --now postfix
|
||||||
|
|
||||||
# ===== Dovecot konfigurieren (IMAP/POP3 + SSL) =====
|
# ===== Dovecot konfigurieren (IMAP/POP3 + SSL) =====
|
||||||
|
log "Dovecot konfigurieren…"
|
||||||
cat > /etc/dovecot/dovecot.conf <<'CONF'
|
cat > /etc/dovecot/dovecot.conf <<'CONF'
|
||||||
!include_try /etc/dovecot/conf.d/*.conf
|
!include_try /etc/dovecot/conf.d/*.conf
|
||||||
CONF
|
CONF
|
||||||
|
|
@ -405,14 +350,15 @@ ssl_cert = <${CERT}
|
||||||
ssl_key = <${KEY}
|
ssl_key = <${KEY}
|
||||||
CONF
|
CONF
|
||||||
|
|
||||||
try_quiet systemctl enable --now dovecot
|
systemctl enable --now dovecot
|
||||||
|
|
||||||
# ===== Rspamd & OpenDKIM =====
|
# ===== Rspamd & OpenDKIM =====
|
||||||
|
log "Rspamd + OpenDKIM aktivieren…"
|
||||||
cat > /etc/rspamd/local.d/worker-controller.inc <<'CONF'
|
cat > /etc/rspamd/local.d/worker-controller.inc <<'CONF'
|
||||||
password = "admin";
|
password = "admin";
|
||||||
bind_socket = "127.0.0.1:11334";
|
bind_socket = "127.0.0.1:11334";
|
||||||
CONF
|
CONF
|
||||||
try_quiet systemctl enable --now rspamd
|
systemctl enable --now rspamd || true
|
||||||
|
|
||||||
cat > /etc/opendkim.conf <<'CONF'
|
cat > /etc/opendkim.conf <<'CONF'
|
||||||
Syslog yes
|
Syslog yes
|
||||||
|
|
@ -428,17 +374,17 @@ LogWhy yes
|
||||||
OversignHeaders From
|
OversignHeaders From
|
||||||
# KeyTable / SigningTable später im Wizard
|
# KeyTable / SigningTable später im Wizard
|
||||||
CONF
|
CONF
|
||||||
try_quiet systemctl enable --now opendkim
|
systemctl enable --now opendkim || true
|
||||||
try_quiet systemctl enable --now redis-server
|
|
||||||
ok
|
# ===== Redis =====
|
||||||
|
systemctl enable --now redis-server
|
||||||
|
|
||||||
# ===== Nginx: Laravel vHost (80/443) =====
|
# ===== Nginx: Laravel vHost (80/443) =====
|
||||||
step "Webserver konfigurieren (Nginx)" "5 Sek"
|
log "Nginx konfigurieren…"
|
||||||
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
|
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
|
||||||
|
|
||||||
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
|
PHP_FPM_SOCK="/run/php/php-fpm.sock"
|
||||||
PHP_FPM_SOCK="/run/php/php${PHPV}-fpm.sock"
|
[ -S "/run/php/php8.2-fpm.sock" ] && PHP_FPM_SOCK="/run/php/php8.2-fpm.sock"
|
||||||
[ -S "$PHP_FPM_SOCK" ] || PHP_FPM_SOCK="/run/php/php-fpm.sock"
|
|
||||||
|
|
||||||
cat > ${NGINX_SITE} <<CONF
|
cat > ${NGINX_SITE} <<CONF
|
||||||
server {
|
server {
|
||||||
|
|
@ -452,10 +398,6 @@ server {
|
||||||
access_log /var/log/nginx/${APP_USER}_access.log;
|
access_log /var/log/nginx/${APP_USER}_access.log;
|
||||||
error_log /var/log/nginx/${APP_USER}_error.log;
|
error_log /var/log/nginx/${APP_USER}_error.log;
|
||||||
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
|
||||||
root /var/www/letsencrypt;
|
|
||||||
try_files \$uri =404;
|
|
||||||
}
|
|
||||||
location / {
|
location / {
|
||||||
try_files \$uri \$uri/ /index.php?\$query_string;
|
try_files \$uri \$uri/ /index.php?\$query_string;
|
||||||
}
|
}
|
||||||
|
|
@ -464,7 +406,38 @@ server {
|
||||||
fastcgi_pass unix:${PHP_FPM_SOCK};
|
fastcgi_pass unix:${PHP_FPM_SOCK};
|
||||||
}
|
}
|
||||||
location ^~ /livewire/ {
|
location ^~ /livewire/ {
|
||||||
try_files \$uri /index.php?\$query_string;
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ {
|
||||||
|
expires 30d;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
ssl_certificate ${CERT};
|
||||||
|
ssl_certificate_key ${KEY};
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
|
||||||
|
root ${APP_DIR}/public;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/${APP_USER}_ssl_access.log;
|
||||||
|
error_log /var/log/nginx/${APP_USER}_ssl_error.log;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files \$uri \$uri/ /index.php?\$query_string;
|
||||||
|
}
|
||||||
|
location ~ \.php\$ {
|
||||||
|
include snippets/fastcgi-php.conf;
|
||||||
|
fastcgi_pass unix:${PHP_FPM_SOCK};
|
||||||
|
}
|
||||||
|
location ^~ /livewire/ {
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
}
|
}
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ {
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ {
|
||||||
expires 30d;
|
expires 30d;
|
||||||
|
|
@ -473,26 +446,36 @@ server {
|
||||||
}
|
}
|
||||||
CONF
|
CONF
|
||||||
ln -sf ${NGINX_SITE} ${NGINX_SITE_LINK}
|
ln -sf ${NGINX_SITE} ${NGINX_SITE_LINK}
|
||||||
try_quiet nginx -t
|
nginx -t && systemctl enable --now nginx
|
||||||
try_quiet systemctl enable --now nginx
|
|
||||||
ok
|
|
||||||
|
|
||||||
step "Projekt herunterladen (Git)" "15 Sek"
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-dev)
|
||||||
|
APP_ENV="local"
|
||||||
|
APP_DEBUG="true"
|
||||||
|
;;
|
||||||
|
-stag|-staging)
|
||||||
|
APP_ENV="staging"
|
||||||
|
APP_DEBUG="false"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# ===== Laravel installieren (als eigener User) =====
|
||||||
|
log "Laravel installieren…"
|
||||||
mkdir -p "$(dirname "$APP_DIR")"
|
mkdir -p "$(dirname "$APP_DIR")"
|
||||||
chown "$APP_USER":$APP_GROUP "$(dirname "$APP_DIR")"
|
chown -R "$APP_USER":$APP_GROUP "$(dirname "$APP_DIR")"
|
||||||
|
|
||||||
if [ ! -d "${APP_DIR}/.git" ]; then
|
if [ ! -d "${APP_DIR}" ] || [ -z "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]; then
|
||||||
rm -rf "${APP_DIR}"
|
sudo -u "$APP_USER" -H bash -lc "cd /var/www && COMPOSER_ALLOW_SUPERUSER=0 composer create-project laravel/laravel ${APP_USER} --no-interaction"
|
||||||
quietly sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}"
|
|
||||||
else
|
|
||||||
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && git fetch --depth=1 origin ${GIT_BRANCH} && git checkout ${GIT_BRANCH} && git pull --ff-only"
|
|
||||||
fi
|
fi
|
||||||
ok
|
|
||||||
|
|
||||||
# ===== .env erstellen und befüllen =====
|
|
||||||
APP_URL="http://${SERVER_IP}"
|
APP_URL="http://${SERVER_IP}"
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force"
|
||||||
|
|
||||||
|
# .env befüllen (MariaDB & Redis Sessions)
|
||||||
sed -i "s|^APP_NAME=.*|APP_NAME=${APP_NAME}|g" "${APP_DIR}/.env"
|
sed -i "s|^APP_NAME=.*|APP_NAME=${APP_NAME}|g" "${APP_DIR}/.env"
|
||||||
sed -i "s|^APP_URL=.*|APP_URL=${APP_URL}|g" "${APP_DIR}/.env"
|
sed -i "s|^APP_URL=.*|APP_URL=${APP_URL}|g" "${APP_DIR}/.env"
|
||||||
sed -i "s|^APP_ENV=.*|APP_ENV=${APP_ENV}|g" "${APP_DIR}/.env"
|
sed -i "s|^APP_ENV=.*|APP_ENV=${APP_ENV}|g" "${APP_DIR}/.env"
|
||||||
|
|
@ -513,356 +496,126 @@ sed -i "s|^REDIS_HOST=.*|REDIS_HOST=127.0.0.1|g" "${APP_DIR}/.env"
|
||||||
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=null|g" "${APP_DIR}/.env"
|
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=null|g" "${APP_DIR}/.env"
|
||||||
sed -i "s|^REDIS_PORT=.*|REDIS_PORT=6379|g" "${APP_DIR}/.env"
|
sed -i "s|^REDIS_PORT=.*|REDIS_PORT=6379|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
REVERB_APP_ID="$(short)"
|
|
||||||
REVERB_APP_KEY="$(short)"
|
|
||||||
REVERB_APP_SECRET="$(short)"
|
|
||||||
grep -q '^REVERB_APP_ID=' "${APP_DIR}/.env" \
|
|
||||||
|| printf '\nBROADCAST_CONNECTION=reverb\nREVERB_APP_ID=%s\nREVERB_APP_KEY=%s\nREVERB_APP_SECRET=%s\nREVERB_HOST=127.0.0.1\nREVERB_PORT=8080\nREVERB_SCHEME=http\nVITE_REVERB_APP_KEY=%s\nVITE_REVERB_HOST=%s\nVITE_REVERB_PORT=8080\nVITE_REVERB_SCHEME=http\n' \
|
|
||||||
"$REVERB_APP_ID" "$REVERB_APP_KEY" "$REVERB_APP_SECRET" "$REVERB_APP_KEY" "$SERVER_IP" >> "${APP_DIR}/.env"
|
|
||||||
|
|
||||||
# Bootstrap-Admin für den ersten Login
|
# === Bootstrap-Admin für den ersten Login (nur .env, kein DB-User) ===
|
||||||
BOOTSTRAP_USER="${APP_USER}"
|
BOOTSTRAP_USER="${APP_USER}"
|
||||||
BOOTSTRAP_EMAIL="${APP_USER}@localhost"
|
BOOTSTRAP_EMAIL="${APP_USER}@localhost"
|
||||||
BOOTSTRAP_PASS="$(openssl rand -base64 18 | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 12)"
|
BOOTSTRAP_PASS="$(openssl rand -base64 18 | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 12)"
|
||||||
BOOTSTRAP_HASH="$(php -r 'echo password_hash($argv[1], PASSWORD_BCRYPT);' "$BOOTSTRAP_PASS")"
|
BOOTSTRAP_HASH="$(php -r 'echo password_hash($argv[1], PASSWORD_BCRYPT);' "$BOOTSTRAP_PASS")"
|
||||||
|
|
||||||
grep -q '^SETUP_PHASE=' "${APP_DIR}/.env" \
|
grep -q '^SETUP_PHASE=' "${APP_DIR}/.env" || echo "SETUP_PHASE=bootstrap" >> "${APP_DIR}/.env"
|
||||||
|| echo "SETUP_PHASE=bootstrap" >> "${APP_DIR}/.env"
|
|
||||||
sed -i "s|^SETUP_PHASE=.*|SETUP_PHASE=bootstrap|g" "${APP_DIR}/.env"
|
sed -i "s|^SETUP_PHASE=.*|SETUP_PHASE=bootstrap|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
grep -q '^BOOTSTRAP_ADMIN_USER=' "${APP_DIR}/.env" \
|
grep -q '^BOOTSTRAP_ADMIN_USER=' "${APP_DIR}/.env" || echo "BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}" >> "${APP_DIR}/.env"
|
||||||
|| echo "BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}" >> "${APP_DIR}/.env"
|
|
||||||
sed -i "s|^BOOTSTRAP_ADMIN_USER=.*|BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}|g" "${APP_DIR}/.env"
|
sed -i "s|^BOOTSTRAP_ADMIN_USER=.*|BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
grep -q '^BOOTSTRAP_ADMIN_EMAIL=' "${APP_DIR}/.env" \
|
grep -q '^BOOTSTRAP_ADMIN_EMAIL=' "${APP_DIR}/.env" || echo "BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}" >> "$>
|
||||||
|| echo "BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}" >> "${APP_DIR}/.env"
|
|
||||||
sed -i "s|^BOOTSTRAP_ADMIN_EMAIL=.*|BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}|g" "${APP_DIR}/.env"
|
sed -i "s|^BOOTSTRAP_ADMIN_EMAIL=.*|BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
grep -q '^BOOTSTRAP_ADMIN_PASSWORD_HASH=' "${APP_DIR}/.env" \
|
grep -q '^BOOTSTRAP_ADMIN_PASSWORD_HASH=' "${APP_DIR}/.env" || echo "BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}" >> "${APP_DIR}/.env"
|
||||||
|| echo "BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}" >> "${APP_DIR}/.env"
|
|
||||||
sed -i "s|^BOOTSTRAP_ADMIN_PASSWORD_HASH=.*|BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}|g" "${APP_DIR}/.env"
|
sed -i "s|^BOOTSTRAP_ADMIN_PASSWORD_HASH=.*|BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
step "PHP-Abhängigkeiten installieren (Composer)" "1–2 Min"
|
# ===== Node/NPM installieren (für Vite/Tailwind Build) =====
|
||||||
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-dev --optimize-autoloader --no-interaction"
|
|
||||||
ok
|
|
||||||
|
|
||||||
step "Datenbank migrieren" "15 Sek"
|
|
||||||
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force"
|
|
||||||
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan migrate --force"
|
|
||||||
try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan storage:link --force"
|
|
||||||
try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan config:cache"
|
|
||||||
try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan route:cache"
|
|
||||||
try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan view:cache"
|
|
||||||
ok
|
|
||||||
|
|
||||||
if [ -f "${APP_DIR}/package.json" ]; then
|
|
||||||
step "Frontend bauen (npm)" "1–3 Min"
|
|
||||||
# Node/npm falls noch nicht installiert
|
|
||||||
if ! command -v node >/dev/null 2>&1; then
|
|
||||||
if [ "$NODE_SETUP" = "nodesource" ]; then
|
if [ "$NODE_SETUP" = "nodesource" ]; then
|
||||||
quietly bash -c "curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh && bash /tmp/nodesource_setup.sh && rm -f /tmp/nodesource_setup.sh"
|
# LTS via NodeSource (empfohlen für aktuelle LTS)
|
||||||
quietly apt-get install -y nodejs
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
else
|
else
|
||||||
quietly apt-get install -y nodejs npm
|
# Debian-Repo (ok für Basics, aber u.U. älter)
|
||||||
|
apt-get install -y nodejs npm
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
|
# ===== Projekt aus Git holen (PLATZHALTER) =====
|
||||||
if ! sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build" >> "$LOG_FILE" 2>&1; then
|
# Falls dein Repo später bereitsteht, überschreibt dieser Block das leere/Standard-Laravel.
|
||||||
warn "npm run build fehlgeschlagen — manuell nachholen: cd ${APP_DIR} && npm run build"
|
if [ "${GIT_REPO}" != "https://example.com/your-repo-placeholder.git" ]; then
|
||||||
|
if [ ! -d "${APP_DIR}/.git" ]; then
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}"
|
||||||
else
|
else
|
||||||
ok
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && git fetch --depth=1 origin ${GIT_BRANCH} && git checkout ${GIT_BRANCH} && git pull --ff-only"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p /var/lib/mailwolt/wizard
|
# ===== Frontend Build (nur wenn package.json existiert) =====
|
||||||
chown www-data:www-data /var/lib/mailwolt/wizard
|
if [ -f "${APP_DIR}/package.json" ]; then
|
||||||
chmod 775 /var/lib/mailwolt/wizard
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
|
||||||
|
# Prod-Build (Vite/Tailwind)
|
||||||
step "Hilfsskripte & Konfiguration installieren" "5 Sek"
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build || npm run build:prod || true"
|
||||||
cat > /usr/local/sbin/mailwolt-apply-domains <<'HELPER'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
UI_HOST=""; WEBMAIL_HOST=""; MAIL_HOST=""; SSL_AUTO=0
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--ui-host) UI_HOST="$2"; shift 2 ;;
|
|
||||||
--webmail-host) WEBMAIL_HOST="$2"; shift 2 ;;
|
|
||||||
--mail-host) MAIL_HOST="$2"; shift 2 ;;
|
|
||||||
--ssl-auto) SSL_AUTO="$2"; shift 2 ;;
|
|
||||||
*) shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
|
|
||||||
PHP_FPM_SOCK="/run/php/php${PHPV}-fpm.sock"
|
|
||||||
[ -S "$PHP_FPM_SOCK" ] || PHP_FPM_SOCK="/run/php/php-fpm.sock"
|
|
||||||
|
|
||||||
APP_DIR="/var/www/mailwolt"
|
|
||||||
NGINX_SITE="/etc/nginx/sites-available/mailwolt.conf"
|
|
||||||
ACME_ROOT="/var/www/letsencrypt"
|
|
||||||
mkdir -p "${ACME_ROOT}/.well-known/acme-challenge"
|
|
||||||
|
|
||||||
# --- Phase 1: HTTP-only Vhosts mit ACME-Challenge ---
|
|
||||||
cat > "${NGINX_SITE}" <<CONF
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
server_name ${UI_HOST} ${WEBMAIL_HOST};
|
|
||||||
|
|
||||||
root ${APP_DIR}/public;
|
|
||||||
index index.php index.html;
|
|
||||||
|
|
||||||
location /.well-known/acme-challenge/ {
|
|
||||||
root ${ACME_ROOT};
|
|
||||||
try_files \$uri =404;
|
|
||||||
}
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.php?\$query_string;
|
|
||||||
}
|
|
||||||
location ~ \.php$ {
|
|
||||||
include snippets/fastcgi-php.conf;
|
|
||||||
fastcgi_pass unix:${PHP_FPM_SOCK};
|
|
||||||
}
|
|
||||||
location ^~ /livewire/ {
|
|
||||||
try_files \$uri /index.php?\$query_string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
nginx -t && systemctl reload nginx
|
|
||||||
|
|
||||||
# --- Phase 2: Let's Encrypt Zertifikate holen ---
|
|
||||||
# Prüfen ob Server globales IPv6 hat (nötig wenn AAAA-Records existieren)
|
|
||||||
has_global_ipv6() {
|
|
||||||
ip -6 addr show scope global 2>/dev/null | grep -q 'inet6'
|
|
||||||
}
|
|
||||||
|
|
||||||
cert_needs_action() {
|
|
||||||
local domain="$1"
|
|
||||||
local cert="/etc/letsencrypt/live/${domain}/fullchain.pem"
|
|
||||||
[ ! -f "${cert}" ] && return 0
|
|
||||||
# 0 = gültig für >10 Tage → überspringen; 1 = läuft ab → erneuern
|
|
||||||
openssl x509 -checkend 864000 -noout -in "${cert}" 2>/dev/null && return 1
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
certbot_safe() {
|
|
||||||
local domain="$1"
|
|
||||||
local has_aaaa
|
|
||||||
has_aaaa=$(dig +short AAAA "${domain}" 2>/dev/null | head -1)
|
|
||||||
if [ -n "${has_aaaa}" ] && ! has_global_ipv6; then
|
|
||||||
echo "[!] ${domain}: AAAA-Record vorhanden aber kein IPv6 auf diesem Server — Let's Encrypt würde fehlschlagen. Self-signed wird verwendet." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
certbot certonly --webroot \
|
|
||||||
-w "${ACME_ROOT}" \
|
|
||||||
-d "${domain}" \
|
|
||||||
--non-interactive --agree-tos \
|
|
||||||
--email "webmaster@${domain}" \
|
|
||||||
--no-eff-email
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "${SSL_AUTO}" = "1" ]; then
|
|
||||||
for DOMAIN in "${UI_HOST}" "${WEBMAIL_HOST}"; do
|
|
||||||
[ -z "${DOMAIN}" ] && continue
|
|
||||||
if cert_needs_action "${DOMAIN}"; then
|
|
||||||
certbot_safe "${DOMAIN}" || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Phase 3: Finale Vhosts ---
|
|
||||||
# Nur HTTPS wenn LE-Cert tatsächlich vorhanden, sonst HTTP-only (kein self-signed Fallback)
|
|
||||||
UI_HAS_CERT=0
|
|
||||||
WM_HAS_CERT=0
|
|
||||||
[ -f "/etc/letsencrypt/live/${UI_HOST}/fullchain.pem" ] && UI_HAS_CERT=1
|
|
||||||
[ -f "/etc/letsencrypt/live/${WEBMAIL_HOST}/fullchain.pem" ] && WM_HAS_CERT=1
|
|
||||||
(
|
|
||||||
|
|
||||||
if [ "${UI_HAS_CERT}" = "1" ] || [ "${WM_HAS_CERT}" = "1" ]; then
|
# ===== App-User/Gruppen & Rechte (am ENDE ausführen) =====
|
||||||
# Mindestens ein Cert vorhanden → HTTP-Redirect Block
|
APP_USER="${APP_USER:-${APP_NAME}app}"
|
||||||
cat <<CONF
|
APP_GROUP="${APP_GROUP}"
|
||||||
server {
|
APP_PW="${APP_PW:-changeme123}"
|
||||||
listen 80;
|
APP_DIR="${APP_DIR}"
|
||||||
listen [::]:80;
|
|
||||||
server_name ${UI_HOST} ${WEBMAIL_HOST};
|
|
||||||
|
|
||||||
location /.well-known/acme-challenge/ {
|
# User anlegen (nur falls noch nicht vorhanden) + Passwort setzen + Gruppe
|
||||||
root ${ACME_ROOT};
|
|
||||||
try_files \$uri =404;
|
|
||||||
}
|
|
||||||
location / { return 301 https://\$host\$request_uri; }
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
else
|
|
||||||
# Kein Cert → HTTP-only, App läuft auf Port 80 weiter
|
|
||||||
cat <<CONF
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
root ${APP_DIR}/public;
|
|
||||||
index index.php index.html;
|
|
||||||
|
|
||||||
location /.well-known/acme-challenge/ {
|
|
||||||
root ${ACME_ROOT};
|
|
||||||
try_files \$uri =404;
|
|
||||||
}
|
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
|
||||||
location ~ \.php\$ {
|
|
||||||
include snippets/fastcgi-php.conf;
|
|
||||||
fastcgi_pass unix:${PHP_FPM_SOCK};
|
|
||||||
}
|
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${UI_HOST}" ] && [ "${UI_HAS_CERT}" = "1" ]; then
|
|
||||||
cat <<CONF
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
listen [::]:443 ssl;
|
|
||||||
http2 on;
|
|
||||||
server_name ${UI_HOST};
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/${UI_HOST}/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/${UI_HOST}/privkey.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
|
|
||||||
root ${APP_DIR}/public;
|
|
||||||
index index.php index.html;
|
|
||||||
|
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
|
||||||
location ~ \.php\$ {
|
|
||||||
include snippets/fastcgi-php.conf;
|
|
||||||
fastcgi_param HTTPS on;
|
|
||||||
fastcgi_pass unix:${PHP_FPM_SOCK};
|
|
||||||
}
|
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${WEBMAIL_HOST}" ] && [ "${WM_HAS_CERT}" = "1" ]; then
|
|
||||||
cat <<CONF
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
listen [::]:443 ssl;
|
|
||||||
http2 on;
|
|
||||||
server_name ${WEBMAIL_HOST};
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/${WEBMAIL_HOST}/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/${WEBMAIL_HOST}/privkey.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
|
|
||||||
root ${APP_DIR}/public;
|
|
||||||
index index.php index.html;
|
|
||||||
|
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
|
||||||
location ~ \.php\$ {
|
|
||||||
include snippets/fastcgi-php.conf;
|
|
||||||
fastcgi_param HTTPS on;
|
|
||||||
fastcgi_pass unix:${PHP_FPM_SOCK};
|
|
||||||
}
|
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
fi
|
|
||||||
) > "${NGINX_SITE}"
|
|
||||||
|
|
||||||
# State-Dateien VOR dem nginx-Switch schreiben:
|
|
||||||
# Browser-Poll (alle 2s) liest done=1 → Polling stoppt → "Zum Login" erscheint.
|
|
||||||
# Danach 6s sleep → nginx switchet auf HTTPS → User klickt Link → funktioniert.
|
|
||||||
STATE_DIR="/var/lib/mailwolt/wizard"
|
|
||||||
if [ -d "${STATE_DIR}" ]; then
|
|
||||||
for k in ui mail webmail; do
|
|
||||||
[ -f "${STATE_DIR}/${k}" ] && printf "done" > "${STATE_DIR}/${k}"
|
|
||||||
done
|
|
||||||
if [ "${UI_HAS_CERT}" = "1" ] || [ "${WM_HAS_CERT}" = "1" ]; then
|
|
||||||
printf "1" > "${STATE_DIR}/done"
|
|
||||||
else
|
|
||||||
printf "0" > "${STATE_DIR}/done"
|
|
||||||
fi
|
|
||||||
sleep 6
|
|
||||||
fi
|
|
||||||
|
|
||||||
nginx -t && systemctl reload nginx
|
|
||||||
HELPER
|
|
||||||
chmod 755 /usr/local/sbin/mailwolt-apply-domains
|
|
||||||
|
|
||||||
# ===== mailwolt-update installieren =====
|
|
||||||
install -m 755 "${APP_DIR}/update.sh" /usr/local/sbin/mailwolt-update
|
|
||||||
|
|
||||||
# ===== Sudoers für www-data (helper + update) =====
|
|
||||||
cat > /etc/sudoers.d/mailwolt-certbot <<'SUDOERS'
|
|
||||||
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-apply-domains
|
|
||||||
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
|
|
||||||
www-data ALL=(root) NOPASSWD: /usr/bin/certbot
|
|
||||||
SUDOERS
|
|
||||||
chmod 440 /etc/sudoers.d/mailwolt-certbot
|
|
||||||
|
|
||||||
# git safe.directory damit spätere pulls als root möglich sind
|
|
||||||
git config --global --add safe.directory "${APP_DIR}" || true
|
|
||||||
|
|
||||||
# ===== Version-Datei schreiben =====
|
|
||||||
mkdir -p /var/lib/mailwolt
|
|
||||||
GIT_TAG="$(sudo -u "$APP_USER" -H bash -lc "git -C ${APP_DIR} ls-remote --tags --sort=-v:refname origin 'v*' 2>/dev/null | grep -v '\^{}' | head -1 | sed 's|.*refs/tags/||'")"
|
|
||||||
if [ -n "$GIT_TAG" ]; then
|
|
||||||
echo "${GIT_TAG#v}" > /var/lib/mailwolt/version
|
|
||||||
echo "$GIT_TAG" > /var/lib/mailwolt/version_raw
|
|
||||||
else
|
|
||||||
warn "Kein Git-Tag gefunden — Version-Datei wird nicht geschrieben"
|
|
||||||
fi
|
|
||||||
ok
|
|
||||||
|
|
||||||
step "Berechtigungen setzen & Dienste starten" "10 Sek"
|
|
||||||
if ! id -u "$APP_USER" >/dev/null 2>&1; then
|
if ! id -u "$APP_USER" >/dev/null 2>&1; then
|
||||||
quietly adduser --disabled-password --gecos "" "$APP_USER"
|
adduser --disabled-password --gecos "" "$APP_USER"
|
||||||
|
echo "${APP_USER}:${APP_PW}" | chpasswd
|
||||||
fi
|
fi
|
||||||
echo "${APP_USER}:${APP_PW}" | quietly chpasswd
|
usermod -a -G "$APP_GROUP" "$APP_USER"
|
||||||
quietly usermod -a -G "$APP_GROUP" "$APP_USER"
|
|
||||||
|
|
||||||
|
# Besitz & Rechte
|
||||||
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
|
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
|
||||||
find "$APP_DIR" -type d -exec chmod 775 {} \; 2>>"$LOG_FILE"
|
find "$APP_DIR" -type d -exec chmod 775 {} \;
|
||||||
find "$APP_DIR" -type f -exec chmod 664 {} \; 2>>"$LOG_FILE"
|
find "$APP_DIR" -type f -exec chmod 664 {} \;
|
||||||
chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache
|
chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache
|
||||||
if command -v setfacl >/dev/null 2>&1; then
|
if command -v setfacl >/dev/null 2>&1; then
|
||||||
try_quiet setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache"
|
setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \
|
||||||
try_quiet setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache"
|
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
||||||
|
setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \
|
||||||
|
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}[i] App-User: ${APP_USER} Passwort: ${APP_PW}${NC}"
|
||||||
|
|
||||||
|
|
||||||
|
# Optional: ACLs, falls verfügbar (robuster bei gemischten Schreibzugriffen)
|
||||||
|
if command -v setfacl >/dev/null 2>&1; then
|
||||||
|
setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \
|
||||||
|
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
||||||
|
setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \
|
||||||
|
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
grep -q 'umask 002' /home/${APP_USER}/.profile 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.profile
|
grep -q 'umask 002' /home/${APP_USER}/.profile 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.profile
|
||||||
grep -q 'umask 002' /home/${APP_USER}/.bashrc 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.bashrc
|
grep -q 'umask 002' /home/${APP_USER}/.bashrc 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.bashrc
|
||||||
try_quiet sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002"
|
|
||||||
|
|
||||||
|
# 7) npm respektiert umask – zur Sicherheit direkt setzen (für APP_USER)
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# 8) PHP-FPM-Worker laufen als www-data (Standard). Stelle sicher, dass der FPM-Socket group-writable ist:
|
||||||
|
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
|
||||||
FPM_POOL="/etc/php/${PHPV}/fpm/pool.d/www.conf"
|
FPM_POOL="/etc/php/${PHPV}/fpm/pool.d/www.conf"
|
||||||
if [ -f "$FPM_POOL" ]; then
|
if [ -f "$FPM_POOL" ]; then
|
||||||
sed -i 's/^;*listen\.owner.*/listen.owner = www-data/' "$FPM_POOL"
|
sed -i 's/^;*listen\.owner.*/listen.owner = www-data/' "$FPM_POOL"
|
||||||
sed -i 's/^;*listen\.group.*/listen.group = www-data/' "$FPM_POOL"
|
sed -i 's/^;*listen\.group.*/listen.group = www-data/' "$FPM_POOL"
|
||||||
sed -i 's/^;*listen\.mode.*/listen.mode = 0660/' "$FPM_POOL"
|
sed -i 's/^;*listen\.mode.*/listen.mode = 0660/' "$FPM_POOL"
|
||||||
try_quiet systemctl restart php${PHPV}-fpm
|
systemctl restart php${PHPV}-fpm || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 9) Optional: deinem Shell-/IDE-User ebenfalls Schreibrechte geben
|
||||||
IDE_USER="${SUDO_USER:-}"
|
IDE_USER="${SUDO_USER:-}"
|
||||||
if [ -n "$IDE_USER" ] && id "$IDE_USER" >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then
|
if [ -n "$IDE_USER" ] && id "$IDE_USER" >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then
|
||||||
try_quiet usermod -a -G "$APP_GROUP" "$IDE_USER"
|
usermod -a -G "$APP_GROUP" "$IDE_USER" || true
|
||||||
try_quiet setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR"
|
setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR"
|
||||||
try_quiet setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR"
|
setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR"
|
||||||
|
echo -e "${YELLOW}[i]${NC} Benutzer '${IDE_USER}' wurde für Schreibzugriff freigeschaltet (ACL + Gruppe ${APP_GROUP})."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
try_quiet systemctl reload nginx
|
# Webstack neu laden
|
||||||
try_quiet systemctl restart php*-fpm
|
systemctl reload nginx || true
|
||||||
ok
|
systemctl restart php*-fpm || true
|
||||||
|
|
||||||
# ===== Monit =====
|
# Hinweis zur neuen Gruppenzugehörigkeit
|
||||||
|
|
||||||
|
echo -e "${YELLOW}[i]${NC} SHELL: Du kannst dich nun als Benutzer '${APP_USER}' mit dem Passwort '${APP_PW}' anmelden."
|
||||||
|
echo -e "${YELLOW}[i]${NC} Hinweis: Nach dem ersten Login solltest du das Passwort mit 'passwd ${APP_USER}' ändern."
|
||||||
|
echo -e "${YELLOW}[i]${NC} Damit die Gruppenrechte (${APP_GROUP}) aktiv werden, bitte einmal ab- und wieder anmelden."
|
||||||
|
|
||||||
|
# ===== Monit (Watchdog) – installiert, aber NICHT aktiviert =====
|
||||||
|
log "Monit (Watchdog) installieren (deaktiviert)"
|
||||||
cat > /etc/monit/monitrc <<'EOF'
|
cat > /etc/monit/monitrc <<'EOF'
|
||||||
set daemon 60
|
set daemon 60
|
||||||
set logfile syslog facility log_daemon
|
set logfile syslog facility log_daemon
|
||||||
|
|
@ -871,127 +624,62 @@ set logfile syslog facility log_daemon
|
||||||
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
||||||
start program = "/bin/systemctl start postfix"
|
start program = "/bin/systemctl start postfix"
|
||||||
stop program = "/bin/systemctl stop postfix"
|
stop program = "/bin/systemctl stop postfix"
|
||||||
if failed host 127.0.0.1 port 25 protocol smtp for 3 cycles then restart
|
if failed port 25 protocol smtp then restart
|
||||||
if 5 restarts within 10 cycles then alert
|
|
||||||
|
|
||||||
check process dovecot with pidfile /run/dovecot/master.pid
|
check process dovecot with pidfile /var/run/dovecot/master.pid
|
||||||
start program = "/bin/systemctl start dovecot"
|
start program = "/bin/systemctl start dovecot"
|
||||||
stop program = "/bin/systemctl stop dovecot"
|
stop program = "/bin/systemctl stop dovecot"
|
||||||
if failed host 127.0.0.1 port 143 type tcp for 3 cycles then restart
|
if failed port 143 type tcp then restart
|
||||||
if failed host 127.0.0.1 port 993 type tcpssl for 3 cycles then restart
|
if failed port 993 type tcp ssl then restart
|
||||||
if 5 restarts within 10 cycles then alert
|
|
||||||
|
|
||||||
check process mariadb matching "mysqld"
|
check process mariadb with pidfile /var/run/mysqld/mysqld.pid
|
||||||
start program = "/bin/systemctl start mariadb"
|
start program = "/bin/systemctl start mariadb"
|
||||||
stop program = "/bin/systemctl stop mariadb"
|
stop program = "/bin/systemctl stop mariadb"
|
||||||
if failed host 127.0.0.1 port 3306 type tcp for 2 cycles then restart
|
if failed port 3306 type tcp then restart
|
||||||
if 5 restarts within 10 cycles then alert
|
|
||||||
|
|
||||||
check process redis with pidfile /run/redis/redis-server.pid
|
check process redis with pidfile /run/redis/redis-server.pid
|
||||||
start program = "/bin/systemctl start redis-server"
|
start program = "/bin/systemctl start redis-server"
|
||||||
stop program = "/bin/systemctl stop redis-server"
|
stop program = "/bin/systemctl stop redis-server"
|
||||||
if failed host 127.0.0.1 port 6379 type tcp for 2 cycles then restart
|
if failed port 6379 type tcp then restart
|
||||||
if 5 restarts within 10 cycles then alert
|
|
||||||
|
|
||||||
check process rspamd matching "rspamd: main process"
|
check process rspamd with pidfile /run/rspamd/rspamd.pid
|
||||||
start program = "/bin/systemctl start rspamd" with timeout 60 seconds
|
start program = "/bin/systemctl start rspamd"
|
||||||
stop program = "/bin/systemctl stop rspamd"
|
stop program = "/bin/systemctl stop rspamd"
|
||||||
if failed host 127.0.0.1 port 11332 type tcp for 3 cycles then restart
|
if failed port 11332 type tcp then restart
|
||||||
if failed host 127.0.0.1 port 11334 type tcp for 3 cycles then restart
|
|
||||||
if 5 restarts within 10 cycles then alert
|
|
||||||
|
|
||||||
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
||||||
start program = "/bin/systemctl start opendkim"
|
start program = "/bin/systemctl start opendkim"
|
||||||
stop program = "/bin/systemctl stop opendkim"
|
stop program = "/bin/systemctl stop opendkim"
|
||||||
if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
if failed port 8891 type tcp then restart
|
||||||
if 5 restarts within 10 cycles then alert
|
|
||||||
|
|
||||||
check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
|
||||||
start program = "/bin/systemctl start opendmarc"
|
|
||||||
stop program = "/bin/systemctl stop opendmarc"
|
|
||||||
if 5 restarts within 10 cycles then alert
|
|
||||||
|
|
||||||
check process nginx with pidfile /run/nginx.pid
|
check process nginx with pidfile /run/nginx.pid
|
||||||
start program = "/bin/systemctl start nginx"
|
start program = "/bin/systemctl start nginx"
|
||||||
stop program = "/bin/systemctl stop nginx"
|
stop program = "/bin/systemctl stop nginx"
|
||||||
if failed host 127.0.0.1 port 80 type tcp for 2 cycles then restart
|
if failed port 80 type tcp then restart
|
||||||
if 5 restarts within 10 cycles then alert
|
if failed port 443 type tcp ssl then restart
|
||||||
|
|
||||||
check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
|
||||||
start program = "/bin/systemctl start fail2ban"
|
|
||||||
stop program = "/bin/systemctl stop fail2ban"
|
|
||||||
if 5 restarts within 10 cycles then alert
|
|
||||||
|
|
||||||
check process clamav matching "clamd"
|
|
||||||
start program = "/bin/systemctl start clamav-daemon"
|
|
||||||
stop program = "/bin/systemctl stop clamav-daemon"
|
|
||||||
if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
|
|
||||||
if 5 restarts within 10 cycles then unmonitor
|
|
||||||
EOF
|
EOF
|
||||||
chmod 600 /etc/monit/monitrc
|
chmod 600 /etc/monit/monitrc
|
||||||
monit -t || { warn "Monit-Config ungültig — prüfe /etc/monit/monitrc"; }
|
systemctl disable --now monit || true
|
||||||
try_quiet systemctl enable --now monit
|
apt-mark hold monit >/dev/null 2>&1 || true
|
||||||
|
|
||||||
# ===== Smoke-Test =====
|
# ===== Smoke-Test (alle Ports, mit Timeouts) =====
|
||||||
step "Dienste prüfen (Port-Check)"
|
log "Smoke-Test (Ports & Banner):"
|
||||||
_sok=0; _sfail=0
|
set +e
|
||||||
|
printf "[25] " && timeout 6s bash -lc 'printf "EHLO localhost\r\nQUIT\r\n" | nc -v -w 4 127.0.0.1 25 2>&1' || true
|
||||||
smoke_smtp() {
|
printf "[465] " && timeout 6s openssl s_client -connect 127.0.0.1:465 -brief -quiet </dev/null || echo "[465] Verbindung fehlgeschlagen"
|
||||||
local port="$1" label="$2"
|
printf "[587] " && timeout 6s openssl s_client -starttls smtp -connect 127.0.0.1:587 -brief -quiet </dev/null || echo "[587] Verbindung fehlgeschlagen"
|
||||||
local out
|
printf "[110] " && timeout 6s bash -lc 'printf "QUIT\r\n" | nc -v -w 4 127.0.0.1 110 2>&1' || true
|
||||||
out=$(printf "EHLO localhost\r\nQUIT\r\n" | timeout 5s nc -w3 127.0.0.1 "$port" 2>/dev/null || true)
|
printf "[995] " && timeout 6s openssl s_client -connect 127.0.0.1:995 -brief -quiet </dev/null || echo "[995] Verbindung fehlgeschlagen"
|
||||||
if echo "$out" | grep -q '^220'; then
|
printf "[143] " && timeout 6s bash -lc 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | nc -v -w 4 127.0.0.1 143 2>&1' || true
|
||||||
printf " ${GREEN}✔${NC} %-5s %s\n" "$port" "$label"; (( _sok++ )) || true
|
printf "[993] " && timeout 6s openssl s_client -connect 127.0.0.1:993 -brief -quiet </dev/null || echo "[993] Verbindung fehlgeschlagen"
|
||||||
else
|
set -e
|
||||||
printf " ${YELLOW}⚠${NC} %-5s %s — nicht erreichbar\n" "$port" "$label"; (( _sfail++ )) || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
smoke_tls() {
|
|
||||||
local port="$1" label="$2" extra="${3:-}"
|
|
||||||
local out
|
|
||||||
out=$(timeout 5s openssl s_client $extra -connect 127.0.0.1:"$port" -brief -quiet </dev/null 2>&1 || true)
|
|
||||||
if echo "$out" | grep -qiE '(CONNECTED|depth|Verify|^220|\+OK|OK)'; then
|
|
||||||
printf " ${GREEN}✔${NC} %-5s %s\n" "$port" "$label"; (( _sok++ )) || true
|
|
||||||
else
|
|
||||||
printf " ${YELLOW}⚠${NC} %-5s %s — nicht erreichbar\n" "$port" "$label"; (( _sfail++ )) || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
smoke_imap() {
|
|
||||||
local port="$1" label="$2"
|
|
||||||
local out
|
|
||||||
out=$(printf ". CAPABILITY\r\n. LOGOUT\r\n" | timeout 5s nc -w3 127.0.0.1 "$port" 2>/dev/null || true)
|
|
||||||
if echo "$out" | grep -qi 'CAPABILITY'; then
|
|
||||||
printf " ${GREEN}✔${NC} %-5s %s\n" "$port" "$label"; (( _sok++ )) || true
|
|
||||||
else
|
|
||||||
printf " ${YELLOW}⚠${NC} %-5s %s — nicht erreichbar\n" "$port" "$label"; (( _sfail++ )) || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
smoke_pop3() {
|
|
||||||
local port="$1" label="$2"
|
|
||||||
local out
|
|
||||||
out=$(printf "QUIT\r\n" | timeout 5s nc -w3 127.0.0.1 "$port" 2>/dev/null || true)
|
|
||||||
if echo "$out" | grep -qi '^\+OK'; then
|
|
||||||
printf " ${GREEN}✔${NC} %-5s %s\n" "$port" "$label"; (( _sok++ )) || true
|
|
||||||
else
|
|
||||||
printf " ${YELLOW}⚠${NC} %-5s %s — nicht erreichbar\n" "$port" "$label"; (( _sfail++ )) || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
smoke_smtp 25 "SMTP"
|
|
||||||
smoke_tls 465 "SMTPS" ""
|
|
||||||
smoke_tls 587 "Submission" "-starttls smtp"
|
|
||||||
smoke_imap 143 "IMAP"
|
|
||||||
smoke_tls 993 "IMAPS" ""
|
|
||||||
smoke_pop3 110 "POP3"
|
|
||||||
smoke_tls 995 "POP3S" ""
|
|
||||||
|
|
||||||
printf "\n ${GREY}%d/%d Dienste erreichbar${NC}\n" "$_sok" "$(( _sok + _sfail ))"
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo -e " ${GREY}Bootstrap-Login (nur für ERSTEN Login & Wizard):${NC}"
|
echo "=============================================================="
|
||||||
echo -e " ${CYAN}User: ${NC}${BOOTSTRAP_USER}"
|
echo " Bootstrap-Login (nur für ERSTEN Login & Wizard):"
|
||||||
echo -e " ${CYAN}Passwort: ${NC}${BOOTSTRAP_PASS}"
|
echo " User: ${BOOTSTRAP_USER}"
|
||||||
echo -e " ${GREY}Log: ${LOG_FILE}${NC}"
|
echo " Passwort: ${BOOTSTRAP_PASS}"
|
||||||
|
echo "=============================================================="
|
||||||
echo
|
echo
|
||||||
|
|
||||||
footer_ok "$SERVER_IP"
|
footer_ok "$SERVER_IP"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AskMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Ask2AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EditMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MarkdownSettingsMigration">
|
||||||
|
<option name="stateVersion" value="1" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/mailwolt-installer.iml" filepath="$PROJECT_DIR$/.idea/mailwolt-installer.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MessDetectorOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCSFixerOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||||
|
<option name="highlightLevel" value="WARNING" />
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PhpStanOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PsalmOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# ===================== HTTP (Port 80) =====================
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# ACME HTTP-01
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/letsencrypt;
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
__HTTP_BODY__
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===================== HTTPS (Port 443) ====================
|
||||||
|
__SSL_SERVER_BLOCK__
|
||||||
|
|
@ -0,0 +1,909 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
(
|
||||||
|
export HISTFILE=
|
||||||
|
set +o history
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
##############################################
|
||||||
|
# MailWolt #
|
||||||
|
# Bootstrap Installer v1.0 #
|
||||||
|
##############################################
|
||||||
|
|
||||||
|
# ===== CLI-Flags (-dev / -stag) =====
|
||||||
|
APP_ENV="${APP_ENV:-production}"
|
||||||
|
APP_DEBUG="${APP_DEBUG:-false}"
|
||||||
|
|
||||||
|
DEV_MODE=0
|
||||||
|
STAG_MODE=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-dev)
|
||||||
|
DEV_MODE=1
|
||||||
|
APP_ENV="local"
|
||||||
|
APP_DEBUG="true"
|
||||||
|
;;
|
||||||
|
-stag|-staging)
|
||||||
|
STAG_MODE=1
|
||||||
|
APP_ENV="staging"
|
||||||
|
APP_DEBUG="false"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# ===== Branding & Pfade =====
|
||||||
|
APP_NAME="${APP_NAME:-MailWolt}"
|
||||||
|
|
||||||
|
APP_USER="${APP_USER:-mailwolt}"
|
||||||
|
APP_GROUP="${APP_GROUP:-www-data}"
|
||||||
|
|
||||||
|
APP_DIR="/var/www/${APP_USER}"
|
||||||
|
|
||||||
|
ADMIN_USER="${APP_USER}"
|
||||||
|
ADMIN_EMAIL="admin@localhost"
|
||||||
|
ADMIN_PASS="ChangeMe"
|
||||||
|
|
||||||
|
CONF_BASE="/etc/${APP_USER}"
|
||||||
|
CERT_DIR="${CONF_BASE}/ssl"
|
||||||
|
CERT="${CERT_DIR}/cert.pem"
|
||||||
|
KEY="${CERT_DIR}/key.pem"
|
||||||
|
|
||||||
|
NGINX_SITE="/etc/nginx/sites-available/${APP_USER}.conf"
|
||||||
|
NGINX_SITE_LINK="/etc/nginx/sites-enabled/${APP_USER}.conf"
|
||||||
|
|
||||||
|
DB_NAME="${DB_NAME:-${APP_USER}}"
|
||||||
|
DB_USER="${DB_USER:-${APP_USER}}"
|
||||||
|
DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
|
||||||
|
GIT_REPO="${GIT_REPO:-http://10.10.20.81:3000/boban/mailwolt.git}"
|
||||||
|
GIT_BRANCH="${GIT_BRANCH:-main}"
|
||||||
|
|
||||||
|
NODE_SETUP="${NODE_SETUP:-deb}"
|
||||||
|
|
||||||
|
# ===== Styling =====
|
||||||
|
GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; CYAN="\033[1;36m"; GREY="\033[0;90m"; NC="\033[0m"
|
||||||
|
BAR="──────────────────────────────────────────────────────────────────────────────"
|
||||||
|
|
||||||
|
header() {
|
||||||
|
echo -e "${CYAN}${BAR}${NC}"
|
||||||
|
echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}"
|
||||||
|
echo -e "${CYAN} 8888b d8888 Y8P 888 888 o 888 888 888 ${NC}"
|
||||||
|
echo -e "${CYAN} 88888b.d88888 888 888 d8b 888 888 888 ${NC}"
|
||||||
|
echo -e "${CYAN} 888Y88888P888 8888b. 888 888 888 d888b 888 .d88b. 888 888888 ${NC}"
|
||||||
|
echo -e "${CYAN} 888 Y888P 888 '88b 888 888 888d88888b888 d88''88b 888 888 ${NC}"
|
||||||
|
echo -e "${CYAN} 888 Y8P 888 .d888888 888 888 88888P Y88888 888 888 888 888 ${NC}"
|
||||||
|
echo -e "${CYAN} 888 ' 888 888 888 888 888 8888P Y8888 Y88..88P 888 Y88b. ${NC}"
|
||||||
|
echo -e "${CYAN} 888 888 'Y888888 888 888 888P Y888 'Y88P' 888 'Y888 ${NC}"
|
||||||
|
echo -e "${CYAN}${BAR}${NC}"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
print_bootstrap_summary() {
|
||||||
|
local ip="$1"
|
||||||
|
local admin_user="$2"
|
||||||
|
local admin_pass="$3"
|
||||||
|
local GREEN="\033[1;32m"
|
||||||
|
local CYAN="\033[1;36m"
|
||||||
|
local GREY="\033[0;90m"
|
||||||
|
local YELLOW="\033[1;33m"
|
||||||
|
local RED="\033[1;31m"
|
||||||
|
local NC="\033[0m"
|
||||||
|
local BAR="${BAR:-──────────────────────────────────────────────────────────────────────────────}"
|
||||||
|
local scheme="http"
|
||||||
|
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then scheme="https"; fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
|
echo -e "${GREEN} ✔ ${APP_NAME} Bootstrap erfolgreich abgeschlossen${NC}"
|
||||||
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
|
echo -e " Bootstrap-Login (nur für ERSTEN Login & Wizard):"
|
||||||
|
echo -e " User: ${YELLOW}${admin_user}${NC}"
|
||||||
|
echo -e " Passwort: ${RED}${admin_pass}${NC}"
|
||||||
|
echo
|
||||||
|
echo -e " Aufruf: ${CYAN}${scheme}://${ip}${NC}"
|
||||||
|
echo -e " Laravel Root: ${GREY}${APP_DIR}${NC}"
|
||||||
|
echo -e " Nginx Site: ${GREY}${NGINX_SITE}${NC}"
|
||||||
|
echo -e " Self-signed Cert: ${GREY}${CERT_DIR}/{cert.pem,key.pem}${NC}"
|
||||||
|
echo -e " Postfix/Dovecot Ports aktiv: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}"
|
||||||
|
echo -e " Rspamd/OpenDKIM: ${GREY}aktiv (DKIM-Keys später im Wizard)${NC}"
|
||||||
|
echo -e " Monit (Watchdog): ${GREY}installiert, NICHT aktiviert${NC}"
|
||||||
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||||
|
err() { echo -e "${RED}[x]${NC} $*"; }
|
||||||
|
require_root() { [ "$(id -u)" -eq 0 ] || { err "Bitte als root ausführen."; exit 1; }; }
|
||||||
|
|
||||||
|
detect_ip() {
|
||||||
|
local ip
|
||||||
|
ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')" || true
|
||||||
|
[[ -n "${ip:-}" ]] || ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||||
|
[[ -n "${ip:-}" ]] || { err "Konnte Server-IP nicht ermitteln."; exit 1; }
|
||||||
|
echo "$ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
gen() { head -c 512 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c "${1:-28}" || true; }
|
||||||
|
pw() { gen 28; }
|
||||||
|
short() { gen 16; }
|
||||||
|
|
||||||
|
# ===== Start =====
|
||||||
|
require_root
|
||||||
|
header
|
||||||
|
|
||||||
|
SERVER_IP="$(detect_ip)"
|
||||||
|
MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}"
|
||||||
|
TZ="${TZ:-""}"
|
||||||
|
|
||||||
|
echo -e "${GREY}Server-IP erkannt: ${SERVER_IP}${NC}"
|
||||||
|
[ -n "$TZ" ] && { ln -fs "/usr/share/zoneinfo/${TZ}" /etc/localtime || true; }
|
||||||
|
|
||||||
|
log "Paketquellen aktualisieren…"
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -y
|
||||||
|
|
||||||
|
# ---- MariaDB-Workaround ----
|
||||||
|
log "MariaDB-Workaround vorbereiten…"
|
||||||
|
mkdir -p /etc/mysql /etc/mysql/mariadb.conf.d
|
||||||
|
[ -f /etc/mysql/mariadb.cnf ] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf
|
||||||
|
|
||||||
|
# ---- Basis-Pakete installieren ----
|
||||||
|
log "Pakete installieren… (dies kann einige Minuten dauern)"
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get -y -o Dpkg::Options::="--force-confdef" \
|
||||||
|
-o Dpkg::Options::="--force-confold" install \
|
||||||
|
postfix postfix-mysql \
|
||||||
|
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
|
||||||
|
mariadb-server mariadb-client \
|
||||||
|
redis-server \
|
||||||
|
rspamd \
|
||||||
|
opendkim opendkim-tools \
|
||||||
|
nginx \
|
||||||
|
php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql php-redis php-gd unzip curl \
|
||||||
|
composer git \
|
||||||
|
certbot python3-certbot-nginx \
|
||||||
|
fail2ban \
|
||||||
|
ca-certificates rsyslog sudo openssl netcat-openbsd monit acl
|
||||||
|
|
||||||
|
NGINX_HTTP2_SUPPORTED=0
|
||||||
|
if nginx -V 2>&1 | grep -q http_v2; then
|
||||||
|
NGINX_HTTP2_SUPPORTED=1
|
||||||
|
log "Nginx: HTTP/2-Unterstützung vorhanden ✅"
|
||||||
|
else
|
||||||
|
warn "Nginx: HTTP/2-Modul nicht gefunden – wechsle auf 'nginx-full'…"
|
||||||
|
apt-get install -y nginx-full || true
|
||||||
|
systemctl restart nginx || true
|
||||||
|
if nginx -V 2>&1 | grep -q http_v2; then
|
||||||
|
NGINX_HTTP2_SUPPORTED=1
|
||||||
|
log "Nginx: HTTP/2 jetzt verfügbar ✅"
|
||||||
|
else
|
||||||
|
warn "HTTP/2 weiterhin nicht verfügbar (verwende SSL ohne http2)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$NGINX_HTTP2_SUPPORTED" = "1" ]; then
|
||||||
|
NGINX_HTTP2_SUFFIX=" http2"
|
||||||
|
else
|
||||||
|
NGINX_HTTP2_SUFFIX=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Verzeichnisse / User =====
|
||||||
|
log "Verzeichnisse und Benutzer anlegen…"
|
||||||
|
mkdir -p "${CERT_DIR}" /etc/postfix/sql /etc/dovecot/conf.d /etc/rspamd/local.d /var/mail/vhosts
|
||||||
|
id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
|
||||||
|
chown -R vmail:vmail /var/mail
|
||||||
|
|
||||||
|
id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
|
||||||
|
usermod -a -G "$APP_GROUP" "$APP_USER"
|
||||||
|
|
||||||
|
# ===== Self-signed TLS (SAN = IP) =====
|
||||||
|
OSSL_CFG="${CERT_DIR}/openssl.cnf"
|
||||||
|
if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then
|
||||||
|
log "Erzeuge Self-Signed TLS Zertifikat (SAN=IP:${SERVER_IP})…"
|
||||||
|
install -d -m 0750 -o root -g "${APP_USER}" "${CERT_DIR}"
|
||||||
|
cat > "$OSSL_CFG" <<CFG
|
||||||
|
[req]
|
||||||
|
default_bits = 2048
|
||||||
|
prompt = no
|
||||||
|
default_md = sha256
|
||||||
|
req_extensions = req_ext
|
||||||
|
distinguished_name = dn
|
||||||
|
|
||||||
|
[dn]
|
||||||
|
CN = ${SERVER_IP}
|
||||||
|
O = ${APP_NAME}
|
||||||
|
C = DE
|
||||||
|
|
||||||
|
[req_ext]
|
||||||
|
subjectAltName = @alt_names
|
||||||
|
|
||||||
|
[alt_names]
|
||||||
|
IP.1 = ${SERVER_IP}
|
||||||
|
CFG
|
||||||
|
openssl req -x509 -newkey rsa:2048 -days 825 -nodes \
|
||||||
|
-keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
|
||||||
|
chown root:"${APP_USER}" "$KEY" "$CERT"
|
||||||
|
chmod 640 "$KEY" "$CERT"
|
||||||
|
chmod 750 "${CERT_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEV_USER="${SUDO_USER:-$USER}"
|
||||||
|
if command -v setfacl >/dev/null 2>&1; then
|
||||||
|
setfacl -m u:${DEV_USER}:x "${CONF_BASE}" "${CERT_DIR}" || true
|
||||||
|
setfacl -m u:${DEV_USER}:r "$CERT" "$KEY" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== MariaDB =====
|
||||||
|
log "MariaDB vorbereiten…"
|
||||||
|
systemctl enable --now mariadb
|
||||||
|
mysql -uroot <<SQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS ${DB_NAME}
|
||||||
|
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost';
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USER}'@'127.0.0.1';
|
||||||
|
ALTER USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
||||||
|
ALTER USER '${DB_USER}'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';
|
||||||
|
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
|
||||||
|
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'127.0.0.1';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
# ===== Postfix =====
|
||||||
|
postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
||||||
|
postconf -e "myorigin = \$myhostname"
|
||||||
|
postconf -e "mydestination = "
|
||||||
|
postconf -e "inet_interfaces = all"
|
||||||
|
postconf -e "inet_protocols = ipv4"
|
||||||
|
postconf -e "smtpd_banner = \$myhostname ESMTP"
|
||||||
|
postconf -e "smtpd_tls_cert_file = ${CERT}"
|
||||||
|
postconf -e "smtpd_tls_key_file = ${KEY}"
|
||||||
|
postconf -e "smtpd_tls_security_level = may"
|
||||||
|
postconf -e "smtp_tls_security_level = may"
|
||||||
|
postconf -e "smtpd_tls_received_header = yes"
|
||||||
|
postconf -e "disable_vrfy_command = yes"
|
||||||
|
postconf -e "smtpd_helo_required = yes"
|
||||||
|
postconf -e "milter_default_action = accept"
|
||||||
|
postconf -e "milter_protocol = 6"
|
||||||
|
postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
postconf -e "smtpd_sasl_type = dovecot"
|
||||||
|
postconf -e "smtpd_sasl_path = private/auth"
|
||||||
|
postconf -e "smtpd_sasl_auth_enable = yes"
|
||||||
|
postconf -e "smtpd_sasl_security_options = noanonymous"
|
||||||
|
postconf -e "smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination"
|
||||||
|
postconf -e "smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination"
|
||||||
|
postconf -M "smtp/inet=smtp inet n - n - - smtpd -o smtpd_peername_lookup=no -o smtpd_timeout=30s"
|
||||||
|
postconf -M "submission/inet=submission inet n - n - - smtpd -o syslog_name=postfix/submission -o smtpd_peername_lookup=no -o smtpd_tls_security_level=encrypt -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
||||||
|
postconf -M "smtps/inet=smtps inet n - n - - smtpd -o syslog_name=postfix/smtps -o smtpd_peername_lookup=no -o smtpd_tls_wrappermode=yes -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
||||||
|
postconf -M "pickup/unix=pickup unix n - y 60 1 pickup"
|
||||||
|
postconf -M "cleanup/unix=cleanup unix n - y - 0 cleanup"
|
||||||
|
postconf -M "qmgr/unix=qmgr unix n - n 300 1 qmgr"
|
||||||
|
|
||||||
|
install -d -o root -g postfix -m 750 /etc/postfix/sql
|
||||||
|
install -o root -g postfix -m 640 /dev/null /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
||||||
|
cat > /etc/postfix/sql/mysql-virtual-mailbox-maps.cf <<CONF
|
||||||
|
hosts = 127.0.0.1
|
||||||
|
user = ${DB_USER}
|
||||||
|
password = ${DB_PASS}
|
||||||
|
dbname = ${DB_NAME}
|
||||||
|
# query = SELECT 1 FROM mail_users u JOIN domains d ON d.id = u.domain_id WHERE u.email = '%s' AND u.is_active = 1 AND d.is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:postfix /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
||||||
|
chmod 640 /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
||||||
|
|
||||||
|
install -o root -g postfix -m 640 /dev/null /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
cat > /etc/postfix/sql/mysql-virtual-alias-maps.cf <<CONF
|
||||||
|
hosts = 127.0.0.1
|
||||||
|
user = ${DB_USER}
|
||||||
|
password = ${DB_PASS}
|
||||||
|
dbname = ${DB_NAME}
|
||||||
|
# query = SELECT destination FROM mail_aliases a JOIN domains d ON d.id = a.domain_id WHERE a.source = '%s' AND a.is_active = 1 AND d.is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
|
||||||
|
systemctl restart postfix
|
||||||
|
systemctl enable --now postfix
|
||||||
|
|
||||||
|
# ===== Dovecot =====
|
||||||
|
log "Dovecot konfigurieren…"
|
||||||
|
cat > /etc/dovecot/dovecot.conf <<'CONF'
|
||||||
|
!include_try /etc/dovecot/conf.d/*.conf
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat > /etc/dovecot/conf.d/10-mail.conf <<'CONF'
|
||||||
|
protocols = imap pop3 lmtp
|
||||||
|
mail_location = maildir:/var/mail/vhosts/%d/%n
|
||||||
|
namespace inbox { inbox = yes }
|
||||||
|
mail_privileged_group = mail
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF'
|
||||||
|
disable_plaintext_auth = yes
|
||||||
|
auth_mechanisms = plain login
|
||||||
|
!include_try auth-sql.conf.ext
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat > /etc/dovecot/dovecot-sql.conf.ext <<CONF
|
||||||
|
driver = mysql
|
||||||
|
connect = host=127.0.0.1 dbname=${DB_NAME} user=${DB_USER} password=${DB_PASS}
|
||||||
|
default_pass_scheme = BLF-CRYPT
|
||||||
|
# password_query = SELECT email AS user, password_hash AS password FROM mail_users WHERE email = '%u' AND is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
|
||||||
|
chmod 640 /etc/dovecot/dovecot-sql.conf.ext
|
||||||
|
|
||||||
|
cat > /etc/dovecot/conf.d/auth-sql.conf.ext <<'CONF'
|
||||||
|
passdb { driver = sql args = /etc/dovecot/dovecot-sql.conf.ext }
|
||||||
|
userdb { driver = static args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n }
|
||||||
|
CONF
|
||||||
|
sudo chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext
|
||||||
|
sudo chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext
|
||||||
|
|
||||||
|
cat > /etc/dovecot/conf.d/10-master.conf <<'CONF'
|
||||||
|
service lmtp {
|
||||||
|
unix_listener /var/spool/postfix/private/dovecot-lmtp { mode = 0600 user = postfix group = postfix }
|
||||||
|
}
|
||||||
|
service auth {
|
||||||
|
unix_listener /var/spool/postfix/private/auth { mode = 0660 user = postfix group = postfix }
|
||||||
|
}
|
||||||
|
service imap-login {
|
||||||
|
inet_listener imap { port = 143 }
|
||||||
|
inet_listener imaps { port = 993 ssl = yes }
|
||||||
|
}
|
||||||
|
service pop3-login {
|
||||||
|
inet_listener pop3 { port = 110 }
|
||||||
|
inet_listener pop3s { port = 995 ssl = yes }
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat > /etc/dovecot/conf.d/10-ssl.conf <<CONF
|
||||||
|
ssl = required
|
||||||
|
ssl_cert = <${CERT}
|
||||||
|
ssl_key = <${KEY}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
sudo mkdir -p /var/spool/postfix/private
|
||||||
|
sudo chown postfix:postfix /var/spool/postfix /var/spool/postfix/private
|
||||||
|
sudo chmod 0755 /var/spool/postfix /var/spool/postfix/private
|
||||||
|
|
||||||
|
sudo systemctl restart dovecot
|
||||||
|
sudo systemctl restart postfix
|
||||||
|
systemctl enable --now dovecot
|
||||||
|
|
||||||
|
# ===== Rspamd & OpenDKIM =====
|
||||||
|
log "Rspamd + OpenDKIM aktivieren…"
|
||||||
|
cat > /etc/rspamd/local.d/worker-controller.inc <<'CONF'
|
||||||
|
password = "admin";
|
||||||
|
bind_socket = "127.0.0.1:11334";
|
||||||
|
CONF
|
||||||
|
systemctl enable --now rspamd || true
|
||||||
|
|
||||||
|
cat > /etc/opendkim.conf <<'CONF'
|
||||||
|
Syslog yes
|
||||||
|
UMask 002
|
||||||
|
Mode sv
|
||||||
|
Socket inet:8891@127.0.0.1
|
||||||
|
Canonicalization relaxed/simple
|
||||||
|
On-BadSignature accept
|
||||||
|
On-Default accept
|
||||||
|
On-KeyNotFound accept
|
||||||
|
On-NoSignature accept
|
||||||
|
LogWhy yes
|
||||||
|
OversignHeaders From
|
||||||
|
# KeyTable / SigningTable werden nach dem Wizard gesetzt
|
||||||
|
CONF
|
||||||
|
systemctl enable --now opendkim || true
|
||||||
|
|
||||||
|
# ===== Redis =====
|
||||||
|
log "Redis absichern (Passwort setzen & nur localhost)…"
|
||||||
|
REDIS_CONF="/etc/redis/redis.conf"
|
||||||
|
REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
sed -i 's/^\s*#\?\s*bind .*/bind 127.0.0.1/' "$REDIS_CONF"
|
||||||
|
sed -i 's/^\s*#\?\s*protected-mode .*/protected-mode yes/' "$REDIS_CONF"
|
||||||
|
if grep -qE '^\s*#?\s*requirepass ' "$REDIS_CONF"; then
|
||||||
|
sed -i "s/^\s*#\?\s*requirepass .*/requirepass ${REDIS_PASS}/" "$REDIS_CONF"
|
||||||
|
else
|
||||||
|
printf "\nrequirepass %s\n" "${REDIS_PASS}" >> "$REDIS_CONF"
|
||||||
|
fi
|
||||||
|
systemctl enable --now redis-server
|
||||||
|
systemctl restart redis-server
|
||||||
|
|
||||||
|
# ===== Nginx =====
|
||||||
|
log "Nginx konfigurieren…"
|
||||||
|
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
|
||||||
|
|
||||||
|
detect_php_fpm_sock() {
|
||||||
|
for v in 8.3 8.2 8.1 8.0 7.4; do s="/run/php/php${v}-fpm.sock"; [ -S "$s" ] && { echo "$s"; return; }; done
|
||||||
|
[ -S "/run/php/php-fpm.sock" ] && { echo "/run/php/php-fpm.sock"; return; }
|
||||||
|
echo "127.0.0.1:9000"
|
||||||
|
}
|
||||||
|
PHP_FPM_SOCK="$(detect_php_fpm_sock)"
|
||||||
|
install -d -m 0755 /var/www/letsencrypt
|
||||||
|
|
||||||
|
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then
|
||||||
|
cat > "${NGINX_SITE}" <<CONF
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name _;
|
||||||
|
location ^~ /.well-known/acme-challenge/ { root /var/www/letsencrypt; allow all; }
|
||||||
|
return 301 https://\$host\$request_uri;
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
listen 443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
server_name _;
|
||||||
|
ssl_certificate ${CERT};
|
||||||
|
ssl_certificate_key ${KEY};
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
root ${APP_DIR}/public;
|
||||||
|
index index.php index.html;
|
||||||
|
access_log /var/log/nginx/${APP_USER}_ssl_access.log;
|
||||||
|
error_log /var/log/nginx/${APP_USER}_ssl_error.log;
|
||||||
|
client_max_body_size 25m;
|
||||||
|
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||||
|
location ~ \.php\$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:${PHP_FPM_SOCK}; }
|
||||||
|
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection "Upgrade";
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
else
|
||||||
|
cat > "${NGINX_SITE}" <<CONF
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name _;
|
||||||
|
root ${APP_DIR}/public;
|
||||||
|
index index.php index.html;
|
||||||
|
access_log /var/log/nginx/${APP_USER}_access.log;
|
||||||
|
error_log /var/log/nginx/${APP_USER}_error.log;
|
||||||
|
client_max_body_size 25m;
|
||||||
|
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||||
|
location ~ \.php\$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:${PHP_FPM_SOCK}; }
|
||||||
|
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
fi
|
||||||
|
|
||||||
|
ln -sf "${NGINX_SITE}" "${NGINX_SITE_LINK}"
|
||||||
|
if nginx -t; then
|
||||||
|
systemctl enable --now nginx
|
||||||
|
systemctl reload nginx || true
|
||||||
|
else
|
||||||
|
echo "[x] Nginx-Konfiguration fehlerhaft – bitte /var/log/nginx/* prüfen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Laravel Projekt =====
|
||||||
|
log "Laravel bereitstellen…"
|
||||||
|
mkdir -p "$(dirname "$APP_DIR")"
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "$(dirname "$APP_DIR")"
|
||||||
|
|
||||||
|
log "Git Repo vorbereiten…"
|
||||||
|
if [ "${GIT_REPO}" = "https://example.com/your-repo-placeholder.git" ]; then
|
||||||
|
if [ ! -d "${APP_DIR}" ] || [ -z "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]; then
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd /var/www && COMPOSER_ALLOW_SUPERUSER=0 composer create-project laravel/laravel ${APP_USER} --no-interaction"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ ! -d "${APP_DIR}/.git" ]; then
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}"
|
||||||
|
else
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "
|
||||||
|
set -e
|
||||||
|
cd ${APP_DIR}
|
||||||
|
git checkout ${GIT_BRANCH} 2>/dev/null || git checkout -B ${GIT_BRANCH}
|
||||||
|
git fetch --depth=1 origin ${GIT_BRANCH}
|
||||||
|
if git merge-base --is-ancestor HEAD origin/${GIT_BRANCH}; then
|
||||||
|
git pull --ff-only
|
||||||
|
else
|
||||||
|
echo '[i] Non-fast-forward erkannt – setze hart auf origin/${GIT_BRANCH}.' >&2
|
||||||
|
git reset --hard origin/${GIT_BRANCH}
|
||||||
|
git clean -fd
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
if [ -f "${APP_DIR}/composer.json" ]; then
|
||||||
|
if [ "${DEV_MODE}" = "1" ]; then
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist"
|
||||||
|
else
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist --no-dev"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Node / Frontend =====
|
||||||
|
if [ -f "${APP_DIR}/package.json" ]; then
|
||||||
|
log "Node/NPM installieren…"
|
||||||
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
NODE_MAJ=$(node -v | sed 's/^v//' | cut -d. -f1)
|
||||||
|
NODE_MIN=$(node -v | sed 's/^v//' | cut -d. -f2)
|
||||||
|
if [ "$NODE_MAJ" -lt 20 ] || { [ "$NODE_MAJ" -eq 20 ] && [ "$NODE_MIN" -lt 19 ]; }; then
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
|
fi
|
||||||
|
|
||||||
|
# .env anlegen & APP_KEY
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
|
||||||
|
if ! grep -q '^APP_KEY=' "${APP_DIR}/.env"; then echo "APP_KEY=" >> "${APP_DIR}/.env"; fi
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force || true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== .env füllen =====
|
||||||
|
ENV_FILE="${APP_DIR}/.env"
|
||||||
|
upsert_env () {
|
||||||
|
local key="$1" val="$2"
|
||||||
|
local esc_key esc_val
|
||||||
|
esc_key="$(printf '%s' "$key" | sed -e 's/[.[\*^$(){}+?|/]/\\&/g')"
|
||||||
|
esc_val="$(printf '%s' "$val" | sed -e 's/[&/]/\\&/g')"
|
||||||
|
if grep -qE "^[#[:space:]]*${esc_key}=" "$ENV_FILE"; then
|
||||||
|
sed -Ei "s|^[#[:space:]]*${esc_key}=.*|${key}=${esc_val}|g" "$ENV_FILE"
|
||||||
|
else
|
||||||
|
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then
|
||||||
|
upsert_env APP_URL "\"https://\${APP_HOST}\""
|
||||||
|
else
|
||||||
|
upsert_env APP_URL "\"http://\${APP_HOST}\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
upsert_env APP_HOST "${SERVER_IP}"
|
||||||
|
upsert_env APP_ADMIN_USER "${ADMIN_USER}"
|
||||||
|
upsert_env APP_ADMIN_EMAIL "${ADMIN_EMAIL}"
|
||||||
|
upsert_env APP_ADMIN_PASS "${ADMIN_PASS}"
|
||||||
|
upsert_env APP_NAME "${APP_NAME}"
|
||||||
|
upsert_env APP_ENV "${APP_ENV}"
|
||||||
|
upsert_env APP_DEBUG "${APP_DEBUG}"
|
||||||
|
|
||||||
|
upsert_env DB_CONNECTION "mysql"
|
||||||
|
upsert_env DB_HOST "127.0.0.1"
|
||||||
|
upsert_env DB_PORT "3306"
|
||||||
|
upsert_env DB_DATABASE "${DB_NAME}"
|
||||||
|
upsert_env DB_USERNAME "${DB_USER}"
|
||||||
|
upsert_env DB_PASSWORD "${DB_PASS}"
|
||||||
|
|
||||||
|
# -------- WICHTIG: Cache-Store auf REDIS setzen (verhindert DB-Tabellenfehler) --------
|
||||||
|
upsert_env CACHE_SETTINGS_STORE "redis"
|
||||||
|
upsert_env CACHE_STORE "redis" # <- HIER geändert (vorher: database)
|
||||||
|
upsert_env CACHE_DRIVER "redis"
|
||||||
|
upsert_env CACHE_PREFIX "${APP_USER}_cache"
|
||||||
|
|
||||||
|
upsert_env SESSION_DRIVER "redis"
|
||||||
|
upsert_env REDIS_CLIENT "phpredis"
|
||||||
|
upsert_env REDIS_HOST "127.0.0.1"
|
||||||
|
upsert_env REDIS_PORT "6379"
|
||||||
|
upsert_env REDIS_PASSWORD "${REDIS_PASS}"
|
||||||
|
upsert_env REDIS_DB "0"
|
||||||
|
upsert_env REDIS_CACHE_DB "1"
|
||||||
|
upsert_env REDIS_CACHE_CONNECTION "cache"
|
||||||
|
upsert_env REDIS_CACHE_LOCK_CONNECTION "default"
|
||||||
|
|
||||||
|
# Reverb / Vite
|
||||||
|
upsert_env BROADCAST_DRIVER "reverb"
|
||||||
|
upsert_env QUEUE_CONNECTION "redis"
|
||||||
|
upsert_env REVERB_APP_ID "${APP_USER}"
|
||||||
|
upsert_env REVERB_APP_KEY "${APP_USER}-yhp47tbt1aebhr1fgvgj"
|
||||||
|
upsert_env REVERB_APP_SECRET "${APP_USER}-ulrdt9agwzkqwqsunbnb"
|
||||||
|
upsert_env REVERB_HOST "127.0.0.1"
|
||||||
|
upsert_env REVERB_PORT "443"
|
||||||
|
upsert_env REVERB_SCHEME "https"
|
||||||
|
upsert_env REVERB_PATH "/ws"
|
||||||
|
upsert_env VITE_REVERB_APP_KEY "\${REVERB_APP_KEY}"
|
||||||
|
upsert_env VITE_REVERB_PORT "\${REVERB_PORT}"
|
||||||
|
upsert_env VITE_REVERB_SCHEME "\${REVERB_SCHEME}"
|
||||||
|
upsert_env VITE_REVERB_PATH "\${REVERB_PATH}"
|
||||||
|
|
||||||
|
if [ "${DEV_MODE}" = "1" ]; then
|
||||||
|
sed -i '/^# --- MailWolt DEV/,/^# --- \/MailWolt DEV/d' "${ENV_FILE}"
|
||||||
|
cat >> "${ENV_FILE}" <<CONF
|
||||||
|
# --- MailWolt DEV ---
|
||||||
|
VITE_DEV_HOST=127.0.0.1
|
||||||
|
VITE_DEV_PORT=5173
|
||||||
|
VITE_HMR_PROTOCOL=wss
|
||||||
|
VITE_HMR_CLIENT_PORT=443
|
||||||
|
VITE_HMR_HOST=${APP_HOST}
|
||||||
|
VITE_DEV_ORIGIN=${APP_URL}
|
||||||
|
# --- /MailWolt DEV ---
|
||||||
|
CONF
|
||||||
|
cat > "${APP_DIR}/vite.config.js" <<'JS'
|
||||||
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
|
import laravel from 'laravel-vite-plugin'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
export default ({ mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
|
const host = env.VITE_DEV_HOST || '127.0.0.1'
|
||||||
|
const port = Number(env.VITE_DEV_PORT || 5173)
|
||||||
|
const origin = env.VITE_DEV_ORIGIN || env.APP_URL || 'https://localhost'
|
||||||
|
const hmrHost = env.VITE_HMR_HOST || (new URL(origin)).hostname
|
||||||
|
return defineConfig({
|
||||||
|
plugins: [laravel({ input: ['resources/css/app.css','resources/js/app.js'], refresh: true }), tailwindcss()],
|
||||||
|
server: { host, port, https:false, strictPort:true,
|
||||||
|
hmr:{ protocol: env.VITE_HMR_PROTOCOL || 'wss', host: hmrHost, clientPort:Number(env.VITE_HMR_CLIENT_PORT||443) },
|
||||||
|
origin
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
JS
|
||||||
|
chown "${APP_USER}:${APP_GROUP}" "${APP_DIR}/vite.config.js"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Frontend Build =====
|
||||||
|
if [ -f "${APP_DIR}/package.json" ]; then
|
||||||
|
log "Frontend Build…"
|
||||||
|
if [ -f "${APP_DIR}/package-lock.json" ] || [ -f "${APP_DIR}/npm-shrinkwrap.json" ]; then
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
|
||||||
|
else
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm install"
|
||||||
|
fi
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build || true"
|
||||||
|
rm -f ${APP_DIR}/bootstrap/cache/*.php
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Rechte / PHP-FPM =====
|
||||||
|
APP_PW="${APP_PW:-changeme}"
|
||||||
|
if ! id -u "$APP_USER" >/dev/null 2>&1; then
|
||||||
|
adduser --disabled-password --gecos "" "$APP_USER"
|
||||||
|
echo "${APP_USER}:${APP_PW}" | chpasswd
|
||||||
|
fi
|
||||||
|
usermod -a -G "$APP_GROUP" "$APP_USER"
|
||||||
|
|
||||||
|
# Sichert, dass alle nötigen Ordner existieren (idempotent)
|
||||||
|
install -d -m 0775 "${APP_DIR}/storage" \
|
||||||
|
"${APP_DIR}/storage/framework" \
|
||||||
|
"${APP_DIR}/storage/framework/cache" \
|
||||||
|
"${APP_DIR}/storage/framework/cache/data" \
|
||||||
|
"${APP_DIR}/storage/framework/sessions" \
|
||||||
|
"${APP_DIR}/storage/framework/views" \
|
||||||
|
"${APP_DIR}/bootstrap/cache"
|
||||||
|
|
||||||
|
# Besitz & Rechte
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
|
||||||
|
find "$APP_DIR" -type d -exec chmod 775 {} \;
|
||||||
|
find "$APP_DIR" -type f -exec chmod 664 {} \;
|
||||||
|
|
||||||
|
[ -f "$APP_DIR/artisan" ] && chmod 755 "$APP_DIR/artisan"
|
||||||
|
[ -d "$APP_DIR/vendor/bin" ] && chmod -R 755 "$APP_DIR/vendor/bin"
|
||||||
|
[ -d "$APP_DIR/node_modules/.bin" ] && chmod -R 755 "$APP_DIR/node_modules/.bin"
|
||||||
|
[ -f "$APP_DIR/node_modules/vite/bin/vite.js" ] && chmod 755 "$APP_DIR/node_modules/vite/bin/vite.js"
|
||||||
|
find "$APP_DIR" -type f -name "*.sh" -exec chmod 755 {} \;
|
||||||
|
|
||||||
|
chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache
|
||||||
|
|
||||||
|
if command -v setfacl >/dev/null 2>&1; then
|
||||||
|
setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
||||||
|
setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
grep -q 'umask 002' /home/${APP_USER}/.profile 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.profile
|
||||||
|
grep -q 'umask 002' /home/${APP_USER}/.bashrc 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.bashrc
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
|
||||||
|
FPM_POOL="/etc/php/${PHPV}/fpm/pool.d/www.conf"
|
||||||
|
if [ -f "$FPM_POOL" ]; then
|
||||||
|
sed -i 's/^;*listen\.owner.*/listen.owner = www-data/' "$FPM_POOL"
|
||||||
|
sed -i 's/^;*listen\.group.*/listen.group = www-data/' "$FPM_POOL"
|
||||||
|
sed -i 's/^;*listen\.mode.*/listen.mode = 0660/' "$FPM_POOL"
|
||||||
|
systemctl restart php${PHPV}-fpm || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
IDE_USER="${SUDO_USER:-}"
|
||||||
|
if [ -n "$IDE_USER" ] && id "$IDE_USER" >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then
|
||||||
|
usermod -a -G "$APP_GROUP" "$IDE_USER" || true
|
||||||
|
setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR"
|
||||||
|
setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR"
|
||||||
|
echo -e "${GREEN}[i]${NC} Benutzer '${IDE_USER}' wurde für Schreibzugriff freigeschaltet (ACL + Gruppe ${APP_GROUP})."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Webstack neu laden
|
||||||
|
systemctl reload nginx || true
|
||||||
|
systemctl restart php*-fpm || true
|
||||||
|
|
||||||
|
# ===== Reverb systemd =====
|
||||||
|
cat > /etc/systemd/system/mailwolt-ws.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=MailWolt WebSocket Backend
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Environment=NODE_ENV=production WS_PORT=8080
|
||||||
|
User=${APP_USER}
|
||||||
|
Group=${APP_GROUP}
|
||||||
|
WorkingDirectory=${APP_DIR}
|
||||||
|
ExecStart=/usr/bin/php artisan reverb:start --host=127.0.0.1 --port=8080 --no-interaction
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=append:/var/log/mailwolt-ws.log
|
||||||
|
StandardError=append:/var/log/mailwolt-ws.log
|
||||||
|
KillSignal=SIGINT
|
||||||
|
TimeoutStopSec=15
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
chown root:root /etc/systemd/system/mailwolt-ws.service
|
||||||
|
chmod 644 /etc/systemd/system/mailwolt-ws.service
|
||||||
|
|
||||||
|
touch /var/log/mailwolt-ws.log
|
||||||
|
chown ${APP_USER}:${APP_GROUP} /var/log/mailwolt-ws.log
|
||||||
|
chmod 664 /var/log/mailwolt-ws.log
|
||||||
|
|
||||||
|
install -d -m 775 -o "${APP_USER}" -g "${APP_GROUP}" \
|
||||||
|
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache"
|
||||||
|
|
||||||
|
# ---- Laravel-Caches als APP_USER aufräumen (funktioniert jetzt ohne DB-Cache-Tabelle) ----
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache"
|
||||||
|
|
||||||
|
# HINWEIS: Wenn du unbedingt DATABASE als Cache-Store willst, dann vor obiger Zeile:
|
||||||
|
# sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan cache:table && php artisan migrate --force"
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan list --no-ansi 2>/dev/null | grep -qE '(^| )reverb:start( |$)'"; then
|
||||||
|
systemctl enable --now mailwolt-ws
|
||||||
|
else
|
||||||
|
systemctl disable --now mailwolt-ws >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== Monit =====
|
||||||
|
log "Monit konfigurieren & starten…"
|
||||||
|
cat > /etc/monit/monitrc <<'EOF'
|
||||||
|
set daemon 60
|
||||||
|
set logfile syslog facility log_daemon
|
||||||
|
|
||||||
|
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
||||||
|
start program = "/bin/systemctl start postfix"
|
||||||
|
stop program = "/bin/systemctl stop postfix"
|
||||||
|
if failed port 25 protocol smtp then restart
|
||||||
|
if failed port 465 type tcp ssl then restart
|
||||||
|
if failed port 587 type tcp then restart
|
||||||
|
|
||||||
|
check process dovecot with pidfile /var/run/dovecot/master.pid
|
||||||
|
start program = "/bin/systemctl start dovecot"
|
||||||
|
stop program = "/bin/systemctl stop dovecot"
|
||||||
|
if failed port 143 type tcp then restart
|
||||||
|
if failed port 993 type tcp ssl then restart
|
||||||
|
if failed port 110 type tcp then restart
|
||||||
|
if failed port 995 type tcp ssl then restart
|
||||||
|
|
||||||
|
check process mariadb with pidfile /var/run/mysqld/mysqld.pid
|
||||||
|
start program = "/bin/systemctl start mariadb"
|
||||||
|
stop program = "/bin/systemctl stop mariadb"
|
||||||
|
if failed port 3306 type tcp then restart
|
||||||
|
|
||||||
|
check process redis-server with pidfile /run/redis/redis-server.pid
|
||||||
|
start program = "/bin/systemctl start redis-server"
|
||||||
|
stop program = "/bin/systemctl stop redis-server"
|
||||||
|
if failed port 6379 type tcp then restart
|
||||||
|
|
||||||
|
check process rspamd with pidfile /run/rspamd/rspamd.pid
|
||||||
|
start program = "/bin/systemctl start rspamd"
|
||||||
|
stop program = "/bin/systemctl stop rspamd"
|
||||||
|
if failed port 11332 type tcp then restart
|
||||||
|
|
||||||
|
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
||||||
|
start program = "/bin/systemctl start opendkim"
|
||||||
|
stop program = "/bin/systemctl stop opendkim"
|
||||||
|
if failed port 8891 type tcp then restart
|
||||||
|
|
||||||
|
check process nginx with pidfile /run/nginx.pid
|
||||||
|
start program = "/bin/systemctl start nginx"
|
||||||
|
stop program = "/bin/systemctl stop nginx"
|
||||||
|
if failed port 80 type tcp then restart
|
||||||
|
if failed port 443 type tcp ssl then restart
|
||||||
|
|
||||||
|
check process mailwolt-ws matching "reverb:start"
|
||||||
|
start program = "/bin/systemctl start mailwolt-ws"
|
||||||
|
stop program = "/bin/systemctl stop mailwolt-ws"
|
||||||
|
if failed host 127.0.0.1 port 8080 type tcp for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then timeout
|
||||||
|
EOF
|
||||||
|
chmod 600 /etc/monit/monitrc
|
||||||
|
monit -t && systemctl enable --now monit
|
||||||
|
monit reload
|
||||||
|
monit summary || true
|
||||||
|
|
||||||
|
# ===== Healthchecks =====
|
||||||
|
GREEN="\033[1;32m"; RED="\033[1;31m"; GREY="\033[0;90m"; NC="\033[0m"
|
||||||
|
ok(){ echo -e " [${GREEN}OK${NC}]"; }
|
||||||
|
fail(){ echo -e " [${RED}FAIL${NC}]"; }
|
||||||
|
|
||||||
|
echo "[+] Quick-Healthchecks…"
|
||||||
|
printf " • MariaDB … " ; mysqladmin ping --silent >/dev/null 2>&1 && ok || fail
|
||||||
|
printf " • Redis … " ; if command -v redis-cli >/dev/null 2>&1; then
|
||||||
|
if [ -n "${REDIS_PASS:-}" ] && [ "${REDIS_PASS}" != "null" ]; then
|
||||||
|
redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q PONG && ok || fail
|
||||||
|
else
|
||||||
|
redis-cli ping 2>/dev/null | grep -q PONG && ok || fail
|
||||||
|
fi
|
||||||
|
else fail; fi
|
||||||
|
printf " • PHP-FPM … " ; if [[ "$PHP_FPM_SOCK" == 127.0.0.1:9000 ]]; then ss -ltn | grep -q ":9000 " && ok || fail; else [ -S "$PHP_FPM_SOCK" ] && ok || fail; fi
|
||||||
|
printf " • App … " ; if command -v curl >/dev/null 2>&1; then
|
||||||
|
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then curl -skI "https://127.0.0.1" >/dev/null 2>&1 && ok || fail; else curl -sI "http://127.0.0.1" >/dev/null 2>&1 && ok || fail; fi
|
||||||
|
else echo -e " ${GREY}(curl fehlt)${NC}"; fi
|
||||||
|
check_port(){ local label="$1" cmd="$2"; printf " • %-5s … " "$label"; timeout 8s bash -lc "$cmd" >/dev/null 2>&1 && ok || fail; }
|
||||||
|
check_port "25" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 25'
|
||||||
|
check_port "465" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:465 -quiet -ign_eof'
|
||||||
|
check_port "587" 'printf "EHLO x\r\nSTARTTLS\r\nQUIT\r\n" | openssl s_client -starttls smtp -connect 127.0.0.1:587 -quiet -ign_eof'
|
||||||
|
check_port "110" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 110'
|
||||||
|
check_port "995" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:995 -quiet -ign_eof'
|
||||||
|
check_port "143" 'printf ". LOGOUT\r\n" | nc -w 3 127.0.0.1 143'
|
||||||
|
check_port "993" 'printf ". LOGOUT\r\n" | openssl s_client -connect 127.0.0.1:993 -quiet -ign_eof'
|
||||||
|
|
||||||
|
print_bootstrap_summary "$SERVER_IP" "$ADMIN_USER" "$ADMIN_PASS"
|
||||||
|
|
||||||
|
# ===== MOTD =====
|
||||||
|
install -d /usr/local/bin
|
||||||
|
cat >/usr/local/bin/mw-motd <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
NC="\033[0m"; CY="\033[1;36m"; GR="\033[1;32m"; YE="\033[1;33m"; RD="\033[1;31m"; GY="\033[0;90m"
|
||||||
|
printf "\033[1;36m"
|
||||||
|
cat <<'ASCII'
|
||||||
|
:::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::
|
||||||
|
+:+:+: :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
|
||||||
|
+:+ +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
|
||||||
|
+#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+
|
||||||
|
+#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+
|
||||||
|
#+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+#
|
||||||
|
### ### ### ### ########### ########## ### ### ######## ########## ###
|
||||||
|
ASCII
|
||||||
|
printf "\033[0m\n"
|
||||||
|
now="$(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||||
|
fqdn="$(hostname -f 2>/dev/null || hostname)"
|
||||||
|
ip_int="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||||
|
ip_ext=""; command -v curl >/dev/null 2>&1 && ip_ext="$(curl -s --max-time 1 https://ifconfig.me || true)"
|
||||||
|
upt="$(uptime -p 2>/dev/null || true)"
|
||||||
|
cores="$(nproc 2>/dev/null || echo -n '?')"
|
||||||
|
mhz="$(LC_ALL=C lscpu 2>/dev/null | awk -F: '/MHz/{gsub(/ /,"",$2); printf("%.0f MHz",$2); exit}')"
|
||||||
|
[ -z "$mhz" ] && mhz="$(awk -F: '/cpu MHz/{printf("%.0f MHz",$2); exit}' /proc/cpuinfo 2>/dev/null)"
|
||||||
|
load="$(awk '{print $1" / "$2" / "$3}' /proc/loadavg 2>/dev/null)"
|
||||||
|
mem_total="$(awk '/MemTotal/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)"
|
||||||
|
mem_free="$(awk '/MemAvailable/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)"
|
||||||
|
svc_status(){ systemctl is-active --quiet "$1" && echo -e "${GR}OK${NC}" || echo -e "${RD}FAIL${NC}"; }
|
||||||
|
printf "${CY}Information as of:${NC} ${YE}%s${NC}\n" "$now"
|
||||||
|
printf "${GY}FQDN :${NC} %s\n" "$fqdn"
|
||||||
|
if [ -n "$ip_ext" ]; then printf "${GY}IP :${NC} %s ${GY}(external:${NC} %s${GY})${NC}\n" "${ip_int:-?}" "$ip_ext"; else printf "${GY}IP :${NC} %s\n" "${ip_int:-?}"; fi
|
||||||
|
printf "${GY}Uptime :${NC} %s\n" "${upt:-?}"
|
||||||
|
printf "${GY}Core(s) :${NC} %s core(s) at ${CY}%s${NC}\n" "$cores" "${mhz:-?}"
|
||||||
|
printf "${GY}Load :${NC} %s (1 / 5 / 15)\n" "${load:-?}"
|
||||||
|
printf "${GY}Memory :${NC} ${RD}%s${NC} ${GY}(free)${NC} / ${CY}%s${NC} ${GY}(total)${NC}\n" "${mem_free:-?}" "${mem_total:-?}"
|
||||||
|
echo
|
||||||
|
printf "${GY}Services :${NC} postfix: $(svc_status postfix) dovecot: $(svc_status dovecot) nginx: $(svc_status nginx) mariadb: $(svc_status mariadb) redis: $(svc_status redis)\n"
|
||||||
|
SH
|
||||||
|
chmod +x /usr/local/bin/mw-motd
|
||||||
|
|
||||||
|
if [ -d /etc/update-motd.d ]; then
|
||||||
|
cat >/etc/update-motd.d/10-mailwolt <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
/usr/local/bin/mw-motd
|
||||||
|
SH
|
||||||
|
chmod +x /etc/update-motd.d/10-mailwolt
|
||||||
|
[ -f /etc/update-motd.d/50-motd-news ] && chmod -x /etc/update-motd.d/50-motd-news || true
|
||||||
|
[ -f /etc/update-motd.d/80-livepatch ] && chmod -x /etc/update-motd.d/80-livepatch || true
|
||||||
|
else
|
||||||
|
cat >/etc/profile.d/10-mailwolt-motd.sh <<'SH'
|
||||||
|
case "$-" in *i*) /usr/local/bin/mw-motd ;; esac
|
||||||
|
SH
|
||||||
|
fi
|
||||||
|
: > /etc/motd 2>/dev/null || true
|
||||||
|
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
if [ -r /etc/mailwolt/installer.env ]; then
|
||||||
|
. /etc/mailwolt/installer.env
|
||||||
|
fi
|
||||||
|
|
||||||
|
REDIS_PASS="${REDIS_PASS:-}"
|
||||||
|
|
||||||
|
SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
find "$SCRIPTS_DIR/.." -type f -name "*.sh" -exec sed -i 's/\r$//' {} \; || true
|
||||||
|
|
||||||
|
log "Pakete installieren …"
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -y
|
||||||
|
# Minimal aber vollständig
|
||||||
|
apt-get -y -o Dpkg::Options::="--force-confdef" \
|
||||||
|
-o Dpkg::Options::="--force-confold" install \
|
||||||
|
postfix postfix-mysql dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
|
||||||
|
mariadb-server mariadb-client redis-server rspamd opendkim opendkim-tools opendmarc clamav \
|
||||||
|
clamav-daemon nginx php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql \
|
||||||
|
php-redis php-gd unzip curl composer git certbot python3-certbot-nginx fail2ban ca-certificates \
|
||||||
|
rsyslog sudo openssl monit acl netcat-openbsd jq sqlite3
|
||||||
|
|
||||||
|
# <<< Apache konsequent entfernen >>>
|
||||||
|
systemctl disable --now apache2 >/dev/null 2>&1 || true
|
||||||
|
apt-get -y purge 'apache2*' >/dev/null 2>&1 || true
|
||||||
|
apt-get -y autoremove >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
log "Systemuser/Dirs …"
|
||||||
|
id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
|
||||||
|
id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
|
||||||
|
# Systemuser/Dirs …
|
||||||
|
id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
|
||||||
|
id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
|
||||||
|
|
||||||
|
# --- FIX: Gruppen und Berechtigungen für Maildir und Dovecot-Zugriff ---
|
||||||
|
# vmail soll primär der Gruppe "mail" angehören, zusätzlich dovecot
|
||||||
|
usermod -g mail -a -G dovecot vmail || true
|
||||||
|
|
||||||
|
# App-User in relevante Gruppen
|
||||||
|
usermod -a -G "$APP_GROUP" "$APP_USER" || true
|
||||||
|
usermod -a -G mail,dovecot "$APP_USER" || true
|
||||||
|
|
||||||
|
# Maildir-Baum für Gruppe mail lesbar
|
||||||
|
chgrp -R mail /var/mail/vhosts || true
|
||||||
|
chmod -R g+rx /var/mail/vhosts || true
|
||||||
|
|
||||||
|
# ACLs setzen, damit neue Verzeichnisse automatisch passende Rechte bekommen
|
||||||
|
setfacl -R -m g:mail:rx /var/mail/vhosts || true
|
||||||
|
setfacl -dR -m g:mail:rx /var/mail/vhosts || true
|
||||||
|
usermod -a -G "$APP_GROUP" "$APP_USER" || true
|
||||||
|
install -d -m 0755 -o root -g root /var/www
|
||||||
|
install -d -m 0775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR"
|
||||||
|
|
||||||
|
SUDOERS_DKIM="/etc/sudoers.d/mailwolt-dkim"
|
||||||
|
cat > "${SUDOERS_DKIM}" <<'EOF'
|
||||||
|
Defaults!/usr/local/sbin/mailwolt-install-dkim !requiretty
|
||||||
|
Defaults!/usr/local/sbin/mailwolt-remove-dkim !requiretty
|
||||||
|
Defaults!/usr/bin/systemctl !requiretty
|
||||||
|
Defaults!/usr/bin/test !requiretty
|
||||||
|
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install-dkim *
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-remove-dkim *
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/bin/systemctl reload opendkim
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/bin/test *
|
||||||
|
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install-dkim *
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-remove-dkim *
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/bin/systemctl reload opendkim
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/bin/test *
|
||||||
|
EOF
|
||||||
|
chown root:root "${SUDOERS_DKIM}"
|
||||||
|
chmod 440 "${SUDOERS_DKIM}"
|
||||||
|
|
||||||
|
if ! visudo -c -f "${SUDOERS_DKIM}" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_DKIM} – entferne Datei."
|
||||||
|
rm -f "${SUDOERS_DKIM}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUDOERS_DOVEADM="/etc/sudoers.d/mailwolt-doveadm"
|
||||||
|
cat > "${SUDOERS_DOVEADM}" <<'EOF'
|
||||||
|
Cmnd_Alias MW_DOVEADM_STATUS = /usr/bin/doveadm -f tab mailbox status -u * messages INBOX, \
|
||||||
|
/usr/bin/doveadm mailbox status -u * messages INBOX
|
||||||
|
www-data ALL=(vmail) NOPASSWD: MW_DOVEADM_STATUS
|
||||||
|
mailwolt ALL=(vmail) NOPASSWD: MW_DOVEADM_STATUS
|
||||||
|
EOF
|
||||||
|
chown root:root "${SUDOERS_DOVEADM}"
|
||||||
|
chmod 440 "${SUDOERS_DOVEADM}"
|
||||||
|
visudo -c -f "${SUDOERS_DOVEADM}" || rm -f "${SUDOERS_DOVEADM}"
|
||||||
|
|
||||||
|
log "MariaDB include-fix …"
|
||||||
|
mkdir -p /etc/mysql/mariadb.conf.d
|
||||||
|
[[ -f /etc/mysql/mariadb.cnf ]] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf
|
||||||
|
|
||||||
|
log "Redis absichern …"
|
||||||
|
if [[ -z "${REDIS_PASS:-}" || "${REDIS_PASS}" == "changeme" ]]; then
|
||||||
|
REDIS_PASS="$(openssl rand -hex 16)"
|
||||||
|
export REDIS_PASS
|
||||||
|
log "Neues Redis-Passwort generiert."
|
||||||
|
fi
|
||||||
|
# Aktiven Redis-Config-Pfad aus systemd holen (Fallback: Standard)
|
||||||
|
REDIS_CONF="$(systemctl show -p ExecStart redis-server \
|
||||||
|
| sed -n 's/^ExecStart=.*redis-server[[:space:]]\+\([^[:space:]]\+\).*/\1/p')"
|
||||||
|
REDIS_CONF="${REDIS_CONF:-/etc/redis/redis.conf}"
|
||||||
|
|
||||||
|
# Bind + protected-mode hart setzen
|
||||||
|
sed -i 's/^[[:space:]]*#\?[[:space:]]*bind .*/bind 127.0.0.1/' "$REDIS_CONF"
|
||||||
|
sed -i 's/^[[:space:]]*#\?[[:space:]]*protected-mode .*/protected-mode yes/' "$REDIS_CONF"
|
||||||
|
|
||||||
|
# Vorherige requirepass-Zeilen entfernen (kommentiert/unkommentiert), dann neu schreiben
|
||||||
|
sed -i '/^[[:space:]]*#\?[[:space:]]*requirepass[[:space:]]\+/d' "$REDIS_CONF"
|
||||||
|
printf '\nrequirepass %s\n' "${REDIS_PASS}" >> "$REDIS_CONF"
|
||||||
|
|
||||||
|
# Dienst aktivieren & neu starten
|
||||||
|
systemctl enable --now redis-server
|
||||||
|
systemctl restart redis-server || true
|
||||||
|
|
||||||
|
# Sanity-Check (kein harter Exit, nur Log)
|
||||||
|
if redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q PONG; then
|
||||||
|
log "Redis mit Passwort OK."
|
||||||
|
else
|
||||||
|
warn "Redis PING mit Passwort fehlgeschlagen – bitte /etc/redis/redis.conf prüfen."
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
CONF_BASE="/etc/${APP_USER}"
|
||||||
|
CERT_DIR="${CONF_BASE}/ssl"
|
||||||
|
UI_SSL_DIR="/etc/ssl/ui"; WEBMAIL_SSL_DIR="/etc/ssl/webmail"; MAIL_SSL_DIR="/etc/ssl/mail"
|
||||||
|
UI_CERT="${UI_SSL_DIR}/fullchain.pem"; UI_KEY="${UI_SSL_DIR}/privkey.pem"
|
||||||
|
WEBMAIL_CERT="${WEBMAIL_SSL_DIR}/fullchain.pem"; WEBMAIL_KEY="${WEBMAIL_SSL_DIR}/privkey.pem"
|
||||||
|
MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"; MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
|
||||||
|
|
||||||
|
install -d -m 0750 "$CERT_DIR"
|
||||||
|
CERT="${CERT_DIR}/cert.pem"; KEY="${CERT_DIR}/key.pem"
|
||||||
|
|
||||||
|
if [[ ! -s "$CERT" || ! -s "$KEY" ]]; then
|
||||||
|
log "Self-signed Zertifikat erzeugen …"
|
||||||
|
OSSL_CFG="${CERT_DIR}/openssl.cnf"
|
||||||
|
cat > "$OSSL_CFG" <<CFG
|
||||||
|
[req]
|
||||||
|
default_bits=2048
|
||||||
|
prompt=no
|
||||||
|
default_md=sha256
|
||||||
|
req_extensions=req_ext
|
||||||
|
distinguished_name=dn
|
||||||
|
[dn]
|
||||||
|
CN=${SERVER_PUBLIC_IPV4}
|
||||||
|
O=${APP_NAME}
|
||||||
|
C=DE
|
||||||
|
[req_ext]
|
||||||
|
subjectAltName=@alt_names
|
||||||
|
[alt_names]
|
||||||
|
IP.1=${SERVER_PUBLIC_IPV4}
|
||||||
|
CFG
|
||||||
|
openssl req -x509 -newkey rsa:2048 -days 825 -nodes -keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
|
||||||
|
chgrp www-data "$CERT" "$KEY" || true
|
||||||
|
chmod 640 "$KEY" "$CERT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -m 0755 "$UI_SSL_DIR" "$WEBMAIL_SSL_DIR" "$MAIL_SSL_DIR"
|
||||||
|
ln -sf "$CERT" "$UI_CERT"; ln -sf "$KEY" "$UI_KEY"
|
||||||
|
ln -sf "$CERT" "$WEBMAIL_CERT";ln -sf "$KEY" "$WEBMAIL_KEY"
|
||||||
|
ln -sf "$CERT" "$MAIL_CERT"; ln -sf "$KEY" "$MAIL_KEY"
|
||||||
|
|
||||||
|
# --- Mail-Zertifikate: Rechte für Postfix & Dovecot -------------------------
|
||||||
|
# WICHTIG: Rechte am *Target* (KEY/CERT im $CERT_DIR) setzen, nicht an den Symlinks.
|
||||||
|
if [[ -f "$KEY" && -f "$CERT" ]]; then
|
||||||
|
echo "[+] Setze Berechtigungen für Mail-Zertifikate …"
|
||||||
|
# Key: nur root + Gruppe lesen. Gruppe → postfix
|
||||||
|
chgrp postfix "$KEY" || true
|
||||||
|
chmod 640 "$KEY" || true
|
||||||
|
# Dovecot zusätzlich Leserechte via ACL
|
||||||
|
setfacl -m u:dovecot:r "$KEY" || true
|
||||||
|
# Zertifikat darf weltweit lesbar sein
|
||||||
|
chmod 644 "$CERT" || true
|
||||||
|
else
|
||||||
|
echo "[!] Zertifikatsdateien fehlen: $KEY oder $CERT" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Optional: kurze Info, wohin verlinkt wurde
|
||||||
|
echo "[i] Mail TLS: $MAIL_CERT -> $CERT ; $MAIL_KEY -> $KEY"
|
||||||
|
|
@ -0,0 +1,588 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 2) POSIX-kompatibler Deploy-Wrapper (von Certbot aufgerufen)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
cat >/usr/local/sbin/mailwolt-deploy.sh <<'WRAP'
|
||||||
|
#!/bin/sh
|
||||||
|
# POSIX-safe Certbot deploy-hook (ohne bashisms)
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Installer-ENV laden (liefert UI_HOST/WEBMAIL_HOST/MAIL_HOSTNAME etc.)
|
||||||
|
if [ -r /etc/mailwolt/installer.env ]; then
|
||||||
|
. /etc/mailwolt/installer.env
|
||||||
|
fi
|
||||||
|
|
||||||
|
UI_HOST="${UI_HOST:-}"
|
||||||
|
WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
ACME_BASE="/etc/letsencrypt/live"
|
||||||
|
|
||||||
|
copy_cert() {
|
||||||
|
le_base="$1" # z.B. /etc/letsencrypt/live/ui.example.com
|
||||||
|
target_dir="$2" # z.B. /etc/ssl/ui
|
||||||
|
|
||||||
|
cert="${le_base}/fullchain.pem"
|
||||||
|
key="${le_base}/privkey.pem"
|
||||||
|
|
||||||
|
[ -s "$cert" ] || { echo "[deploy] missing $cert"; return 1; }
|
||||||
|
[ -s "$key" ] || { echo "[deploy] missing $key"; return 1; }
|
||||||
|
|
||||||
|
mkdir -p "$target_dir"
|
||||||
|
|
||||||
|
# echte Dateien (keine Symlinks), feste Rechte
|
||||||
|
install -m 0644 "$cert" "${target_dir}/fullchain.pem"
|
||||||
|
install -m 0600 "$key" "${target_dir}/privkey.pem"
|
||||||
|
|
||||||
|
echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_services() {
|
||||||
|
kind="$1" # ui | mail
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
if [ "$kind" = "mail" ]; then
|
||||||
|
systemctl reload postfix 2>/dev/null || true
|
||||||
|
systemctl reload dovecot 2>/dev/null || true
|
||||||
|
else
|
||||||
|
systemctl reload nginx 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Certbot-Kontext
|
||||||
|
LINEAGE="${RENEWED_LINEAGE:-}"
|
||||||
|
HOST=""
|
||||||
|
if [ -n "$LINEAGE" ]; then
|
||||||
|
HOST="$(basename "$LINEAGE")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
did_any=0
|
||||||
|
|
||||||
|
maybe_copy_for_host() {
|
||||||
|
host="$1"
|
||||||
|
dir="$2"
|
||||||
|
[ -n "$host" ] || return 0
|
||||||
|
|
||||||
|
# Fall A: Certbot liefert RENEWED_DOMAINS (Space-getrennt)
|
||||||
|
if [ -n "${RENEWED_DOMAINS:-}" ]; then
|
||||||
|
case " ${RENEWED_DOMAINS} " in
|
||||||
|
*" ${host} "*) copy_cert "${ACME_BASE}/${host}" "${dir}" && did_any=1 ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fall B: Erst-issue / kein RENEWED_DOMAINS → über LINEAGE matchen
|
||||||
|
if [ -n "$HOST" ] && [ "$HOST" = "$host" ]; then
|
||||||
|
copy_cert "${ACME_BASE}/${host}" "${dir}" && did_any=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gezieltes Kopieren
|
||||||
|
maybe_copy_for_host "$UI_HOST" "/etc/ssl/ui"
|
||||||
|
maybe_copy_for_host "$WEBMAIL_HOST" "/etc/ssl/webmail"
|
||||||
|
maybe_copy_for_host "$MAIL_HOSTNAME" "/etc/ssl/mail"
|
||||||
|
|
||||||
|
# Fallback (Erstlauf): kopiere vorhandene Lineages
|
||||||
|
if [ "$did_any" -eq 0 ]; then
|
||||||
|
[ -n "$UI_HOST" ] && [ -d "${ACME_BASE}/${UI_HOST}" ] && copy_cert "${ACME_BASE}/${UI_HOST}" "/etc/ssl/ui"
|
||||||
|
[ -n "$WEBMAIL_HOST" ] && [ -d "${ACME_BASE}/${WEBMAIL_HOST}" ] && copy_cert "${ACME_BASE}/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
||||||
|
[ -n "$MAIL_HOSTNAME" ] && [ -d "${ACME_BASE}/${MAIL_HOSTNAME}" ] && copy_cert "${ACME_BASE}/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TLSA-Refresh (tolerant falls App noch nicht ready)
|
||||||
|
if command -v php >/dev/null 2>&1 && [ -f /var/www/mailwolt/artisan ]; then
|
||||||
|
(cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Services neu laden
|
||||||
|
if [ -n "$HOST" ]; then
|
||||||
|
if [ -n "$MAIL_HOSTNAME" ] && [ "$HOST" = "$MAIL_HOSTNAME" ]; then
|
||||||
|
reload_services mail
|
||||||
|
else
|
||||||
|
reload_services ui
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
reload_services ui
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
WRAP
|
||||||
|
chmod +x /usr/local/sbin/mailwolt-deploy.sh
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 3) Certbot deploy-hook, der den Wrapper aufruft
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh <<'HOOK'
|
||||||
|
#!/bin/sh
|
||||||
|
exec /usr/local/sbin/mailwolt-deploy.sh
|
||||||
|
HOOK
|
||||||
|
chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh
|
||||||
|
|
||||||
|
|
||||||
|
log "[✓] MailWolt Deploy-Hook eingerichtet"
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
## Persistente Installer-Variablen (werden vom Wrapper gelesen)
|
||||||
|
#install -d -m 0755 /etc/mailwolt
|
||||||
|
#cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
#UI_HOST=${UI_HOST}
|
||||||
|
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
#BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
#LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
||||||
|
#SYSMAIL_SUB="${SYSMAIL_SUB}"
|
||||||
|
#SYSMAIL_DOMAIN="${SYSMAIL_DOMAIN}"
|
||||||
|
#DKIM_ENABLE="${DKIM_ENABLE}"
|
||||||
|
#DKIM_SELECTOR="${DKIM_SELECTOR}"
|
||||||
|
#DKIM_GENERATE="${DKIM_GENERATE}"
|
||||||
|
#APP_ENV=${APP_ENV:-production}
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
||||||
|
#
|
||||||
|
## 1) Wrapper, den Certbot bei Issue/Renew aufruft
|
||||||
|
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
## Installer-Variablen laden
|
||||||
|
#set +u
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
#set -u
|
||||||
|
#
|
||||||
|
#UI_HOST="${UI_HOST:-}"
|
||||||
|
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
#
|
||||||
|
## --- Kopieren statt Symlinks (damit Laravel lesen kann) ---------------------
|
||||||
|
#copy_cert() {
|
||||||
|
# local le_base="$1" target_dir="$2"
|
||||||
|
# local cert="${le_base}/fullchain.pem"
|
||||||
|
# local key="${le_base}/privkey.pem"
|
||||||
|
#
|
||||||
|
# [[ -s "$cert" && -s "$key" ]] || return 0
|
||||||
|
#
|
||||||
|
# install -d -m 0755 "$target_dir"
|
||||||
|
#
|
||||||
|
# # Vorhandene Symlinks entfernen, sonst kopierst du in die LE-Datei hinein
|
||||||
|
# [ -L "${target_dir}/fullchain.pem" ] && rm -f "${target_dir}/fullchain.pem"
|
||||||
|
# [ -L "${target_dir}/privkey.pem" ] && rm -f "${target_dir}/privkey.pem"
|
||||||
|
#
|
||||||
|
# # Echte Dateien ablegen
|
||||||
|
# install -m 0644 "$cert" "${target_dir}/fullchain.pem"
|
||||||
|
# install -m 0600 "$key" "${target_dir}/privkey.pem"
|
||||||
|
#
|
||||||
|
# echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
## Nur Domains bearbeiten, die in diesem Lauf betroffen sind.
|
||||||
|
## Bei manchen Distros ist RENEWED_DOMAINS auf Erst-issue leer -> Fallback nutzen.
|
||||||
|
#RDOMS=" ${RENEWED_DOMAINS:-} "
|
||||||
|
#did_any=0
|
||||||
|
#
|
||||||
|
#maybe_copy_for() {
|
||||||
|
# local host="$1" dir="$2"
|
||||||
|
# [[ -z "$host" ]] && return 0
|
||||||
|
# if [[ "$RDOMS" == *" ${host} "* ]]; then
|
||||||
|
# copy_cert "/etc/letsencrypt/live/${host}" "${dir}"
|
||||||
|
# did_any=1
|
||||||
|
# fi
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
## 1) Normalfall: nur die vom Certbot gemeldeten Hosts kopieren
|
||||||
|
#maybe_copy_for "$UI_HOST" "/etc/ssl/ui"
|
||||||
|
#maybe_copy_for "$WEBMAIL_HOST" "/etc/ssl/webmail"
|
||||||
|
#maybe_copy_for "$MAIL_HOSTNAME" "/etc/ssl/mail"
|
||||||
|
#
|
||||||
|
## 2) Fallback: Beim Erstlauf/Edge-Cases alles kopieren, was bereits existiert
|
||||||
|
#if [[ "$did_any" -eq 0 ]]; then
|
||||||
|
# [[ -n "$UI_HOST" && -d "/etc/letsencrypt/live/${UI_HOST}" ]] && copy_cert "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
||||||
|
# [[ -n "$WEBMAIL_HOST" && -d "/etc/letsencrypt/live/${WEBMAIL_HOST}" ]] && copy_cert "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
||||||
|
# [[ -n "$MAIL_HOSTNAME" && -d "/etc/letsencrypt/live/${MAIL_HOSTNAME}"]] && copy_cert "/etc/letsencrypt/live/${MAIL_HOSTNAME}"/etc/ssl/mail
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Optional: TLSA via Laravel (tolerant, falls App noch nicht gebaut)
|
||||||
|
#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ] && [ -f /var/www/mailwolt/artisan ]; then
|
||||||
|
# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Nginx nur neu laden, wenn aktiv
|
||||||
|
#if systemctl is-active --quiet nginx; then
|
||||||
|
# systemctl reload nginx || true
|
||||||
|
#fi
|
||||||
|
#WRAP
|
||||||
|
#chmod +x /usr/local/sbin/mw-deploy.sh
|
||||||
|
#
|
||||||
|
## 2) Certbot-Deploy-Hook: ruft den Wrapper bei jeder erfolgreichen Ausstellung/Renew auf
|
||||||
|
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh <<'HOOK'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#exec /usr/local/sbin/mw-deploy.sh
|
||||||
|
#HOOK
|
||||||
|
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh
|
||||||
|
#
|
||||||
|
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
## Persistente Installer-Variablen (werden vom Wrapper gelesen)
|
||||||
|
#install -d -m 0755 /etc/mailwolt
|
||||||
|
#cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
#UI_HOST=${UI_HOST}
|
||||||
|
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
#BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
#LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
||||||
|
#APP_ENV=${APP_ENV:-production}
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
||||||
|
#
|
||||||
|
## 1) Wrapper, den Certbot bei Issue/Renew aufruft
|
||||||
|
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
## Installer-Variablen laden
|
||||||
|
#set +u
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
#set -u
|
||||||
|
#
|
||||||
|
#UI_HOST="${UI_HOST:-}"
|
||||||
|
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
#
|
||||||
|
## --- Kopieren statt Symlinks (damit Laravel lesen kann) ---------------------
|
||||||
|
#copy_cert() {
|
||||||
|
# local le_base="$1" target_dir="$2"
|
||||||
|
# local cert="${le_base}/fullchain.pem"
|
||||||
|
# local key="${le_base}/privkey.pem"
|
||||||
|
#
|
||||||
|
# [[ -s "$cert" && -s "$key" ]] || return 0
|
||||||
|
#
|
||||||
|
# # Zielordner sicherstellen
|
||||||
|
# install -d -m 0755 "$target_dir"
|
||||||
|
#
|
||||||
|
# # Falls vorher Symlinks existieren → entfernen, sonst würde "install" das Ziel des Links überschreiben
|
||||||
|
# [ -L "${target_dir}/fullchain.pem" ] && rm -f "${target_dir}/fullchain.pem"
|
||||||
|
# [ -L "${target_dir}/privkey.pem" ] && rm -f "${target_dir}/privkey.pem"
|
||||||
|
#
|
||||||
|
# # KOPIEREN mit sauberen Rechten (Chain world-readable, Key nur root)
|
||||||
|
# install -m 0644 "$cert" "${target_dir}/fullchain.pem"
|
||||||
|
# install -m 0600 "$key" "${target_dir}/privkey.pem"
|
||||||
|
#
|
||||||
|
# echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
## Nur für Domains arbeiten, die in diesem Lauf betroffen sind
|
||||||
|
#RDOMS=" ${RENEWED_DOMAINS:-} "
|
||||||
|
#
|
||||||
|
## UI
|
||||||
|
#if [[ -n "$UI_HOST" && "$RDOMS" == *" ${UI_HOST} "* ]]; then
|
||||||
|
# copy_cert "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
||||||
|
#fi
|
||||||
|
## Webmail
|
||||||
|
#if [[ -n "$WEBMAIL_HOST" && "$RDOMS" == *" ${WEBMAIL_HOST} "* ]]; then
|
||||||
|
# copy_cert "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
||||||
|
#fi
|
||||||
|
## MX
|
||||||
|
#if [[ -n "$MAIL_HOSTNAME" && "$RDOMS" == *" ${MAIL_HOSTNAME} "* ]]; then
|
||||||
|
# copy_cert "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Optional: TLSA via Laravel (still tolerant, falls App noch nicht gebaut)
|
||||||
|
#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ] && [ -f /var/www/mailwolt/artisan ]; then
|
||||||
|
# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Nginx nur neu laden, wenn aktiv
|
||||||
|
#if systemctl is-active --quiet nginx; then
|
||||||
|
# systemctl reload nginx || true
|
||||||
|
#fi
|
||||||
|
#WRAP
|
||||||
|
#chmod +x /usr/local/sbin/mw-deploy.sh
|
||||||
|
#
|
||||||
|
## 2) Certbot-Deploy-Hook: ruft den Wrapper bei jeder erfolgreichen Ausstellung/Renew auf
|
||||||
|
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#exec /usr/local/sbin/mw-deploy.sh
|
||||||
|
#HOOK
|
||||||
|
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
||||||
|
#
|
||||||
|
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
#install -d -m 0755 /etc/mailwolt
|
||||||
|
#cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
#UI_HOST=${UI_HOST}
|
||||||
|
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
#BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
#LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
||||||
|
#APP_ENV=${APP_ENV:-production}
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
||||||
|
#
|
||||||
|
## 1) Wrapper, den Certbot bei Issue/Renew aufruft
|
||||||
|
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
## Installer-Variablen laden (UI_HOST, WEBMAIL_HOST, MAIL_HOSTNAME, optional LE_EMAIL etc.)
|
||||||
|
#set +u
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
#set -u
|
||||||
|
#
|
||||||
|
#UI_HOST="${UI_HOST:-}"
|
||||||
|
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
#
|
||||||
|
#link_if() {
|
||||||
|
# local le_base="$1" target_dir="$2"
|
||||||
|
# local cert="${le_base}/fullchain.pem"
|
||||||
|
# local key="${le_base}/privkey.pem"
|
||||||
|
# [[ -s "$cert" && -s "$key" ]] || return 0
|
||||||
|
# install -d -m 0755 "$target_dir"
|
||||||
|
# ln -sf "$cert" "${target_dir}/fullchain.pem"
|
||||||
|
# ln -sf "$key" "${target_dir}/privkey.pem"
|
||||||
|
# chmod 644 "${target_dir}/fullchain.pem" 2>/dev/null || true
|
||||||
|
# chmod 600 "${target_dir}/privkey.pem" 2>/dev/null || true
|
||||||
|
# echo "[+] Linked ${target_dir} -> ${le_base}"
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
## Nur für Domains arbeiten, die im aktuellen Lauf erneuert/ausgestellt wurden
|
||||||
|
#RDOMS=" ${RENEWED_DOMAINS:-} "
|
||||||
|
#
|
||||||
|
## UI
|
||||||
|
#if [[ -n "$UI_HOST" && "$RDOMS" == *" ${UI_HOST} "* ]]; then
|
||||||
|
# link_if "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
||||||
|
#fi
|
||||||
|
## Webmail
|
||||||
|
#if [[ -n "$WEBMAIL_HOST" && "$RDOMS" == *" ${WEBMAIL_HOST} "* ]]; then
|
||||||
|
# link_if "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
||||||
|
#fi
|
||||||
|
## MX
|
||||||
|
#if [[ -n "$MAIL_HOSTNAME" && "$RDOMS" == *" ${MAIL_HOSTNAME} "* ]]; then
|
||||||
|
# link_if "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Optional: TLSA via Laravel, falls App schon vorhanden (sonst still überspringen)
|
||||||
|
#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ]; then
|
||||||
|
# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Nginx nur neu laden, wenn aktiv
|
||||||
|
#if systemctl is-active --quiet nginx; then
|
||||||
|
# systemctl reload nginx || true
|
||||||
|
#fi
|
||||||
|
#WRAP
|
||||||
|
#chmod +x /usr/local/sbin/mw-deploy.sh
|
||||||
|
#
|
||||||
|
## 2) Certbot-Deploy-Hooks einrichten (ruft nur den Wrapper auf)
|
||||||
|
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#exec /usr/local/sbin/mw-deploy.sh
|
||||||
|
#HOOK
|
||||||
|
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
||||||
|
#
|
||||||
|
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
||||||
|
#
|
||||||
|
## 1) Wrapper-Skript, das Symlinks setzt und Nginx reloaded
|
||||||
|
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
#link_if() {
|
||||||
|
# local le_base="$1" target_dir="$2"
|
||||||
|
# local cert="${le_base}/fullchain.pem"
|
||||||
|
# local key="${le_base}/privkey.pem"
|
||||||
|
# [[ -s "$cert" && -s "$key" ]] || return 0
|
||||||
|
# install -d -m 0755 "$target_dir"
|
||||||
|
# ln -sf "$cert" "${target_dir}/fullchain.pem"
|
||||||
|
# ln -sf "$key" "${target_dir}/privkey.pem"
|
||||||
|
# chmod 644 "${target_dir}/fullchain.pem" 2>/dev/null || true
|
||||||
|
# chmod 600 "${target_dir}/privkey.pem" 2>/dev/null || true
|
||||||
|
# echo "[+] Linked ${target_dir} -> ${le_base}"
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#UI_HOST="${UI_HOST:-}"
|
||||||
|
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
#
|
||||||
|
#[[ -n "$UI_HOST" ]] && link_if "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
||||||
|
#[[ -n "$WEBMAIL_HOST" ]] && link_if "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
||||||
|
#[[ -n "$MAIL_HOSTNAME" ]] && link_if "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
||||||
|
#
|
||||||
|
#if systemctl is-active --quiet nginx; then
|
||||||
|
# systemctl reload nginx || true
|
||||||
|
#fi
|
||||||
|
#WRAP
|
||||||
|
#
|
||||||
|
#chmod +x /usr/local/sbin/mw-deploy.sh
|
||||||
|
#
|
||||||
|
## 2) Certbot Deploy-Hook-Verzeichnis + Symlink für Renewals
|
||||||
|
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#exec /usr/local/sbin/mw-deploy.sh
|
||||||
|
#HOOK
|
||||||
|
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
||||||
|
#
|
||||||
|
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
||||||
|
#
|
||||||
|
###!/usr/bin/env bash
|
||||||
|
##set -euo pipefail
|
||||||
|
##source ./lib.sh
|
||||||
|
##
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
### 21-le-deploy-hook.sh
|
||||||
|
### • legt /etc/mailwolt/installer.env an (falls fehlt)
|
||||||
|
### • erzeugt Deploy-Hooks:
|
||||||
|
### - 50-mailwolt-symlinks.sh → verlinkt LE-Zerts nach /etc/ssl/{ui,webmail,mail}
|
||||||
|
### - 60-mailwolt-tlsa.sh → aktualisiert TLSA (3 1 1) für MX bei jedem Renew
|
||||||
|
### • KEIN Reload von Postfix/Dovecot (kommt später im Installer)
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
##
|
||||||
|
### 0) Hostnamen persistent speichern (für spätere Deploys)
|
||||||
|
##install -d -m 0755 /etc/mailwolt
|
||||||
|
##if [[ ! -f /etc/mailwolt/installer.env ]]; then
|
||||||
|
## cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
##UI_HOST=${UI_HOST}
|
||||||
|
##WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
##MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
##EOF
|
||||||
|
## echo "[+] /etc/mailwolt/installer.env erstellt."
|
||||||
|
##fi
|
||||||
|
##
|
||||||
|
### 1) Deploy-Hooks-Verzeichnis anlegen
|
||||||
|
##install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
||||||
|
##
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
### 2) 50-mailwolt-symlinks.sh
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
##cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<HOOK
|
||||||
|
###!/usr/bin/env bash
|
||||||
|
##set -euo pipefail
|
||||||
|
##
|
||||||
|
##UI_LE="/etc/letsencrypt/live/${UI_HOST}"
|
||||||
|
##WEBMAIL_LE="/etc/letsencrypt/live/${WEBMAIL_HOST}"
|
||||||
|
##MX_LE="/etc/letsencrypt/live/${MAIL_HOSTNAME}"
|
||||||
|
##
|
||||||
|
##UI_SSL_DIR="/etc/ssl/ui"
|
||||||
|
##WEBMAIL_SSL_DIR="/etc/ssl/webmail"
|
||||||
|
##MAIL_SSL_DIR="/etc/ssl/mail"
|
||||||
|
##
|
||||||
|
### Zielverzeichnisse anlegen (einmalig)
|
||||||
|
##install -d -m 0755 "\$UI_SSL_DIR" "\$WEBMAIL_SSL_DIR" "\$MAIL_SSL_DIR"
|
||||||
|
##
|
||||||
|
##link_if() {
|
||||||
|
## local le_base="\$1" target_dir="\$2"
|
||||||
|
## local cert="\${le_base}/fullchain.pem"
|
||||||
|
## local key="\${le_base}/privkey.pem"
|
||||||
|
## [[ -s "\$cert" && -s "\$key" ]] || return 0
|
||||||
|
## ln -sf "\$cert" "\${target_dir}/fullchain.pem"
|
||||||
|
## ln -sf "\$key" "\${target_dir}/privkey.pem"
|
||||||
|
## chmod 644 "\${target_dir}/fullchain.pem" 2>/dev/null || true
|
||||||
|
## chmod 600 "\${target_dir}/privkey.pem" 2>/dev/null || true
|
||||||
|
## echo "[+] Linked \${target_dir} -> \${le_base}"
|
||||||
|
##}
|
||||||
|
##
|
||||||
|
### Verlinken (nur wenn Host konfiguriert)
|
||||||
|
##[[ -n "${UI_HOST}" ]] && link_if "\$UI_LE" "\$UI_SSL_DIR"
|
||||||
|
##[[ -n "${WEBMAIL_HOST}" ]] && link_if "\$WEBMAIL_LE" "\$WEBMAIL_SSL_DIR"
|
||||||
|
##[[ -n "${MAIL_HOSTNAME}" ]] && link_if "\$MX_LE" "\$MAIL_SSL_DIR"
|
||||||
|
##
|
||||||
|
### Nur reloaden, wenn Nginx aktiv ist (Installer startet ihn später erst)
|
||||||
|
##if systemctl is-active --quiet nginx; then
|
||||||
|
## systemctl reload nginx || true
|
||||||
|
##fi
|
||||||
|
##HOOK
|
||||||
|
##chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
||||||
|
##
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
### 3) 60-mailwolt-tlsa.sh
|
||||||
|
### → nutzt Laravel, falls vorhanden; sonst Fallback mit OpenSSL.
|
||||||
|
### → schreibt nur, wenn sich der Hash geändert hat (idempotent)
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
##cat >/etc/letsencrypt/renewal-hooks/deploy/60-mailwolt-tlsa.sh <<'HOOK'
|
||||||
|
###!/usr/bin/env bash
|
||||||
|
##set -euo pipefail
|
||||||
|
##
|
||||||
|
### installer.env lesen
|
||||||
|
##set +u
|
||||||
|
##[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
##set -u
|
||||||
|
##
|
||||||
|
##APP_ENV_VAL="${APP_ENV:-production}"
|
||||||
|
##BASE_DOMAIN_VAL="${BASE_DOMAIN:-example.com}"
|
||||||
|
##
|
||||||
|
##case "$APP_ENV_VAL" in
|
||||||
|
## local|dev|development) exit 0 ;;
|
||||||
|
##esac
|
||||||
|
##[ "$BASE_DOMAIN_VAL" = "example.com" ] && exit 0
|
||||||
|
##
|
||||||
|
##MX_HOST="${MAIL_HOSTNAME:-}"
|
||||||
|
##SERVICE="_25._tcp"
|
||||||
|
##DNS_DIR="/etc/mailwolt/dns"
|
||||||
|
##OUT_FILE="${DNS_DIR}/${MX_HOST}.tlsa.txt"
|
||||||
|
##
|
||||||
|
### Nur reagieren, wenn MX-Zertifikat betroffen war
|
||||||
|
##case " ${RENEWED_DOMAINS:-} " in
|
||||||
|
## *" ${MX_HOST} "*) ;;
|
||||||
|
## *) exit 0 ;;
|
||||||
|
##esac
|
||||||
|
##
|
||||||
|
##CERT="${RENEWED_LINEAGE}/fullchain.pem"
|
||||||
|
##[ -s "$CERT" ] || exit 0
|
||||||
|
##
|
||||||
|
### Wenn Laravel vorhanden ist → interner Command (DB + Datei idempotent)
|
||||||
|
##if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ]; then
|
||||||
|
## cd /var/www/mailwolt || exit 0
|
||||||
|
## php artisan dns:tlsa:refresh || true
|
||||||
|
## exit 0
|
||||||
|
##fi
|
||||||
|
##
|
||||||
|
### Fallback: nur Datei aktualisieren, wenn Hash sich ändert
|
||||||
|
##HASH="$(openssl x509 -in "$CERT" -noout -pubkey \
|
||||||
|
## | openssl pkey -pubin -outform DER \
|
||||||
|
## | openssl dgst -sha256 | sed 's/^.*= //')"
|
||||||
|
##NEW_LINE="${SERVICE}.${MX_HOST}. IN TLSA 3 1 1 ${HASH}"
|
||||||
|
##
|
||||||
|
##mkdir -p "$DNS_DIR"
|
||||||
|
##
|
||||||
|
##if [ -r "$OUT_FILE" ] && grep -q "IN TLSA" "$OUT_FILE"; then
|
||||||
|
## if grep -q "$HASH" "$OUT_FILE"; then
|
||||||
|
## echo "[TLSA] Unverändert – kein Update nötig."
|
||||||
|
## exit 0
|
||||||
|
## fi
|
||||||
|
##fi
|
||||||
|
##
|
||||||
|
##echo "$NEW_LINE" > "$OUT_FILE"
|
||||||
|
##echo "[TLSA] Aktualisiert: $NEW_LINE"
|
||||||
|
##HOOK
|
||||||
|
##chmod +x /etc/letsencrypt/renewal-hooks/deploy/60-mailwolt-tlsa.sh
|
||||||
|
##
|
||||||
|
### ────────────────────────────────────────────────────────────────────────────
|
||||||
|
##echo "[✓] Deploy-Hooks installiert."
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Installiere DKIM-Helper …"
|
||||||
|
|
||||||
|
install -d -m 0755 /usr/local/sbin
|
||||||
|
|
||||||
|
cat >/usr/local/sbin/mailwolt-install-dkim <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOMAIN="$1" # z.B. sysmail.toastra.com
|
||||||
|
SELECTOR="${2:-mwl1}"
|
||||||
|
|
||||||
|
[[ -n "$DOMAIN" ]] || { echo "Usage: $0 <domain> [selector]"; exit 2; }
|
||||||
|
|
||||||
|
KEYDIR="/etc/opendkim/keys/${DOMAIN}"
|
||||||
|
PRIV="${KEYDIR}/${SELECTOR}.private"
|
||||||
|
TXT="${KEYDIR}/${SELECTOR}.txt"
|
||||||
|
|
||||||
|
install -d -m 0750 -o opendkim -g opendkim "$KEYDIR"
|
||||||
|
|
||||||
|
if [[ ! -s "$PRIV" ]]; then
|
||||||
|
opendkim-genkey -b 2048 -s "$SELECTOR" -d "$DOMAIN" -D "$KEYDIR"
|
||||||
|
chown opendkim:opendkim "$PRIV"
|
||||||
|
chmod 600 "$PRIV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
grep -q "^${SELECTOR}\._domainkey\.${DOMAIN} " /etc/opendkim/KeyTable 2>/dev/null \
|
||||||
|
|| echo "${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:${PRIV}" >> /etc/opendkim/KeyTable
|
||||||
|
|
||||||
|
grep -q "^\*@${DOMAIN} " /etc/opendkim/SigningTable 2>/dev/null \
|
||||||
|
|| echo "*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}" >> /etc/opendkim/SigningTable
|
||||||
|
|
||||||
|
install -d -m 0755 /etc/mailwolt/dns
|
||||||
|
[[ -s "$TXT" ]] && cp -f "$TXT" "/etc/mailwolt/dns/dkim-${DOMAIN}.txt" || true
|
||||||
|
|
||||||
|
systemctl restart opendkim
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "[✓] DKIM-Helper installiert: /usr/local/sbin/mailwolt-install-dkim"
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "MariaDB vorbereiten …"
|
||||||
|
systemctl enable --now mariadb
|
||||||
|
mysql -uroot <<SQL
|
||||||
|
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
||||||
|
CREATE USER IF NOT EXISTS '${DB_USER}'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';
|
||||||
|
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
|
||||||
|
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'127.0.0.1';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
SQL
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
MAIL_SSL_DIR="/etc/ssl/mail"
|
||||||
|
MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"
|
||||||
|
MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
|
||||||
|
|
||||||
|
log "Postfix konfigurieren …"
|
||||||
|
|
||||||
|
# --- TLS-Dateirechte (falls du sie in /etc/mailwolt/ssl spiegelst) -----------
|
||||||
|
if [[ -e "${MAIL_KEY}" ]]; then
|
||||||
|
chgrp -R postfix /etc/mailwolt/ssl || true
|
||||||
|
chmod 750 /etc/mailwolt/ssl || true
|
||||||
|
chmod 640 /etc/mailwolt/ssl/key.pem /etc/mailwolt/ssl/cert.pem || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Basiskonfiguration -------------------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
||||||
|
/usr/sbin/postconf -e "myorigin = \$myhostname"
|
||||||
|
/usr/sbin/postconf -e "mydestination = "
|
||||||
|
/usr/sbin/postconf -e "inet_interfaces = all"
|
||||||
|
/usr/sbin/postconf -e "inet_protocols = all"
|
||||||
|
/usr/sbin/postconf -e "smtpd_banner = \$myhostname ESMTP"
|
||||||
|
|
||||||
|
# --- TLS ----------------------------------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_cert_file = ${MAIL_CERT}"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_key_file = ${MAIL_KEY}"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_security_level = may"
|
||||||
|
/usr/sbin/postconf -e "smtpd_use_tls = yes"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_received_header = yes"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_protocols = !SSLv2,!SSLv3"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_mandatory_protocols = !SSLv2,!SSLv3"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_loglevel = 1"
|
||||||
|
/usr/sbin/postconf -e "smtp_tls_security_level = may"
|
||||||
|
/usr/sbin/postconf -e "smtp_tls_loglevel = 1"
|
||||||
|
|
||||||
|
DH_FILE="/etc/ssl/private/dhparam.pem"
|
||||||
|
if [[ ! -s "$DH_FILE" ]]; then
|
||||||
|
log "Generiere 2048-Bit DH-Parameter …"
|
||||||
|
openssl dhparam -out "$DH_FILE" 2048
|
||||||
|
chmod 600 "$DH_FILE"
|
||||||
|
chown root:root "$DH_FILE"
|
||||||
|
fi
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_dh1024_param_file = ${DH_FILE}"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_dh1024_param_file = ${DH_FILE}"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_eecdh_grade = strong"
|
||||||
|
/usr/sbin/postconf -e "tls_preempt_cipherlist = yes"
|
||||||
|
|
||||||
|
# Nur moderne TLS-Versionen (auch für ausgehendes SMTP)
|
||||||
|
# (überschreibt die älteren Zeilen oben)
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_mandatory_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1"
|
||||||
|
/usr/sbin/postconf -e "smtp_tls_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1"
|
||||||
|
|
||||||
|
# Hohe Cipher, alte raus
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_ciphers = high"
|
||||||
|
/usr/sbin/postconf -e "smtp_tls_ciphers = high"
|
||||||
|
/usr/sbin/postconf -e "smtpd_tls_exclude_ciphers = aNULL,eNULL,MD5,RC4,DES,3DES"
|
||||||
|
/usr/sbin/postconf -e "smtp_tls_exclude_ciphers = aNULL,eNULL,MD5,RC4,DES,3DES"
|
||||||
|
|
||||||
|
# --- SMTP Sicherheit ----------------------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "disable_vrfy_command = yes"
|
||||||
|
/usr/sbin/postconf -e "smtpd_helo_required = yes"
|
||||||
|
|
||||||
|
# --- Milter -------------------------------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "milter_default_action = accept"
|
||||||
|
/usr/sbin/postconf -e "milter_protocol = 6"
|
||||||
|
/usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
/usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
|
||||||
|
# --- SASL Auth via Dovecot ----------------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "smtpd_sasl_type = dovecot"
|
||||||
|
/usr/sbin/postconf -e "smtpd_sasl_path = private/auth"
|
||||||
|
/usr/sbin/postconf -e "smtpd_sasl_auth_enable = yes"
|
||||||
|
/usr/sbin/postconf -e "smtpd_sasl_security_options = noanonymous"
|
||||||
|
|
||||||
|
# --- Recipient & Relay Restriction --------------------------------------------
|
||||||
|
/usr/sbin/postconf -e "smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination"
|
||||||
|
/usr/sbin/postconf -e "smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination"
|
||||||
|
|
||||||
|
# --- Listener / Master.cf Definition ------------------------------------------
|
||||||
|
/usr/sbin/postconf -M "smtp/inet=smtp inet n - n - - smtpd -o smtpd_peername_lookup=no -o smtpd_timeout=30s"
|
||||||
|
/usr/sbin/postconf -M "submission/inet=submission inet n - n - - smtpd -o syslog_name=postfix/submission -o smtpd_peername_lookup=no -o smtpd_tls_security_level=encrypt -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
||||||
|
/usr/sbin/postconf -M "smtps/inet=smtps inet n - n - - smtpd -o syslog_name=postfix/smtps -o smtpd_peername_lookup=no -o smtpd_tls_wrappermode=yes -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
||||||
|
|
||||||
|
# postscreen ggf. deaktivieren
|
||||||
|
sed -i 's/^[[:space:]]*smtp[[:space:]]\+inet[[:space:]]\+.*postscreen/# &/' /etc/postfix/master.cf || true
|
||||||
|
|
||||||
|
# --- SQL Maps (Verzeichnis zuerst!) -------------------------------------------
|
||||||
|
install -d -o root -g postfix -m 750 /etc/postfix/sql
|
||||||
|
|
||||||
|
# Domains
|
||||||
|
cat > /etc/postfix/sql/mysql-virtual-domains.cf <<CONF
|
||||||
|
hosts = 127.0.0.1
|
||||||
|
user = ${DB_USER}
|
||||||
|
password = ${DB_PASS}
|
||||||
|
dbname = ${DB_NAME}
|
||||||
|
query = SELECT 1 FROM domains WHERE domain = '%s' AND is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:postfix /etc/postfix/sql/mysql-virtual-domains.cf
|
||||||
|
chmod 640 /etc/postfix/sql/mysql-virtual-domains.cf
|
||||||
|
|
||||||
|
# Mailboxen
|
||||||
|
cat > /etc/postfix/sql/mysql-virtual-mailbox-maps.cf <<CONF
|
||||||
|
hosts = 127.0.0.1
|
||||||
|
user = ${DB_USER}
|
||||||
|
password = ${DB_PASS}
|
||||||
|
dbname = ${DB_NAME}
|
||||||
|
query = SELECT 1 FROM mail_users u JOIN domains d ON d.id = u.domain_id WHERE u.email = '%s' AND u.is_active = 1 AND u.can_login = 1 AND u.password_hash IS NOT NULL AND d.is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:postfix /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
||||||
|
chmod 640 /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
||||||
|
|
||||||
|
# Aliase
|
||||||
|
cat > /etc/postfix/sql/mysql-virtual-alias-maps.cf <<CONF
|
||||||
|
hosts = 127.0.0.1
|
||||||
|
user = ${DB_USER}
|
||||||
|
password = ${DB_PASS}
|
||||||
|
dbname = ${DB_NAME}
|
||||||
|
query = SELECT COALESCE(mu.email, r.email) AS destination FROM mail_aliases a JOIN domains d ON d.id = a.domain_id JOIN mail_alias_recipients r ON r.alias_id = a.id LEFT JOIN mail_users mu ON mu.id = r.mail_user_id WHERE d.domain = SUBSTRING_INDEX('%s','@',-1) AND a.local = SUBSTRING_INDEX('%s','@', 1) AND a.is_active = 1 AND d.is_active = 1 AND (mu.email IS NOT NULL OR r.email IS NOT NULL) ORDER BY r.position ASC;
|
||||||
|
CONF
|
||||||
|
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
|
||||||
|
# Aktivieren
|
||||||
|
/usr/sbin/postconf -e "virtual_mailbox_domains = proxy:mysql:/etc/postfix/sql/mysql-virtual-domains.cf"
|
||||||
|
/usr/sbin/postconf -e "virtual_mailbox_maps = proxy:mysql:/etc/postfix/sql/mysql-virtual-mailbox-maps.cf"
|
||||||
|
/usr/sbin/postconf -e "virtual_alias_maps = proxy:mysql:/etc/postfix/sql/mysql-virtual-alias-maps.cf"
|
||||||
|
/usr/sbin/postconf -e "virtual_transport = lmtp:unix:private/dovecot-lmtp"
|
||||||
|
|
||||||
|
# --- Dienst aktivieren & neu laden --------------------------------------------
|
||||||
|
#systemctl enable postfix >/dev/null 2>&1 || true
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
MAIL_SSL_DIR="/etc/ssl/mail"
|
||||||
|
MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"
|
||||||
|
MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
|
||||||
|
|
||||||
|
log "Dovecot konfigurieren …"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 1) vmail-Benutzer/Gruppe & Mailspool vorbereiten (DYNAMIC UID!)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Sicherstellen, dass die Gruppe 'mail' existiert (auf Debian/Ubuntu idR vorhanden)
|
||||||
|
getent group mail >/dev/null || groupadd -g 8 mail || true
|
||||||
|
|
||||||
|
# vmail anlegen, wenn er fehlt. Bevorzugt UID 109, falls frei – sonst automatisch.
|
||||||
|
if ! getent passwd vmail >/dev/null; then
|
||||||
|
if ! getent passwd 109 >/dev/null; then
|
||||||
|
useradd -u 109 -g mail -d /var/mail -M -s /usr/sbin/nologin vmail
|
||||||
|
else
|
||||||
|
useradd -g mail -d /var/mail -M -s /usr/sbin/nologin vmail
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tatsächliche vmail-UID ermitteln (wird unten in die Dovecot-Config geschrieben)
|
||||||
|
VMAIL_UID="$(id -u vmail)"
|
||||||
|
|
||||||
|
# Mailspool-Basis
|
||||||
|
install -d -m 0770 -o vmail -g mail /var/mail/vhosts
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 2) Dovecot Grundgerüst
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Hauptdatei
|
||||||
|
install -d -m 0755 /etc/dovecot/conf.d
|
||||||
|
cat > /etc/dovecot/dovecot.conf <<'CONF'
|
||||||
|
!include_try /etc/dovecot/conf.d/*.conf
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# Mail-Location & Namespace + UID-Grenzen
|
||||||
|
cat > /etc/dovecot/conf.d/10-mail.conf <<CONF
|
||||||
|
protocols = imap pop3 lmtp
|
||||||
|
mail_location = maildir:/var/mail/vhosts/%d/%n
|
||||||
|
|
||||||
|
namespace inbox {
|
||||||
|
inbox = yes
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_privileged_group = mail
|
||||||
|
mail_access_groups = mail
|
||||||
|
first_valid_uid = ${VMAIL_UID}
|
||||||
|
last_valid_uid = ${VMAIL_UID}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat > /etc/dovecot/conf.d/15-mailboxes.conf <<'CONF'
|
||||||
|
namespace inbox {
|
||||||
|
inbox = yes
|
||||||
|
|
||||||
|
mailbox Drafts {
|
||||||
|
special_use = \Drafts
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Junk {
|
||||||
|
special_use = \Junk
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Trash {
|
||||||
|
special_use = \Trash
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Sent {
|
||||||
|
special_use = \Sent
|
||||||
|
auto = subscribe
|
||||||
|
}
|
||||||
|
mailbox Archive {
|
||||||
|
special_use = \Archive
|
||||||
|
auto = create
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF'
|
||||||
|
disable_plaintext_auth = yes
|
||||||
|
auth_mechanisms = plain login
|
||||||
|
!include_try auth-sql.conf.ext
|
||||||
|
|
||||||
|
auth_cache_size = 10M
|
||||||
|
auth_cache_ttl = 1 hour
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# SQL-Anbindung (Passwörter aus App-DB)
|
||||||
|
cat > /etc/dovecot/dovecot-sql.conf.ext <<CONF
|
||||||
|
driver = mysql
|
||||||
|
connect = host=127.0.0.1 dbname=${DB_NAME} user=${DB_USER} password=${DB_PASS}
|
||||||
|
default_pass_scheme = BLF-CRYPT
|
||||||
|
password_query = SELECT u.email AS user, u.password_hash AS password FROM mail_users u JOIN domains d ON d.id = u.domain_id WHERE u.email = '%u' AND u.is_active = 1 AND u.can_login = 1 AND u.password_hash IS NOT NULL AND d.is_active = 1 LIMIT 1;
|
||||||
|
CONF
|
||||||
|
chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
|
||||||
|
chmod 640 /etc/dovecot/dovecot-sql.conf.ext
|
||||||
|
|
||||||
|
# Auth-SQL → userdb static auf vmail:mail (Home unter /var/mail/vhosts/%d/%n)
|
||||||
|
cat > /etc/dovecot/conf.d/auth-sql.conf.ext <<'CONF'
|
||||||
|
passdb {
|
||||||
|
driver = sql
|
||||||
|
args = /etc/dovecot/dovecot-sql.conf.ext
|
||||||
|
}
|
||||||
|
userdb {
|
||||||
|
driver = static
|
||||||
|
args = uid=vmail gid=mail home=/var/mail/vhosts/%d/%n
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext
|
||||||
|
chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 3) IMAP Optimierung (iOS/IDLE-freundlich)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
cat > /etc/dovecot/conf.d/20-imap.conf <<'CONF'
|
||||||
|
# IMAP-spezifische Einstellungen
|
||||||
|
|
||||||
|
imap_idle_notify_interval = 2 mins
|
||||||
|
imap_hibernate_timeout = 0
|
||||||
|
|
||||||
|
protocol imap {
|
||||||
|
mail_max_userip_connections = 20
|
||||||
|
imap_logout_format = in=%i out=%o deleted=%{deleted} expunged=%{expunged}
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 4) Master Services (LMTP, AUTH, IMAP, POP3, STATS)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
cat > /etc/dovecot/conf.d/10-master.conf <<'CONF'
|
||||||
|
service lmtp {
|
||||||
|
unix_listener /var/spool/postfix/private/dovecot-lmtp {
|
||||||
|
mode = 0600
|
||||||
|
user = postfix
|
||||||
|
group = postfix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service auth {
|
||||||
|
unix_listener /var/spool/postfix/private/auth {
|
||||||
|
mode = 0660
|
||||||
|
user = postfix
|
||||||
|
group = postfix
|
||||||
|
}
|
||||||
|
unix_listener auth-userdb {
|
||||||
|
mode = 0660
|
||||||
|
user = vmail
|
||||||
|
group = mail
|
||||||
|
}
|
||||||
|
process_limit = 1
|
||||||
|
}
|
||||||
|
service imap-login {
|
||||||
|
inet_listener imap {
|
||||||
|
port = 143
|
||||||
|
}
|
||||||
|
inet_listener imaps {
|
||||||
|
port = 993
|
||||||
|
ssl = yes
|
||||||
|
}
|
||||||
|
process_limit = 128
|
||||||
|
process_min_avail = 10
|
||||||
|
service_count = 0
|
||||||
|
vsz_limit = 512M
|
||||||
|
}
|
||||||
|
service pop3-login {
|
||||||
|
inet_listener pop3 {
|
||||||
|
port = 110
|
||||||
|
}
|
||||||
|
inet_listener pop3s {
|
||||||
|
port = 995
|
||||||
|
ssl = yes
|
||||||
|
}
|
||||||
|
process_limit = 50
|
||||||
|
service_count = 0
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# --- Dovecot: doveadm-server für App-Zugriff ---
|
||||||
|
cat >/etc/dovecot/conf.d/99-mailwolt-perms.conf <<'CONF'
|
||||||
|
service auth {
|
||||||
|
unix_listener auth-userdb {
|
||||||
|
mode = 0660
|
||||||
|
user = vmail
|
||||||
|
group = mail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service stats {
|
||||||
|
unix_listener stats-reader {
|
||||||
|
mode = 0660
|
||||||
|
user = vmail
|
||||||
|
group = mail
|
||||||
|
}
|
||||||
|
unix_listener stats-writer {
|
||||||
|
mode = 0660
|
||||||
|
user = vmail
|
||||||
|
group = mail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 5) SSL-Konfiguration (ohne DH-Param-Erzeugung)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
DOVECOT_SSL_CONF="/etc/dovecot/conf.d/10-ssl.conf"
|
||||||
|
touch "$DOVECOT_SSL_CONF"
|
||||||
|
grep -q '^ssl\s*=' "$DOVECOT_SSL_CONF" 2>/dev/null || echo "ssl = required" >> "$DOVECOT_SSL_CONF"
|
||||||
|
if grep -q '^\s*ssl_cert\s*=' "$DOVECOT_SSL_CONF"; then
|
||||||
|
sed -i "s|^\s*ssl_cert\s*=.*|ssl_cert = <${MAIL_CERT}|" "$DOVECOT_SSL_CONF"
|
||||||
|
else
|
||||||
|
echo "ssl_cert = <${MAIL_CERT}" >> "$DOVECOT_SSL_CONF"
|
||||||
|
fi
|
||||||
|
if grep -q '^\s*ssl_key\s*=' "$DOVECOT_SSL_CONF"; then
|
||||||
|
sed -i "s|^\s*ssl_key\s*=.*|ssl_key = <${MAIL_KEY}|" "$DOVECOT_SSL_CONF"
|
||||||
|
else
|
||||||
|
echo "ssl_key = <${MAIL_KEY}" >> "$DOVECOT_SSL_CONF"
|
||||||
|
fi
|
||||||
|
grep -q '^ssl_min_protocol' "$DOVECOT_SSL_CONF" || echo "ssl_min_protocol = TLSv1.2" >> "$DOVECOT_SSL_CONF"
|
||||||
|
grep -q '^ssl_prefer_server_ciphers' "$DOVECOT_SSL_CONF" || echo "ssl_prefer_server_ciphers = yes" >> "$DOVECOT_SSL_CONF"
|
||||||
|
|
||||||
|
grep -q '^ssl_dh' "$DOVECOT_SSL_CONF" || echo "ssl_dh = </etc/ssl/private/dhparam.pem" >> "$DOVECOT_SSL_CONF"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 6) Verzeichnisse & Rechte prüfen
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
mkdir -p /var/spool/postfix/private
|
||||||
|
chown root:root /var/spool/postfix
|
||||||
|
chmod 0755 /var/spool/postfix
|
||||||
|
chown postfix:postfix /var/spool/postfix/private
|
||||||
|
chmod 0755 /var/spool/postfix/private
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 7) Abschluss
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log "Dovecot-Konfiguration abgeschlossen."
|
||||||
|
|
@ -0,0 +1,319 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Rspamd + OpenDKIM einrichten …"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# ENV laden
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
set +u
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
set -u
|
||||||
|
|
||||||
|
BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
||||||
|
SYSMAIL_DOMAIN="${SYSMAIL_DOMAIN:-sysmail.${BASE_DOMAIN}}" # z.B. sysmail.example.com
|
||||||
|
DKIM_ENABLE="${DKIM_ENABLE:-1}" # 1=OpenDKIM aktiv
|
||||||
|
DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}" # z.B. mwl1
|
||||||
|
DKIM_GENERATE="${DKIM_GENERATE:-0}" # 1=Key generieren, falls fehlt
|
||||||
|
RSPAMD_CONTROLLER_PASSWORD="${RSPAMD_CONTROLLER_PASSWORD:-admin}"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# Rspamd (Controller + Milter)
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
install -d -m 0750 /etc/rspamd/local.d
|
||||||
|
|
||||||
|
if command -v rspamadm >/dev/null 2>&1; then
|
||||||
|
RSPAMD_HASH="$(rspamadm pw -p "${RSPAMD_CONTROLLER_PASSWORD}")"
|
||||||
|
else
|
||||||
|
RSPAMD_HASH="${RSPAMD_CONTROLLER_PASSWORD}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/worker-controller.inc <<CONF
|
||||||
|
worker "controller" {
|
||||||
|
bind_socket = "127.0.0.1:11334";
|
||||||
|
password = "${RSPAMD_HASH}";
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/statistic.conf <<CONF
|
||||||
|
classifier "bayes" {
|
||||||
|
backend = "redis";
|
||||||
|
autolearn = true;
|
||||||
|
autolearn_threshold = 6.0;
|
||||||
|
ham_symbols = ["BAYES_HAM"];
|
||||||
|
spam_symbols = ["BAYES_SPAM"];
|
||||||
|
min_learns = 10;
|
||||||
|
store_tokens = true;
|
||||||
|
per_user = false;
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/worker-proxy.inc <<'CONF'
|
||||||
|
worker "proxy" {
|
||||||
|
bind_socket = "127.0.0.1:11332";
|
||||||
|
milter = yes;
|
||||||
|
timeout = 120s;
|
||||||
|
|
||||||
|
upstream "scan" {
|
||||||
|
default = yes;
|
||||||
|
self_scan = yes;
|
||||||
|
servers = "127.0.0.1:11333";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/worker-normal.inc <<'CONF'
|
||||||
|
worker "normal" {
|
||||||
|
bind_socket = "127.0.0.1:11333";
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/milter_headers.conf <<'CONF'
|
||||||
|
use = ["authentication-results"];
|
||||||
|
header = "Authentication-Results";
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/options.inc <<'CONF'
|
||||||
|
dns {
|
||||||
|
servers = ["9.9.9.9:53", "1.1.1.1:53"];
|
||||||
|
timeout = 5s;
|
||||||
|
retransmits = 2;
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# Rspamd Redis-Konfiguration
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
log "Rspamd Redis konfigurieren …"
|
||||||
|
|
||||||
|
: "${REDIS_PASS:=}"
|
||||||
|
|
||||||
|
cat >/etc/rspamd/local.d/redis.conf <<CONF
|
||||||
|
servers = "127.0.0.1:6379";
|
||||||
|
${REDIS_PASS:+password = "${REDIS_PASS}";}
|
||||||
|
db = 0;
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# Eigentümer und Rechte setzen
|
||||||
|
chown root:_rspamd /etc/rspamd/local.d /etc/rspamd/local.d/redis.conf
|
||||||
|
chmod 750 /etc/rspamd/local.d
|
||||||
|
chmod 640 /etc/rspamd/local.d/redis.conf
|
||||||
|
|
||||||
|
# Testweise prüfen, ob Redis erreichbar ist (nicht kritisch)
|
||||||
|
if command -v redis-cli >/dev/null 2>&1; then
|
||||||
|
if [[ -n "${REDIS_PASS}" ]]; then
|
||||||
|
if redis-cli -h 127.0.0.1 -p 6379 -a "${REDIS_PASS}" ping >/dev/null 2>&1; then
|
||||||
|
log "[✓] Redis erreichbar und Passwort akzeptiert."
|
||||||
|
else
|
||||||
|
log "[!] Warnung: Redis antwortet nicht oder Passwort falsch."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if redis-cli -h 127.0.0.1 -p 6379 ping >/dev/null 2>&1; then
|
||||||
|
log "[✓] Redis erreichbar (ohne Passwort)."
|
||||||
|
else
|
||||||
|
log "[!] Warnung: Redis antwortet nicht."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl enable --now rspamd || true
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# OpenDKIM – nur wenn DKIM_ENABLE=1
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [[ "${DKIM_ENABLE}" != "1" ]]; then
|
||||||
|
log "DKIM_ENABLE=0 → OpenDKIM wird übersprungen."
|
||||||
|
/usr/sbin/postconf -e "milter_default_action = accept"
|
||||||
|
/usr/sbin/postconf -e "milter_protocol = 6"
|
||||||
|
/usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332"
|
||||||
|
/usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
install -d -m 0755 /etc/opendkim
|
||||||
|
install -d -m 0750 /etc/opendkim/keys
|
||||||
|
chown -R opendkim:opendkim /etc/opendkim
|
||||||
|
chmod 750 /etc/opendkim/keys
|
||||||
|
|
||||||
|
# TrustedHosts
|
||||||
|
cat >/etc/opendkim/TrustedHosts <<'CONF'
|
||||||
|
127.0.0.1
|
||||||
|
::1
|
||||||
|
localhost
|
||||||
|
CONF
|
||||||
|
chown opendkim:opendkim /etc/opendkim/TrustedHosts
|
||||||
|
chmod 640 /etc/opendkim/TrustedHosts
|
||||||
|
|
||||||
|
# ── Key-Verzeichnis für SYSMAIL_DOMAIN vorbereiten ───────────────────────────
|
||||||
|
KEY_DIR="/etc/opendkim/keys/${SYSMAIL_DOMAIN}"
|
||||||
|
KEY_PRIV="${KEY_DIR}/${DKIM_SELECTOR}.private"
|
||||||
|
KEY_DNSTXT="${KEY_DIR}/${DKIM_SELECTOR}.txt"
|
||||||
|
install -d -m 0750 -o opendkim -g opendkim "${KEY_DIR}"
|
||||||
|
|
||||||
|
# ── Key optional generieren (nur wenn gewünscht) ─────────────────────────────
|
||||||
|
if [[ ! -s "${KEY_PRIV}" && "${DKIM_GENERATE}" = "1" ]]; then
|
||||||
|
if command -v opendkim-genkey >/dev/null 2>&1; then
|
||||||
|
opendkim-genkey -b 2048 -s "${DKIM_SELECTOR}" -d "${SYSMAIL_DOMAIN}" -D "${KEY_DIR}"
|
||||||
|
chown opendkim:opendkim "${KEY_DIR}/${DKIM_SELECTOR}.private" || true
|
||||||
|
chmod 600 "${KEY_DIR}/${DKIM_SELECTOR}.private" || true
|
||||||
|
else
|
||||||
|
echo "[!] opendkim-genkey fehlt – kann DKIM-Key nicht generieren."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Key-/SigningTable nur anlegen, nicht leeren ───────────────────────────────
|
||||||
|
touch /etc/opendkim/KeyTable /etc/opendkim/SigningTable
|
||||||
|
chown opendkim:opendkim /etc/opendkim/KeyTable /etc/opendkim/SigningTable
|
||||||
|
chmod 640 /etc/opendkim/KeyTable /etc/opendkim/SigningTable
|
||||||
|
|
||||||
|
if [[ -s "${KEY_PRIV}" && "${BASE_DOMAIN}" != "example.com" ]]; then
|
||||||
|
LINE_KT="${DKIM_SELECTOR}._domainkey.${SYSMAIL_DOMAIN} ${SYSMAIL_DOMAIN}:${DKIM_SELECTOR}:${KEY_PRIV}"
|
||||||
|
LINE_ST="*@${SYSMAIL_DOMAIN} ${DKIM_SELECTOR}._domainkey.${SYSMAIL_DOMAIN}"
|
||||||
|
grep -Fqx "$LINE_KT" /etc/opendkim/KeyTable || echo "$LINE_KT" >> /etc/opendkim/KeyTable
|
||||||
|
grep -Fqx "$LINE_ST" /etc/opendkim/SigningTable || echo "$LINE_ST" >> /etc/opendkim/SigningTable
|
||||||
|
else
|
||||||
|
echo "[i] Kein Private Key unter ${KEY_PRIV} – App-Helper trägt später ein."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Hauptkonfiguration ───────────────────────────────────────────────────────
|
||||||
|
cat >/etc/opendkim.conf <<'CONF'
|
||||||
|
Syslog yes
|
||||||
|
UMask 002
|
||||||
|
Mode sv
|
||||||
|
Socket inet:8891@127.0.0.1
|
||||||
|
PidFile /run/opendkim/opendkim.pid
|
||||||
|
Canonicalization relaxed/simple
|
||||||
|
|
||||||
|
On-BadSignature accept
|
||||||
|
On-Default accept
|
||||||
|
On-KeyNotFound accept
|
||||||
|
On-NoSignature accept
|
||||||
|
|
||||||
|
LogWhy yes
|
||||||
|
OversignHeaders From
|
||||||
|
|
||||||
|
KeyTable /etc/opendkim/KeyTable
|
||||||
|
SigningTable refile:/etc/opendkim/SigningTable
|
||||||
|
ExternalIgnoreList /etc/opendkim/TrustedHosts
|
||||||
|
InternalHosts /etc/opendkim/TrustedHosts
|
||||||
|
|
||||||
|
UserID opendkim:opendkim
|
||||||
|
AutoRestart yes
|
||||||
|
AutoRestartRate 10/1h
|
||||||
|
Background yes
|
||||||
|
DNSTimeout 5
|
||||||
|
SignatureAlgorithm rsa-sha256
|
||||||
|
SyslogSuccess yes
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# ── systemd Drop-in: /run/opendkim sicherstellen ─────────────────────────────
|
||||||
|
install -d -m 0755 /etc/systemd/system/opendkim.service.d
|
||||||
|
cat >/etc/systemd/system/opendkim.service.d/override.conf <<'EOF'
|
||||||
|
[Service]
|
||||||
|
RuntimeDirectory=opendkim
|
||||||
|
RuntimeDirectoryMode=0755
|
||||||
|
EOF
|
||||||
|
|
||||||
|
install -d -o opendkim -g opendkim -m 0755 /run/opendkim
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# Root-Helper: DKIM installieren / entfernen + sudoers-Regel
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
install -d -m 0750 /usr/local/sbin
|
||||||
|
|
||||||
|
# --- mailwolt-install-dkim ------------------------------------
|
||||||
|
cat > /usr/local/sbin/mailwolt-install-dkim <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOMAIN="$1"
|
||||||
|
SELECTOR="$2"
|
||||||
|
SRC_PRIV="$3"
|
||||||
|
SRC_TXT="${4:-}"
|
||||||
|
|
||||||
|
OKDIR="/etc/opendkim"
|
||||||
|
KEYDIR="${OKDIR}/keys/${DOMAIN}"
|
||||||
|
KEYPRI="${KEYDIR}/${SELECTOR}.private"
|
||||||
|
|
||||||
|
install -d -m 0750 -o opendkim -g opendkim "${KEYDIR}"
|
||||||
|
install -m 0600 -o opendkim -g opendkim "${SRC_PRIV}" "${KEYPRI}"
|
||||||
|
|
||||||
|
KT="${OKDIR}/KeyTable"
|
||||||
|
ST="${OKDIR}/SigningTable"
|
||||||
|
touch "$KT" "$ST"
|
||||||
|
chown opendkim:opendkim "$KT" "$ST"
|
||||||
|
chmod 0640 "$KT" "$ST"
|
||||||
|
|
||||||
|
LINE_KT="${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:${KEYPRI}"
|
||||||
|
LINE_ST="*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}"
|
||||||
|
|
||||||
|
grep -Fqx "$LINE_KT" "$KT" || echo "$LINE_KT" >> "$KT"
|
||||||
|
grep -Fqx "$LINE_ST" "$ST" || echo "$LINE_ST" >> "$ST"
|
||||||
|
|
||||||
|
if [[ -n "${SRC_TXT}" && -s "${SRC_TXT}" ]]; then
|
||||||
|
install -d -m 0755 /etc/mailwolt/dns
|
||||||
|
cp -f "${SRC_TXT}" "/etc/mailwolt/dns/dkim-${DOMAIN}.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl is-active --quiet opendkim && systemctl reload opendkim || true
|
||||||
|
echo "OK"
|
||||||
|
EOSH
|
||||||
|
chmod 0750 /usr/local/sbin/mailwolt-install-dkim
|
||||||
|
chown root:root /usr/local/sbin/mailwolt-install-dkim
|
||||||
|
|
||||||
|
# --- 2) mailwolt-remove-dkim ----------------------------------
|
||||||
|
cat >/usr/local/sbin/mailwolt-remove-dkim <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOMAIN="$1" # z.B. kunden.tld oder sysmail.example.com
|
||||||
|
SELECTOR="$2" # z.B. mwl1
|
||||||
|
|
||||||
|
OKDIR="/etc/opendkim"
|
||||||
|
KEYDIR="${OKDIR}/keys/${DOMAIN}"
|
||||||
|
KEYPRI="${KEYDIR}/${SELECTOR}.private"
|
||||||
|
KT="${OKDIR}/KeyTable"
|
||||||
|
ST="${OKDIR}/SigningTable"
|
||||||
|
|
||||||
|
# Key-Datei löschen (falls vorhanden)
|
||||||
|
[[ -f "${KEYPRI}" ]] && rm -f "${KEYPRI}"
|
||||||
|
|
||||||
|
# Zeilen aus KeyTable und SigningTable entfernen
|
||||||
|
if [[ -f "$KT" ]]; then
|
||||||
|
tmp="$(mktemp)"; grep -v -F "${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:" "$KT" >"$tmp" && mv "$tmp" "$KT"
|
||||||
|
chown opendkim:opendkim "$KT"; chmod 0640 "$KT"
|
||||||
|
fi
|
||||||
|
if [[ -f "$ST" ]]; then
|
||||||
|
tmp="$(mktemp)"; grep -v -F "*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}" "$ST" >"$tmp" && mv "$tmp" "$ST"
|
||||||
|
chown opendkim:opendkim "$ST"; chmod 0640 "$ST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verzeichnis ggf. aufräumen
|
||||||
|
rmdir "${KEYDIR}" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Dienst neu laden, falls aktiv
|
||||||
|
if systemctl is-active --quiet opendkim; then
|
||||||
|
systemctl reload opendkim || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK"
|
||||||
|
EOSH
|
||||||
|
chown root:root /usr/local/sbin/mailwolt-remove-dkim
|
||||||
|
chmod 0750 /usr/local/sbin/mailwolt-remove-dkim
|
||||||
|
|
||||||
|
# ── Dienst + Postfix-Milter aktivieren ─────────────────────────
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable opendkim || true
|
||||||
|
|
||||||
|
touch /run/mailwolt.need-apply-milters || true
|
||||||
|
|
||||||
|
chgrp _rspamd /etc/rspamd/local.d/*.inc /etc/rspamd/local.d/*.conf || true
|
||||||
|
chmod 0640 /etc/rspamd/local.d/*.inc /etc/rspamd/local.d/*.conf || true
|
||||||
|
|
||||||
|
#/usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
#/usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
||||||
|
|
||||||
|
log "[✓] Rspamd + OpenDKIM eingerichtet (läuft; signiert, sobald Keys vorhanden sind)."
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "OpenDMARC installieren/konfigurieren …"
|
||||||
|
|
||||||
|
# Flags laden
|
||||||
|
set +u
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
set -u
|
||||||
|
OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
||||||
|
|
||||||
|
# Paket sicherstellen
|
||||||
|
if ! dpkg -s opendmarc >/dev/null 2>&1; then
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y opendmarc
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Config-Verzeichnisse
|
||||||
|
install -d -m 0755 /etc/opendmarc
|
||||||
|
install -d -m 0755 /run/opendmarc
|
||||||
|
|
||||||
|
# IgnoreHosts
|
||||||
|
cat >/etc/opendmarc/ignore.hosts <<'EOF'
|
||||||
|
127.0.0.1
|
||||||
|
::1
|
||||||
|
localhost
|
||||||
|
EOF
|
||||||
|
chmod 0644 /etc/opendmarc/ignore.hosts
|
||||||
|
|
||||||
|
# Hauptkonfiguration
|
||||||
|
cat >/etc/opendmarc.conf <<'EOF'
|
||||||
|
AuthservID mailwolt
|
||||||
|
TrustedAuthservIDs mailwolt
|
||||||
|
IgnoreHosts /etc/opendmarc/ignore.hosts
|
||||||
|
Syslog true
|
||||||
|
SoftwareHeader true
|
||||||
|
Socket local:/run/opendmarc/opendmarc.sock
|
||||||
|
RejectFailures false
|
||||||
|
EOF
|
||||||
|
chmod 0644 /etc/opendmarc.conf
|
||||||
|
|
||||||
|
# systemd Drop-in für RuntimeDirectory (robust nach Reboot)
|
||||||
|
install -d -m 0755 /etc/systemd/system/opendmarc.service.d
|
||||||
|
cat >/etc/systemd/system/opendmarc.service.d/override.conf <<'EOF'
|
||||||
|
[Service]
|
||||||
|
RuntimeDirectory=opendmarc
|
||||||
|
RuntimeDirectoryMode=0755
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Dienst nach Flag
|
||||||
|
if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
||||||
|
systemctl enable --now opendmarc
|
||||||
|
else
|
||||||
|
systemctl disable --now opendmarc || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Postfix-Milter-Kette konsistent setzen (Rspamd + OpenDKIM + optional OpenDMARC)
|
||||||
|
touch /run/mailwolt.need-apply-milters || true
|
||||||
|
|
||||||
|
log "[✓] OpenDMARC (ENABLE=${OPENDMARC_ENABLE}) bereit."
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "ClamAV (clamav-daemon) installieren/konfigurieren …"
|
||||||
|
|
||||||
|
# Flags laden
|
||||||
|
set +u
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
set -u
|
||||||
|
CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
|
||||||
|
|
||||||
|
# Pakete
|
||||||
|
if ! dpkg -s clamav-daemon >/dev/null 2>&1; then
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y clamav clamav-daemon
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Signaturen aktualisieren (erst Freshclam starten)
|
||||||
|
systemctl stop clamav-freshclam 2>/dev/null || true
|
||||||
|
freshclam || true
|
||||||
|
systemctl start clamav-freshclam || true
|
||||||
|
|
||||||
|
# clamd LocalSocket setzen
|
||||||
|
sed -i 's|^#\?LocalSocket .*|LocalSocket /run/clamav/clamd.ctl|' /etc/clamav/clamd.conf || true
|
||||||
|
install -d -m 0755 /run/clamav
|
||||||
|
chown clamav:clamav /run/clamav
|
||||||
|
|
||||||
|
# Dienst nach Flag
|
||||||
|
if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
||||||
|
systemctl enable --now clamav-daemon
|
||||||
|
else
|
||||||
|
systemctl disable --now clamav-daemon || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rspamd-Integration (nur wenn aktiv)
|
||||||
|
AV_CONF="/etc/rspamd/local.d/antivirus.conf"
|
||||||
|
if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
||||||
|
cat >"$AV_CONF" <<'EOF'
|
||||||
|
clamav {
|
||||||
|
symbol = "CLAM_VIRUS";
|
||||||
|
type = "clamav";
|
||||||
|
servers = "/run/clamav/clamd.ctl";
|
||||||
|
scan_mime_parts = true;
|
||||||
|
scan_text_mime = true;
|
||||||
|
max_size = 50mb;
|
||||||
|
log_clean = false;
|
||||||
|
action = "reject";
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
chown root:_rspamd "$AV_CONF" || true
|
||||||
|
chmod 0640 "$AV_CONF" || true
|
||||||
|
systemctl reload rspamd || systemctl restart rspamd
|
||||||
|
else
|
||||||
|
rm -f "$AV_CONF" || true
|
||||||
|
systemctl reload rspamd || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "[✓] ClamAV (ENABLE=${CLAMAV_ENABLE}) konfiguriert."
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Fail2Ban installieren/konfigurieren …"
|
||||||
|
|
||||||
|
# Flags laden
|
||||||
|
set +u
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
set -u
|
||||||
|
FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
|
||||||
|
# Paket
|
||||||
|
if ! dpkg -s fail2ban >/dev/null 2>&1; then
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y fail2ban sqlite3
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -m 0755 /etc/fail2ban/jail.d
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Basis-Jails (praxisnah)
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
cat >/etc/fail2ban/jail.d/mailwolt.conf <<'EOF'
|
||||||
|
[sshd]
|
||||||
|
enabled = true
|
||||||
|
port = ssh
|
||||||
|
logpath = /var/log/auth.log
|
||||||
|
|
||||||
|
[postfix]
|
||||||
|
enabled = true
|
||||||
|
logpath = /var/log/mail.log
|
||||||
|
port = smtp,ssmtp,submission,465
|
||||||
|
|
||||||
|
[dovecot]
|
||||||
|
enabled = true
|
||||||
|
logpath = /var/log/mail.log
|
||||||
|
port = pop3,pop3s,imap,imaps,submission,465,587,993
|
||||||
|
|
||||||
|
[rspamd-controller]
|
||||||
|
enabled = true
|
||||||
|
port = 11334
|
||||||
|
filter = rspamd
|
||||||
|
logpath = /var/log/rspamd/rspamd.log
|
||||||
|
maxretry = 5
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# einfacher Filter für Rspamd-Controller
|
||||||
|
if [ ! -f /etc/fail2ban/filter.d/rspamd.conf ]; then
|
||||||
|
cat >/etc/fail2ban/filter.d/rspamd.conf <<'EOF'
|
||||||
|
[Definition]
|
||||||
|
failregex = .*Authentication failed for user.* from <HOST>
|
||||||
|
ignoreregex =
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Fail2Ban-Backend auf SQLite umstellen
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
log "SQLite-Backend aktivieren …"
|
||||||
|
|
||||||
|
cat >/etc/fail2ban/fail2ban.local <<'EOF'
|
||||||
|
[Definition]
|
||||||
|
loglevel = INFO
|
||||||
|
logtarget = /var/log/fail2ban.log
|
||||||
|
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
|
||||||
|
dbpurgeage = 86400
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Datenbankverzeichnis sicherstellen
|
||||||
|
install -d -o fail2ban -g fail2ban -m 0750 /var/lib/fail2ban
|
||||||
|
|
||||||
|
# Falls DB nicht existiert, Dummy anlegen (wird vom Dienst erweitert)
|
||||||
|
if [ ! -f /var/lib/fail2ban/fail2ban.sqlite3 ]; then
|
||||||
|
sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "VACUUM;"
|
||||||
|
fi
|
||||||
|
chown fail2ban:fail2ban /var/lib/fail2ban/fail2ban.sqlite3
|
||||||
|
chmod 0640 /var/lib/fail2ban/fail2ban.sqlite3
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# sudoers für Web-UI
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Fail2Ban Blacklist-Jail
|
||||||
|
cat >/etc/fail2ban/jail.d/mailwolt-blacklist.local <<'EOF'
|
||||||
|
[mailwolt-blacklist]
|
||||||
|
enabled = true
|
||||||
|
filter = none
|
||||||
|
port = anyport
|
||||||
|
bantime = -1
|
||||||
|
findtime = 1
|
||||||
|
maxretry = 1
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat >/etc/fail2ban/filter.d/none.conf <<'EOF'
|
||||||
|
[Definition]
|
||||||
|
failregex =
|
||||||
|
ignoreregex =
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 0640 /etc/fail2ban/filter.d/none.conf
|
||||||
|
|
||||||
|
|
||||||
|
SUDOERS_F2B="/etc/sudoers.d/mailwolt-fail2ban"
|
||||||
|
cat > "${SUDOERS_F2B}" <<'EOF'
|
||||||
|
Defaults:www-data !requiretty
|
||||||
|
www-data ALL=(root) NOPASSWD: \
|
||||||
|
/usr/bin/fail2ban-client, \
|
||||||
|
/usr/bin/fail2ban-client ping, \
|
||||||
|
/usr/bin/fail2ban-client status, \
|
||||||
|
/usr/bin/fail2ban-client status *, \
|
||||||
|
/usr/bin/fail2ban-client get *, \
|
||||||
|
/usr/bin/fail2ban-client set * banip *, \
|
||||||
|
/usr/bin/fail2ban-client set * unbanip *, \
|
||||||
|
/usr/bin/fail2ban-client reload, \
|
||||||
|
/usr/bin/journalctl, \
|
||||||
|
/bin/journalctl, \
|
||||||
|
/usr/bin/zgrep, \
|
||||||
|
/bin/zgrep, \
|
||||||
|
/usr/bin/grep, \
|
||||||
|
/bin/grep, \
|
||||||
|
/usr/bin/tail, \
|
||||||
|
/bin/tail, \
|
||||||
|
/usr/bin/sqlite3, \
|
||||||
|
/usr/bin/tee /etc/fail2ban/jail.d/*
|
||||||
|
EOF
|
||||||
|
chown root:root "${SUDOERS_F2B}"
|
||||||
|
chmod 440 "${SUDOERS_F2B}"
|
||||||
|
|
||||||
|
if ! visudo -c -f "${SUDOERS_F2B}" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_F2B} – entferne Datei."
|
||||||
|
rm -f "${SUDOERS_F2B}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Dienst aktivieren/deaktivieren
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
||||||
|
systemctl enable --now fail2ban
|
||||||
|
else
|
||||||
|
systemctl disable --now fail2ban || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "[✓] Fail2Ban (ENABLE=${FAIL2BAN_ENABLE}) bereit."
|
||||||
|
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
#log "Fail2Ban installieren/konfigurieren …"
|
||||||
|
#
|
||||||
|
## Flags laden
|
||||||
|
#set +u
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
#set -u
|
||||||
|
#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
#
|
||||||
|
## Paket
|
||||||
|
#if ! dpkg -s fail2ban >/dev/null 2>&1; then
|
||||||
|
# apt-get update -qq
|
||||||
|
# apt-get install -y fail2ban
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#install -d -m 0755 /etc/fail2ban/jail.d
|
||||||
|
#
|
||||||
|
## Basis-Jails (praxisnah)
|
||||||
|
#cat >/etc/fail2ban/jail.d/mailwolt.conf <<'EOF'
|
||||||
|
#[DEFAULT]
|
||||||
|
#bantime = 1h
|
||||||
|
#findtime = 10m
|
||||||
|
#maxretry = 5
|
||||||
|
#backend = auto
|
||||||
|
#
|
||||||
|
#[sshd]
|
||||||
|
#enabled = true
|
||||||
|
#port = ssh
|
||||||
|
#logpath = /var/log/auth.log
|
||||||
|
#
|
||||||
|
#[postfix]
|
||||||
|
#enabled = true
|
||||||
|
#logpath = /var/log/mail.log
|
||||||
|
#port = smtp,ssmtp,submission,465
|
||||||
|
#
|
||||||
|
#[dovecot]
|
||||||
|
#enabled = true
|
||||||
|
#logpath = /var/log/mail.log
|
||||||
|
#port = pop3,pop3s,imap,imaps,submission,465,587,993
|
||||||
|
#
|
||||||
|
#[rspamd-controller]
|
||||||
|
#enabled = true
|
||||||
|
#port = 11334
|
||||||
|
#filter = rspamd
|
||||||
|
#logpath = /var/log/rspamd/rspamd.log
|
||||||
|
#maxretry = 5
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## einfacher Filter für Rspamd-Controller
|
||||||
|
#if [ ! -f /etc/fail2ban/filter.d/rspamd.conf ]; then
|
||||||
|
# cat >/etc/fail2ban/filter.d/rspamd.conf <<'EOF'
|
||||||
|
#[Definition]
|
||||||
|
#failregex = .*Authentication failed for user.* from <HOST>
|
||||||
|
#ignoreregex =
|
||||||
|
#EOF
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#SUDOERS_F2B="/etc/sudoers.d/mailwolt-fail2ban"
|
||||||
|
#cat > "${SUDOERS_F2B}" <<'EOF'
|
||||||
|
#www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status *
|
||||||
|
#EOF
|
||||||
|
#chown root:root "${SUDOERS_F2B}"
|
||||||
|
#chmod 440 "${SUDOERS_F2B}"
|
||||||
|
#
|
||||||
|
#if ! visudo -c -f "${SUDOERS_F2B}" >/dev/null 2>&1; then
|
||||||
|
# echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_F2B} – entferne Datei."
|
||||||
|
# rm -f "${SUDOERS_F2B}"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#sudo tee /etc/sudoers.d/mailwolt-fail2ban >/dev/null <<'EOF'
|
||||||
|
#www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status *
|
||||||
|
#EOF
|
||||||
|
#sudo visudo -cf /etc/sudoers.d/mailwolt-fail2ban
|
||||||
|
#
|
||||||
|
## Dienst nach Flag
|
||||||
|
#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
||||||
|
# systemctl enable --now fail2ban
|
||||||
|
#else
|
||||||
|
# systemctl disable --now fail2ban || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#log "[✓] Fail2Ban (ENABLE=${FAIL2BAN_ENABLE}) bereit."
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
# nur ausführen, wenn vorherige Schritte das Flag gesetzt haben
|
||||||
|
if [[ -f /run/mailwolt.need-apply-milters ]]; then
|
||||||
|
if command -v /usr/local/sbin/mailwolt-apply-milters >/dev/null 2>&1; then
|
||||||
|
log "Setze Postfix-Milter-Kette (Rspamd/OpenDKIM[/OpenDMARC]) …"
|
||||||
|
/usr/local/sbin/mailwolt-apply-milters || true
|
||||||
|
else
|
||||||
|
# Fallback (ident wie im Tool)
|
||||||
|
/usr/sbin/postconf -e "milter_default_action = accept"
|
||||||
|
/usr/sbin/postconf -e "milter_protocol = 6"
|
||||||
|
CHAIN="inet:127.0.0.1:11333, inet:127.0.0.1:8891"
|
||||||
|
systemctl is-active --quiet opendmarc && CHAIN="$CHAIN, inet:127.0.0.1:8893" || true
|
||||||
|
/usr/sbin/postconf -e "smtpd_milters = $CHAIN"
|
||||||
|
/usr/sbin/postconf -e "non_smtpd_milters = $CHAIN"
|
||||||
|
systemctl reload postfix || true
|
||||||
|
fi
|
||||||
|
rm -f /run/mailwolt.need-apply-milters || true
|
||||||
|
log "[✓] Milter-Kette angewandt."
|
||||||
|
else
|
||||||
|
log "Milter-Kette: kein Bedarf (Flag nicht gesetzt) – überspringe."
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,465 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Nginx konfigurieren …"
|
||||||
|
|
||||||
|
# ── Flags/Umgebung (vom Bootstrap gesetzt; hier Fallbacks) ────────────────
|
||||||
|
DEV_MODE="${DEV_MODE:-0}" # 1 = DEV (Vite-Proxy aktiv), 0 = PROD
|
||||||
|
PROXY_MODE="${PROXY_MODE:-0}" # 1 = NPM/Proxy davor, Backend spricht nur HTTP:80
|
||||||
|
NPM_IP="${NPM_IP:-}" # z.B. 10.10.20.20
|
||||||
|
|
||||||
|
# Erwartet vom Bootstrap/Installer exportiert:
|
||||||
|
: "${UI_HOST:?UI_HOST fehlt}"
|
||||||
|
: "${WEBMAIL_HOST:?WEBMAIL_HOST fehlt}"
|
||||||
|
: "${APP_DIR:?APP_DIR fehlt}"
|
||||||
|
|
||||||
|
ACME_ROOT="/var/www/letsencrypt"
|
||||||
|
install -d -m 0755 "$ACME_ROOT"
|
||||||
|
|
||||||
|
# Default-Sites entfernen (verhindert doppelten default_server)
|
||||||
|
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
|
||||||
|
|
||||||
|
# HTTP/2-Unterstützung erkennen
|
||||||
|
NGINX_HTTP2_SUFFIX=""
|
||||||
|
if nginx -V 2>&1 | grep -q http_v2; then
|
||||||
|
NGINX_HTTP2_SUFFIX=" http2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PHP-FPM Socket/TCP finden → fastcgi_pass bauen
|
||||||
|
detect_php_fpm_sock(){
|
||||||
|
for v in 8.3 8.2 8.1 8.0 7.4; do
|
||||||
|
s="/run/php/php${v}-fpm.sock"
|
||||||
|
[[ -S "$s" ]] && { echo "unix:${s}"; return; }
|
||||||
|
done
|
||||||
|
[[ -S "/run/php/php-fpm.sock" ]] && { echo "unix:/run/php/php-fpm.sock"; return; }
|
||||||
|
echo "127.0.0.1:9000"
|
||||||
|
}
|
||||||
|
PHP_FPM_TARGET="$(detect_php_fpm_sock)"
|
||||||
|
if [[ "$PHP_FPM_TARGET" == unix:* ]]; then
|
||||||
|
FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET};"
|
||||||
|
else
|
||||||
|
FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET};"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Builder 1: HTTP-only (Proxy-Mode: TLS endet im NPM) ───────────────────
|
||||||
|
## $1=host, $2=outfile
|
||||||
|
#build_site_http_only(){
|
||||||
|
# local host="$1" outfile="$2"
|
||||||
|
#
|
||||||
|
# local def=""
|
||||||
|
# [[ "${DEV_MODE}" = "1" ]] && def=" default_server"
|
||||||
|
# [[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
||||||
|
#
|
||||||
|
# cat > "$outfile" <<CONF
|
||||||
|
## --- ${host} : HTTP (kein Redirect, kein TLS; läuft hinter Reverse-Proxy) ---
|
||||||
|
#server {
|
||||||
|
# listen 80;
|
||||||
|
# listen [::]:80;
|
||||||
|
# server_name ${host};
|
||||||
|
#
|
||||||
|
# # ACME HTTP-01 (optional; meist übernimmt das der Proxy)
|
||||||
|
# location ^~ /.well-known/acme-challenge/ {
|
||||||
|
# root ${ACME_ROOT};
|
||||||
|
# allow all;
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# root ${APP_DIR}/public;
|
||||||
|
# index index.php index.html;
|
||||||
|
#
|
||||||
|
# access_log /var/log/nginx/${host}_access.log;
|
||||||
|
# error_log /var/log/nginx/${host}_error.log;
|
||||||
|
#
|
||||||
|
# client_max_body_size 25m;
|
||||||
|
#
|
||||||
|
# location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||||
|
#
|
||||||
|
# location ~ \.php\$ {
|
||||||
|
# include snippets/fastcgi-php.conf;
|
||||||
|
# ${FASTCGI_PASS}
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
|
# location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
||||||
|
#
|
||||||
|
# # WebSocket: Laravel Reverb (Backend intern HTTP)
|
||||||
|
# location /ws/ {
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
# proxy_set_header Connection "Upgrade";
|
||||||
|
# proxy_set_header Host \$host;
|
||||||
|
# proxy_read_timeout 60s;
|
||||||
|
# proxy_send_timeout 60s;
|
||||||
|
# proxy_pass http://127.0.0.1:8080/;
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# # Reverb HTTP API
|
||||||
|
# location /apps/ {
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Host \$host;
|
||||||
|
# proxy_read_timeout 60s;
|
||||||
|
# proxy_send_timeout 60s;
|
||||||
|
# proxy_pass http://127.0.0.1:8080/apps/;
|
||||||
|
# }
|
||||||
|
#CONF
|
||||||
|
#
|
||||||
|
# if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
# cat >> "$outfile" <<'CONF'
|
||||||
|
# # DEV: Vite-Proxy (HMR)
|
||||||
|
# location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
|
||||||
|
# location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
|
||||||
|
# location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
|
||||||
|
#CONF
|
||||||
|
# fi
|
||||||
|
#
|
||||||
|
# echo "}" >> "$outfile"
|
||||||
|
#}
|
||||||
|
|
||||||
|
#build_site_http_only(){
|
||||||
|
# local host="$1" outfile="$2"
|
||||||
|
#
|
||||||
|
# # DEV: IP-Zugriff ohne Hostname → default_server + server_name _
|
||||||
|
# local def=""
|
||||||
|
# if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
# def=" default_server"
|
||||||
|
# host="_"
|
||||||
|
# fi
|
||||||
|
# [[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
||||||
|
#
|
||||||
|
# cat > "$outfile" <<CONF
|
||||||
|
## --- ${host} : HTTP (kein Redirect, kein TLS; läuft hinter Reverse-Proxy/DEV) ---
|
||||||
|
#server {
|
||||||
|
# listen 80${def};
|
||||||
|
# listen [::]:80${def};
|
||||||
|
# server_name ${host};
|
||||||
|
#
|
||||||
|
# # ACME HTTP-01 (optional; meist übernimmt das der Proxy)
|
||||||
|
# location ^~ /.well-known/acme-challenge/ {
|
||||||
|
# root ${ACME_ROOT};
|
||||||
|
# allow all;
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# root ${APP_DIR}/public;
|
||||||
|
# index index.php index.html;
|
||||||
|
#
|
||||||
|
# access_log /var/log/nginx/${host/_/__}_access.log;
|
||||||
|
# error_log /var/log/nginx/${host/_/__}_error.log;
|
||||||
|
#
|
||||||
|
# client_max_body_size 25m;
|
||||||
|
#
|
||||||
|
# location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||||
|
#
|
||||||
|
# location ~ \.php\$ {
|
||||||
|
# include snippets/fastcgi-php.conf;
|
||||||
|
# ${FASTCGI_PASS}
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
|
# location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
||||||
|
#
|
||||||
|
# # WebSocket: Laravel Reverb
|
||||||
|
# location /ws/ {
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
# proxy_set_header Connection "Upgrade";
|
||||||
|
# proxy_set_header Host \$host;
|
||||||
|
# proxy_read_timeout 60s;
|
||||||
|
# proxy_send_timeout 60s;
|
||||||
|
# proxy_pass http://127.0.0.1:8080/;
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# # Reverb HTTP API
|
||||||
|
# location /apps/ {
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Host \$host;
|
||||||
|
# proxy_read_timeout 60s;
|
||||||
|
# proxy_send_timeout 60s;
|
||||||
|
# proxy_pass http://127.0.0.1:8080/apps/;
|
||||||
|
# }
|
||||||
|
#CONF
|
||||||
|
#
|
||||||
|
# if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
# cat >> "$outfile" <<'CONF'
|
||||||
|
# # DEV: Vite-Proxy (HMR)
|
||||||
|
# location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
|
||||||
|
# location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
|
||||||
|
# location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
|
||||||
|
#CONF
|
||||||
|
# fi
|
||||||
|
#
|
||||||
|
# echo "}" >> "$outfile"
|
||||||
|
#}
|
||||||
|
|
||||||
|
# $1=host, $2=outfile, $3=default_flag (default|nodefault)
|
||||||
|
build_site_http_only(){
|
||||||
|
local host="$1" outfile="$2" def_flag="${3:-default}"
|
||||||
|
|
||||||
|
local def=""
|
||||||
|
if [[ "${DEV_MODE}" = "1" && "${def_flag}" = "default" ]]; then
|
||||||
|
def=" default_server"
|
||||||
|
fi
|
||||||
|
[[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
||||||
|
|
||||||
|
cat > "$outfile" <<CONF
|
||||||
|
# --- ${host} : HTTP (kein Redirect, kein TLS; läuft hinter Reverse-Proxy/DEV) ---
|
||||||
|
server {
|
||||||
|
listen 80${def};
|
||||||
|
listen [::]:80${def};
|
||||||
|
server_name ${host};
|
||||||
|
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root ${ACME_ROOT};
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
root ${APP_DIR}/public;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/${host/_/__}_access.log;
|
||||||
|
error_log /var/log/nginx/${host/_/__}_error.log;
|
||||||
|
|
||||||
|
client_max_body_size 25m;
|
||||||
|
|
||||||
|
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||||
|
|
||||||
|
location ~ \.php\$ {
|
||||||
|
include snippets/fastcgi-php.conf;
|
||||||
|
${FASTCGI_PASS}
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
||||||
|
|
||||||
|
# WebSocket: Laravel Reverb
|
||||||
|
location /ws/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection "Upgrade";
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_pass http://127.0.0.1:8080/;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverb HTTP API
|
||||||
|
location /apps/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_pass http://127.0.0.1:8080/apps/;
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
cat >> "$outfile" <<'CONF'
|
||||||
|
# DEV: Vite-Proxy (HMR)
|
||||||
|
location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
|
||||||
|
location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
|
||||||
|
location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
|
||||||
|
CONF
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "}" >> "$outfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Builder 2: 80→443 Redirect + 443/TLS (Live-Server) ────────────────────
|
||||||
|
# $1=host, $2=cert_dir (/etc/ssl/ui | /etc/ssl/webmail), $3=outfile
|
||||||
|
build_site_tls(){
|
||||||
|
local host="$1" cert_dir="$2" outfile="$3"
|
||||||
|
local cert="${cert_dir}/fullchain.pem"
|
||||||
|
local key="${cert_dir}/privkey.pem"
|
||||||
|
|
||||||
|
cat > "$outfile" <<CONF
|
||||||
|
# --- ${host} : HTTP (ACME + Redirect) ---
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name ${host};
|
||||||
|
|
||||||
|
# ACME HTTP-01 auf Port 80
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root ${ACME_ROOT};
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 301 https://\$host\$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- ${host} : HTTPS ---
|
||||||
|
server {
|
||||||
|
listen 443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
server_name ${host};
|
||||||
|
|
||||||
|
ssl_certificate ${cert};
|
||||||
|
ssl_certificate_key ${key};
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
|
||||||
|
# WICHTIG: ACME auch auf 443, sonst 404 bei Redirects
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root ${ACME_ROOT};
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
root ${APP_DIR}/public;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/${host}_ssl_access.log;
|
||||||
|
error_log /var/log/nginx/${host}_ssl_error.log;
|
||||||
|
|
||||||
|
client_max_body_size 25m;
|
||||||
|
|
||||||
|
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
||||||
|
|
||||||
|
location ~ \.php\$ {
|
||||||
|
include snippets/fastcgi-php.conf;
|
||||||
|
${FASTCGI_PASS}
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
||||||
|
|
||||||
|
# WebSocket: Laravel Reverb (Backend intern HTTP)
|
||||||
|
location /ws/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection "Upgrade";
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_pass http://127.0.0.1:8080/;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverb HTTP API
|
||||||
|
location /apps/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_pass http://127.0.0.1:8080/apps/;
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
|
||||||
|
if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
cat >> "$outfile" <<'CONF'
|
||||||
|
# DEV: Vite-Proxy
|
||||||
|
location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
|
||||||
|
location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
|
||||||
|
location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
|
||||||
|
CONF
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "}" >> "$outfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_site_acme_only(){
|
||||||
|
local host="$1" outfile="$2"
|
||||||
|
|
||||||
|
cat > "$outfile" <<CONF
|
||||||
|
# --- ${host} : ACME-only (80 + 443), KEIN App-Root ---
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name ${host};
|
||||||
|
|
||||||
|
# HTTP-01 Challenge exakt ausliefern
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root ${ACME_ROOT};
|
||||||
|
default_type "text/plain";
|
||||||
|
try_files \$uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alles andere → nach https
|
||||||
|
location / { return 301 https://\$host\$request_uri; }
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
||||||
|
server_name ${host};
|
||||||
|
|
||||||
|
ssl_certificate /etc/ssl/mail/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/ssl/mail/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
|
||||||
|
# Auch via https die Challenge bedienen (falls Redirects gefolgt werden)
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root ${ACME_ROOT};
|
||||||
|
default_type "text/plain";
|
||||||
|
try_files \$uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sonst nichts preisgeben
|
||||||
|
location / { return 444; }
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Sites erzeugen ─────────────────────────────────────────────────────────
|
||||||
|
MX_SITE="/etc/nginx/sites-available/mx-mailwolt.conf"
|
||||||
|
UI_SITE="/etc/nginx/sites-available/ui-mailwolt.conf"
|
||||||
|
WEBMAIL_SITE="/etc/nginx/sites-available/webmail-mailwolt.conf"
|
||||||
|
|
||||||
|
# UI & Webmail wie gehabt …
|
||||||
|
#if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
||||||
|
# build_site_http_only "$UI_HOST" "$UI_SITE"
|
||||||
|
# build_site_http_only "$WEBMAIL_HOST" "$WEBMAIL_SITE"
|
||||||
|
#else
|
||||||
|
# build_site_tls "$UI_HOST" "/etc/ssl/ui" "$UI_SITE"
|
||||||
|
# build_site_tls "$WEBMAIL_HOST" "/etc/ssl/webmail" "$WEBMAIL_SITE"
|
||||||
|
#fi
|
||||||
|
|
||||||
|
# UI & Webmail …
|
||||||
|
if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
# UI = Catch-All + default_server, Webmail = Catch-All ohne default
|
||||||
|
build_site_http_only "_" "$UI_SITE" "default"
|
||||||
|
build_site_http_only "_" "$WEBMAIL_SITE" "nodefault"
|
||||||
|
else
|
||||||
|
if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
||||||
|
build_site_http_only "$UI_HOST" "$UI_SITE"
|
||||||
|
build_site_http_only "$WEBMAIL_HOST" "$WEBMAIL_SITE"
|
||||||
|
else
|
||||||
|
build_site_tls "$UI_HOST" "/etc/ssl/ui" "$UI_SITE"
|
||||||
|
build_site_tls "$WEBMAIL_HOST" "/etc/ssl/webmail" "$WEBMAIL_SITE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
#if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
# # DEV: per IP erreichbar → Catch-All („_“) und HTTP-only
|
||||||
|
# build_site_http_only "_" "$UI_SITE"
|
||||||
|
# build_site_http_only "_" "$WEBMAIL_SITE"
|
||||||
|
#else
|
||||||
|
# if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
||||||
|
# build_site_http_only "$UI_HOST" "$UI_SITE"
|
||||||
|
# build_site_http_only "$WEBMAIL_HOST" "$WEBMAIL_SITE"
|
||||||
|
# else
|
||||||
|
# build_site_tls "$UI_HOST" "/etc/ssl/ui" "$UI_SITE"
|
||||||
|
# build_site_tls "$WEBMAIL_HOST" "/etc/ssl/webmail" "$WEBMAIL_SITE"
|
||||||
|
# fi
|
||||||
|
#fi
|
||||||
|
|
||||||
|
# MX: **immer** ACME-only (kein Laravel dahinter)
|
||||||
|
build_site_acme_only "${MAIL_HOSTNAME}" "$MX_SITE"
|
||||||
|
|
||||||
|
ln -sf "$UI_SITE" /etc/nginx/sites-enabled/ui-mailwolt.conf
|
||||||
|
ln -sf "$WEBMAIL_SITE" /etc/nginx/sites-enabled/webmail-mailwolt.conf
|
||||||
|
ln -sf "$MX_SITE" /etc/nginx/sites-enabled/mx-mailwolt.conf
|
||||||
|
|
||||||
|
# ── Real-IP nur, wenn Proxy davor ──────────────────────────────────────────
|
||||||
|
if [[ "${PROXY_MODE}" -eq 1 && -n "${NPM_IP}" ]]; then
|
||||||
|
cat > /etc/nginx/conf.d/realip.conf <<NGX
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
set_real_ip_from ${NPM_IP};
|
||||||
|
real_ip_recursive on;
|
||||||
|
NGX
|
||||||
|
else
|
||||||
|
rm -f /etc/nginx/conf.d/realip.conf || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Test & reload ──────────────────────────────────────────────────────────
|
||||||
|
if nginx -t; then
|
||||||
|
systemctl enable --now nginx >/dev/null 2>&1 || true
|
||||||
|
systemctl reload nginx || true
|
||||||
|
else
|
||||||
|
die "nginx -t fehlgeschlagen – siehe /var/log/nginx/*.log"
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
ACME_WEBROOT="/var/www/letsencrypt"
|
||||||
|
install -d -m 0755 "${ACME_WEBROOT}/.well-known/acme-challenge"
|
||||||
|
|
||||||
|
# Staging optional (verbraucht kein Live-Limit)
|
||||||
|
CERTBOT_EXTRA=()
|
||||||
|
LE_STAGING="${LE_STAGING:-0}"
|
||||||
|
[[ "$LE_STAGING" = "1" ]] && CERTBOT_EXTRA+=(--test-cert)
|
||||||
|
|
||||||
|
# Einheitliche LE-Mail (Fallback)
|
||||||
|
LE_MAIL="${LE_EMAIL:-admin@${BASE_DOMAIN}}"
|
||||||
|
|
||||||
|
resolve_ok() {
|
||||||
|
local host="$1"
|
||||||
|
local pats=()
|
||||||
|
[[ -n "${SERVER_PUBLIC_IPV4:-}" ]] && pats+=("${SERVER_PUBLIC_IPV4//./\\.}")
|
||||||
|
[[ -n "${SERVER_PUBLIC_IPV6:-}" ]] && pats+=("${SERVER_PUBLIC_IPV6//:/\\:}")
|
||||||
|
[[ ${#pats[@]} -eq 0 ]] && return 0
|
||||||
|
getent ahosts "$host" | awk '{print $1}' | sort -u \
|
||||||
|
| grep -Eq "^($(IFS='|'; echo "${pats[*]}"))$"
|
||||||
|
}
|
||||||
|
|
||||||
|
probe_http() {
|
||||||
|
local host="$1"
|
||||||
|
echo test > "${ACME_WEBROOT}/.well-known/acme-challenge/_probe"
|
||||||
|
curl -fsS --max-time 5 -4 "http://${host}/.well-known/acme-challenge/_probe" >/dev/null \
|
||||||
|
|| curl -fsS --max-time 5 -6 "http://${host}/.well-known/acme-challenge/_probe" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
issue() {
|
||||||
|
local host="${1:-}"
|
||||||
|
[[ -z "$host" ]] && return 0
|
||||||
|
|
||||||
|
echo "[i] Versuche LE für ${host} …"
|
||||||
|
|
||||||
|
if ! resolve_ok "$host"; then
|
||||||
|
echo "[!] DNS zeigt (noch) nicht hierher – überspringe: ${host}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! probe_http "$host"; then
|
||||||
|
echo "[!] ACME-HTTP-Check für ${host} fehlgeschlagen (Port 80/IPv6/Firewall/Nginx prüfen)."
|
||||||
|
# wir versuchen trotzdem – Certbot meldet sich, falls es scheitert
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
# Für MX den Key wiederverwenden → stabiler TLSA (3 1 1)
|
||||||
|
[[ "$host" == "${MAIL_HOSTNAME}" ]] && EXTRA_ARGS+=(--reuse-key)
|
||||||
|
|
||||||
|
# WICHTIG: Deploy-Wrapper anhängen, damit Symlinks/Nginx gesetzt werden
|
||||||
|
certbot certonly \
|
||||||
|
--agree-tos -m "${LE_MAIL}" --non-interactive \
|
||||||
|
--webroot -w "${ACME_WEBROOT}" -d "${host}" \
|
||||||
|
--deploy-hook /usr/local/sbin/mailwolt-deploy.sh \
|
||||||
|
"${EXTRA_ARGS[@]}" "${CERTBOT_EXTRA[@]}" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${BASE_DOMAIN}" != "example.com" ]]; then
|
||||||
|
issue "${UI_HOST:-}"
|
||||||
|
issue "${WEBMAIL_HOST:-}"
|
||||||
|
issue "${MAIL_HOSTNAME:-}"
|
||||||
|
|
||||||
|
# Nginx nur neu laden, wenn aktiv
|
||||||
|
if systemctl is-active --quiet nginx; then
|
||||||
|
systemctl reload nginx || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[i] BASE_DOMAIN=example.com – LE wird übersprungen."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# FIX: Validierung & Reparatur des Mail-Zertifikats
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
MAIL_SSL_DIR="/etc/ssl/mail"
|
||||||
|
install -d -m 0755 "$MAIL_SSL_DIR"
|
||||||
|
|
||||||
|
MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"
|
||||||
|
MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
|
||||||
|
|
||||||
|
HOST="${MAIL_HOSTNAME:-}"
|
||||||
|
LE_DIR=""
|
||||||
|
[[ -n "$HOST" ]] && LE_DIR="/etc/letsencrypt/live/${HOST}"
|
||||||
|
|
||||||
|
need_fix=0
|
||||||
|
|
||||||
|
# Ist der vorhandene Key gültig? (leer/nicht vorhanden/ungültig -> fix)
|
||||||
|
if [[ ! -s "$MAIL_KEY" ]] || ! openssl pkey -in "$MAIL_KEY" -noout >/dev/null 2>&1; then
|
||||||
|
need_fix=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wenn Fix nötig: aus Let's Encrypt Live kopieren
|
||||||
|
if [[ $need_fix -eq 1 ]]; then
|
||||||
|
echo "[!] Ungültiger oder fehlender Mail-Private-Key – versuche Reparatur …"
|
||||||
|
if [[ -n "$LE_DIR" && -r "${LE_DIR}/privkey.pem" && -r "${LE_DIR}/fullchain.pem" ]]; then
|
||||||
|
cp -f "${LE_DIR}/privkey.pem" "$MAIL_KEY"
|
||||||
|
cp -f "${LE_DIR}/fullchain.pem" "$MAIL_CERT"
|
||||||
|
chown root:root "$MAIL_CERT" "$MAIL_KEY"
|
||||||
|
chmod 600 "$MAIL_KEY"
|
||||||
|
chmod 644 "$MAIL_CERT"
|
||||||
|
echo "[+] Zertifikate neu kopiert aus ${LE_DIR}."
|
||||||
|
# Reload NICHT sofort – flaggen für 90-services
|
||||||
|
touch /run/mailwolt.need-dovecot-reload
|
||||||
|
else
|
||||||
|
echo "[!] Konnte ${LE_DIR}/privkey.pem oder fullchain.pem nicht lesen – bitte prüfen."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[✓] Mail-Zertifikat & -Key sind gültig."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Optionaler Live-Check (nur wenn Host gesetzt)
|
||||||
|
if [[ -n "$HOST" ]]; then
|
||||||
|
if openssl s_client -connect "${HOST}:993" -servername "${HOST}" </dev/null 2>/dev/null \
|
||||||
|
| grep -q "Verify return code: 0"; then
|
||||||
|
echo "[✓] TLS-Handshake erfolgreich auf imaps://${HOST}:993."
|
||||||
|
else
|
||||||
|
echo "[!] TLS-Handshake auf imaps://${HOST}:993 fehlgeschlagen (Dovecot Reload folgt in 90-services, falls Flag gesetzt)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,343 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
# --- Helper: sicherer Frontend-Build als APP_USER ---------------------------
|
||||||
|
safe_frontend_build() {
|
||||||
|
echo "[i] Frontend build …"
|
||||||
|
|
||||||
|
# Verzeichnisse & Rechte vorbereiten (Gruppen-sticky & ACL)
|
||||||
|
install -d -m 2775 -o "$APP_USER" -g "$APP_GROUP" \
|
||||||
|
"${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.npm-cache"
|
||||||
|
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}"
|
||||||
|
find "${APP_DIR}" -type d -exec chmod 2775 {} \;
|
||||||
|
find "${APP_DIR}" -type f -exec chmod 664 {} \;
|
||||||
|
setfacl -R -m g:"$APP_GROUP":rwX -m d:g:"$APP_GROUP":rwX "${APP_DIR}" || true
|
||||||
|
|
||||||
|
# Vite-/Build-Reste bereinigen (falls mal root dort gebaut hat)
|
||||||
|
rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true
|
||||||
|
|
||||||
|
# npm auf projektlokales Cache konfigurieren
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cat > ~/.npmrc <<'RC'
|
||||||
|
fund=false
|
||||||
|
audit=false
|
||||||
|
prefer-offline=true
|
||||||
|
cache=${APP_DIR}/.npm-cache
|
||||||
|
RC"
|
||||||
|
|
||||||
|
# Node ggf. installieren
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||||
|
apt-get install -y nodejs
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Dependencies + Build (als App-User)
|
||||||
|
if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && (npm ci --no-audit --no-fund || npm install --no-audit --no-fund) && npm run build"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[!] Build fehlgeschlagen – Rechtefix + Clean + Retry …"
|
||||||
|
rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}"
|
||||||
|
find "${APP_DIR}" -type d -exec chmod 2775 {} \;
|
||||||
|
find "${APP_DIR}" -type f -exec chmod 664 {} \;
|
||||||
|
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build"
|
||||||
|
}
|
||||||
|
|
||||||
|
relink_and_reload() {
|
||||||
|
if [[ -d /etc/letsencrypt/renewal-hooks/deploy ]]; then
|
||||||
|
run-parts /etc/letsencrypt/renewal-hooks/deploy || true
|
||||||
|
fi
|
||||||
|
if systemctl is-active --quiet nginx; then
|
||||||
|
systemctl reload nginx || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log "App bereitstellen …"
|
||||||
|
mkdir -p "$(dirname "$APP_DIR")"
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "$(dirname "$APP_DIR")"
|
||||||
|
|
||||||
|
# Repo holen oder Laravel anlegen – passe GIT_REPO/GIT_BRANCH bei Bedarf an
|
||||||
|
GIT_REPO="${GIT_REPO:-https://git.nexlab.at/boban/mailwolt.git}"
|
||||||
|
GIT_BRANCH="${GIT_BRANCH:-main}"
|
||||||
|
|
||||||
|
if [[ "${GIT_REPO}" == "https://example.com/your-repo-placeholder.git" ]]; then
|
||||||
|
[[ -d "$APP_DIR" && -n "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]] || \
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd /var/www && composer create-project laravel/laravel ${APP_USER} --no-interaction"
|
||||||
|
else
|
||||||
|
if [[ ! -d "${APP_DIR}/.git" ]]; then
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}"
|
||||||
|
else
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && git fetch --depth=1 origin ${GIT_BRANCH} && git reset --hard origin/${GIT_BRANCH}"
|
||||||
|
fi
|
||||||
|
[[ -f "${APP_DIR}/composer.json" ]] && sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENV_FILE="${APP_DIR}/.env"
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
|
||||||
|
grep -q '^APP_KEY=' "$ENV_FILE" || echo "APP_KEY=" >> "$ENV_FILE"
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force || true"
|
||||||
|
|
||||||
|
# --- App-URL/Hosts ----------------------------------------------------------
|
||||||
|
#SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-}"
|
||||||
|
#if [[ -z "$SERVER_PUBLIC_IPV4" ]] && command -v curl >/dev/null 2>&1; then
|
||||||
|
# SERVER_PUBLIC_IPV4="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
|
||||||
|
# [[ "$SERVER_PUBLIC_IPV4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || SERVER_PUBLIC_IPV4=""
|
||||||
|
#fi
|
||||||
|
#[[ -n "$SERVER_PUBLIC_IPV4" ]] || SERVER_PUBLIC_IPV4="$(detect_ip)"
|
||||||
|
#
|
||||||
|
#UI_CERT="/etc/ssl/ui/fullchain.pem"
|
||||||
|
#UI_KEY="/etc/ssl/ui/privkey.pem"
|
||||||
|
#
|
||||||
|
#if [[ -n "${UI_HOST:-}" ]]; then
|
||||||
|
# APP_HOST_VAL="$UI_HOST"
|
||||||
|
# APP_URL_VAL="https://${UI_HOST}"
|
||||||
|
#else
|
||||||
|
# APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
|
||||||
|
# SCHEME="http"
|
||||||
|
# [[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
|
||||||
|
# APP_URL_VAL="${SCHEME}://${SERVER_PUBLIC_IPV4}"
|
||||||
|
#fi
|
||||||
|
|
||||||
|
SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-}"
|
||||||
|
if [[ -z "$SERVER_PUBLIC_IPV4" ]] && command -v curl >/dev/null 2>&1; then
|
||||||
|
SERVER_PUBLIC_IPV4="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
|
||||||
|
[[ "$SERVER_PUBLIC_IPV4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || SERVER_PUBLIC_IPV4=""
|
||||||
|
fi
|
||||||
|
[[ -n "$SERVER_PUBLIC_IPV4" ]] || SERVER_PUBLIC_IPV4="$(detect_ip)"
|
||||||
|
|
||||||
|
UI_CERT="/etc/ssl/ui/fullchain.pem"
|
||||||
|
UI_KEY="/etc/ssl/ui/privkey.pem"
|
||||||
|
|
||||||
|
# DEV-Modus: immer IP als Host, http (kein example.com / keine Fake-Domain)
|
||||||
|
if [[ "${DEV_MODE:-0}" = "1" || "${APP_ENV:-production}" = "local" ]]; then
|
||||||
|
APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
|
||||||
|
APP_URL_VAL="http://${APP_HOST_VAL}"
|
||||||
|
else
|
||||||
|
# PROD/normal: wenn UI_HOST gesetzt → benutzen, sonst IP
|
||||||
|
if [[ -n "${UI_HOST:-}" ]]; then
|
||||||
|
APP_HOST_VAL="$UI_HOST"
|
||||||
|
APP_URL_VAL="https://${UI_HOST}"
|
||||||
|
else
|
||||||
|
APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
|
||||||
|
SCHEME="http"
|
||||||
|
[[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
|
||||||
|
APP_URL_VAL="${SCHEME}://${APP_HOST_VAL}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
SECURE=$([[ "${APP_ENV}" = "production" ]] && echo true || echo false)
|
||||||
|
|
||||||
|
# --- .env schreiben ---------------------------------------------------------
|
||||||
|
upsert_env APP_URL "${APP_URL_VAL}"
|
||||||
|
|
||||||
|
if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
||||||
|
TP_LIST="127.0.0.1,::1"
|
||||||
|
[[ -n "${NPM_IP:-}" ]] && TP_LIST="${TP_LIST},${NPM_IP}"
|
||||||
|
upsert_env TRUSTED_PROXIES "$TP_LIST"
|
||||||
|
upsert_env TRUSTED_HEADERS "x-forwarded-all"
|
||||||
|
else
|
||||||
|
upsert_env TRUSTED_PROXIES ""
|
||||||
|
upsert_env TRUSTED_HEADERS "x-forwarded-all"
|
||||||
|
fi
|
||||||
|
|
||||||
|
upsert_env APP_HOST "${APP_HOST_VAL}"
|
||||||
|
upsert_env APP_NAME "${APP_NAME}"
|
||||||
|
upsert_env APP_ENV "${APP_ENV:-production}"
|
||||||
|
upsert_env APP_DEBUG "${APP_DEBUG:-false}"
|
||||||
|
upsert_env APP_TIMEZONE "${APP_TZ:-UTC}"
|
||||||
|
|
||||||
|
upsert_env APP_LOCALE "${APP_LOCALE:-de}"
|
||||||
|
upsert_env APP_FALLBACK_LOCALE "en"
|
||||||
|
|
||||||
|
upsert_env SERVER_PUBLIC_IPV4 "${SERVER_PUBLIC_IPV4}"
|
||||||
|
upsert_env SERVER_PUBLIC_IPV6 "${SERVER_PUBLIC_IPV6:-}"
|
||||||
|
|
||||||
|
upsert_env SYSMAIL_SUB "${SYSMAIL_SUB}"
|
||||||
|
upsert_env SYSMAIL_DOMAIN "${SYSMAIL_DOMAIN}"
|
||||||
|
upsert_env DKIM_ENABLE "${DKIM_ENABLE}"
|
||||||
|
upsert_env DKIM_SELECTOR "${DKIM_SELECTOR}"
|
||||||
|
upsert_env DKIM_GENERATE "${DKIM_GENERATE}"
|
||||||
|
|
||||||
|
upsert_env BASE_DOMAIN "${BASE_DOMAIN}"
|
||||||
|
upsert_env UI_SUB "${UI_SUB}"
|
||||||
|
upsert_env WEBMAIL_SUB "${WEBMAIL_SUB}"
|
||||||
|
upsert_env MTA_SUB "${MTA_SUB}"
|
||||||
|
upsert_env LE_EMAIL "${LE_EMAIL:-admin@${BASE_DOMAIN}}"
|
||||||
|
|
||||||
|
upsert_env DB_CONNECTION "mysql"
|
||||||
|
upsert_env DB_HOST "127.0.0.1"
|
||||||
|
upsert_env DB_PORT "3306"
|
||||||
|
upsert_env DB_DATABASE "${DB_NAME}"
|
||||||
|
upsert_env DB_USERNAME "${DB_USER}"
|
||||||
|
upsert_env DB_PASSWORD "${DB_PASS}"
|
||||||
|
|
||||||
|
upsert_env CACHE_SETTINGS_STORE "redis"
|
||||||
|
upsert_env CACHE_STORE "redis"
|
||||||
|
upsert_env CACHE_DRIVER "redis"
|
||||||
|
upsert_env CACHE_PREFIX "${APP_USER_PREFIX}_cache:"
|
||||||
|
upsert_env SESSION_DRIVER "redis"
|
||||||
|
upsert_env SESSION_SECURE_COOKIE "${SECURE}" # DEV=false, PROD=true
|
||||||
|
upsert_env SESSION_SAMESITE "lax"
|
||||||
|
upsert_env REDIS_CLIENT "phpredis"
|
||||||
|
upsert_env REDIS_HOST "127.0.0.1"
|
||||||
|
upsert_env REDIS_PORT "6379"
|
||||||
|
upsert_env REDIS_PASSWORD "${REDIS_PASS}"
|
||||||
|
upsert_env REDIS_DB "0"
|
||||||
|
upsert_env REDIS_CACHE_DB "1"
|
||||||
|
upsert_env REDIS_CACHE_CONNECTION "cache"
|
||||||
|
upsert_env REDIS_CACHE_LOCK_CONNECTION "default"
|
||||||
|
|
||||||
|
upsert_env BROADCAST_DRIVER "reverb"
|
||||||
|
upsert_env QUEUE_CONNECTION "redis"
|
||||||
|
upsert_env LOG_CHANNEL "daily"
|
||||||
|
|
||||||
|
upsert_env REVERB_APP_ID "${APP_USER_PREFIX}"
|
||||||
|
grep -q '^REVERB_APP_KEY=' "$ENV_FILE" || upsert_env REVERB_APP_KEY "${APP_USER_PREFIX}_$(openssl rand -hex 16)"
|
||||||
|
grep -q '^REVERB_APP_SECRET=' "$ENV_FILE" || upsert_env REVERB_APP_SECRET "${APP_USER_PREFIX}_$(openssl rand -hex 32)"
|
||||||
|
upsert_env REVERB_HOST "\${APP_HOST}"
|
||||||
|
upsert_env REVERB_PORT "443"
|
||||||
|
upsert_env REVERB_SCHEME "https"
|
||||||
|
upsert_env REVERB_PATH "/ws"
|
||||||
|
upsert_env REVERB_SCALING_ENABLED "true"
|
||||||
|
upsert_env REVERB_SCALING_CHANNEL "reverb"
|
||||||
|
|
||||||
|
upsert_env VITE_REVERB_APP_KEY "\${REVERB_APP_KEY}"
|
||||||
|
upsert_env VITE_REVERB_HOST "\${REVERB_HOST}"
|
||||||
|
upsert_env VITE_REVERB_PORT "\${REVERB_PORT}"
|
||||||
|
upsert_env VITE_REVERB_SCHEME "\${REVERB_SCHEME}"
|
||||||
|
upsert_env VITE_REVERB_PATH "\${REVERB_PATH}"
|
||||||
|
|
||||||
|
upsert_env REVERB_SERVER_APP_KEY "\${REVERB_APP_KEY}"
|
||||||
|
upsert_env REVERB_SERVER_HOST "127.0.0.1"
|
||||||
|
upsert_env REVERB_SERVER_PORT "8080"
|
||||||
|
upsert_env REVERB_SERVER_PATH ""
|
||||||
|
upsert_env REVERB_SERVER_SCHEME "http"
|
||||||
|
|
||||||
|
# --- DEV Block (optional) ---------------------------------------------------
|
||||||
|
DEV_MODE="${DEV_MODE:-0}"
|
||||||
|
if [[ "$DEV_MODE" = "1" ]]; then
|
||||||
|
sed -i '/^# --- MailWolt DEV/,/^# --- \/MailWolt DEV/d' "${ENV_FILE}"
|
||||||
|
cat >> "${ENV_FILE}" <<CONF
|
||||||
|
# --- MailWolt DEV ---
|
||||||
|
VITE_DEV_HOST=127.0.0.1
|
||||||
|
VITE_DEV_PORT=5173
|
||||||
|
VITE_HMR_PROTOCOL=wss
|
||||||
|
VITE_HMR_CLIENT_PORT=443
|
||||||
|
VITE_HMR_HOST=${SERVER_PUBLIC_IPV4}
|
||||||
|
VITE_DEV_ORIGIN=$(grep '^APP_URL=' "${ENV_FILE}" | cut -d= -f2-)
|
||||||
|
# --- /MailWolt DEV ---
|
||||||
|
CONF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Zertifikate in /etc/ssl/* bereitstellen, bevor Laravel irgendwas liest --
|
||||||
|
relink_and_reload
|
||||||
|
|
||||||
|
# --- RECHTE FIXEN: storage & bootstrap/cache (www-data + mailwolt) ----------
|
||||||
|
log "Setze korrekte Rechte für Laravel-Verzeichnisse …"
|
||||||
|
cd "${APP_DIR}"
|
||||||
|
chgrp -R www-data storage bootstrap/cache || true
|
||||||
|
find storage bootstrap/cache -type d -exec chmod 2775 {} \; || true
|
||||||
|
find storage bootstrap/cache -type f -exec chmod 0664 {} \; || true
|
||||||
|
setfacl -R -m u:www-data:rwx,u:${APP_USER}:rwx storage bootstrap/cache || true
|
||||||
|
setfacl -dR -m u:www-data:rwx,u:${APP_USER}:rwx storage bootstrap/cache || true
|
||||||
|
log "[✓] Schreibrechte für Laravel korrigiert."
|
||||||
|
|
||||||
|
# --- DKIM: Verzeichnisse & Basisrechte --------------------------------------
|
||||||
|
install -d -m 2775 -o "$APP_USER" -g www-data "$APP_DIR/storage/app/private"
|
||||||
|
install -d -m 2775 -o "$APP_USER" -g www-data "$APP_DIR/storage/app/private/dkim"
|
||||||
|
setfacl -R -m u:${APP_USER}:rwx,u:www-data:rwx "$APP_DIR/storage/app/private" || true
|
||||||
|
setfacl -dR -m u:${APP_USER}:rwx,u:www-data:rwx "$APP_DIR/storage/app/private" || true
|
||||||
|
|
||||||
|
# --- OpenDKIM: keys & DNS-Verzeichnis --------------------------------------
|
||||||
|
install -d -m 0750 -o opendkim -g opendkim /etc/opendkim
|
||||||
|
install -d -m 0750 -o opendkim -g opendkim /etc/opendkim/keys
|
||||||
|
install -d -m 0755 -o root -g root /etc/mailwolt
|
||||||
|
install -d -m 0755 -o root -g root /etc/mailwolt/dns
|
||||||
|
|
||||||
|
# --- Caches leeren, Migrationen ausführen -----------------------------------
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear"
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan migrate --force"
|
||||||
|
|
||||||
|
# --- Seeder (legt Domains/DKIM etc. an) -------------------------------------
|
||||||
|
if [[ "${BASE_DOMAIN}" != "example.com" ]]; then
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan db:seed --class=SystemDomainSeeder --force"
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan db:seed --class=SystemBackupSeeder --force"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- DKIM für SYSMAIL_DOMAIN via App erzeugen & per Helper einhängen --------
|
||||||
|
DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
||||||
|
DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
||||||
|
SYSMAIL_DOMAIN="${SYSMAIL_DOMAIN:-sysmail.${BASE_DOMAIN}}"
|
||||||
|
|
||||||
|
if [[ "${DKIM_ENABLE}" = "1" && -n "${SYSMAIL_DOMAIN}" ]]; then
|
||||||
|
log "Erzeuge/aktualisiere DKIM für ${SYSMAIL_DOMAIN} (Selector: ${DKIM_SELECTOR}) …"
|
||||||
|
|
||||||
|
# 1) In der App generieren (als mailwolt), und Pfad + TXT zurückgeben
|
||||||
|
OUT="$(sudo -u "${APP_USER}" -H bash -lc "
|
||||||
|
set -e
|
||||||
|
cd '${APP_DIR}'
|
||||||
|
php -r '
|
||||||
|
require \"vendor/autoload.php\";
|
||||||
|
\$app=require \"bootstrap/app.php\";
|
||||||
|
\$app->make(Illuminate\\Contracts\\Console\\Kernel::class)->bootstrap();
|
||||||
|
\$d = App\\Models\\Domain::firstOrCreate([\"domain\"=>\"${SYSMAIL_DOMAIN}\"],[\"is_active\"=>1,\"is_system\"=>1]);
|
||||||
|
\$r = app(App\\Services\\DkimService::class)->generateForDomain(\$d, 2048, \"${DKIM_SELECTOR}\");
|
||||||
|
echo \$r[\"priv_path\"], \"\\n\";
|
||||||
|
echo \$r[\"dns_txt\"], \"\\n\";
|
||||||
|
'
|
||||||
|
")"
|
||||||
|
|
||||||
|
PRIV_PATH="$(printf '%s\n' "$OUT" | sed -n '1p')"
|
||||||
|
DNS_TXT="$(printf '%s\n' "$OUT" | sed -n '2,$p')"
|
||||||
|
|
||||||
|
if [[ -z "$PRIV_PATH" || ! -s "$PRIV_PATH" ]]; then
|
||||||
|
echo "[!] DKIM priv_path fehlt oder Datei leer: $PRIV_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_TXT="$(mktemp /tmp/dkim_txt_XXXXXX.txt)"
|
||||||
|
printf '%s' "$DNS_TXT" >"$TMP_TXT"
|
||||||
|
|
||||||
|
# 2) Root-Helper ausführen (hängt Key ein, pflegt Key/SigningTable, kopiert TXT)
|
||||||
|
if [[ -x /usr/local/sbin/mailwolt-install-dkim ]]; then
|
||||||
|
/usr/local/sbin/mailwolt-install-dkim "${SYSMAIL_DOMAIN}" "${DKIM_SELECTOR}" "${PRIV_PATH}" "${TMP_TXT}"
|
||||||
|
else
|
||||||
|
echo "[!] Helper /usr/local/sbin/mailwolt-install-dkim fehlt oder ist nicht ausführbar." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$TMP_TXT" || true
|
||||||
|
|
||||||
|
# 3) OpenDKIM neu laden
|
||||||
|
touch /run/mailwolt.need-opendkim-reload || true
|
||||||
|
else
|
||||||
|
log "DKIM übersprungen (DKIM_ENABLE=${DKIM_ENABLE}, SYSMAIL_DOMAIN='${SYSMAIL_DOMAIN}')."
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# --- TLSA aus App heraus (idempotent; läuft, wenn Zert lesbar ist) ----------
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan dns:tlsa:refresh || true"
|
||||||
|
|
||||||
|
# --- Build Frontend (nur wenn nötig) ----------------------------------------
|
||||||
|
if [[ -f "${APP_DIR}/package.json" && ! -f "${APP_DIR}/public/build/manifest.json" ]]; then
|
||||||
|
safe_frontend_build
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Abschluss: Caches + Rechte + Reloads -----------------------------------
|
||||||
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache && php artisan optimize:clear"
|
||||||
|
|
||||||
|
# Konsistente Rechte/ACL für das gesamte App-Verzeichnis
|
||||||
|
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
|
||||||
|
find "$APP_DIR" -type d -exec chmod 2775 {} \;
|
||||||
|
find "$APP_DIR" -type f -exec chmod 664 {} \;
|
||||||
|
setfacl -R -m g:"$APP_GROUP":rwX -m d:g:"$APP_GROUP":rwX "$APP_DIR" || true
|
||||||
|
|
||||||
|
# Laravel-Write-Dirs sicherstellen (mit setgid & ACL)
|
||||||
|
install -d -m 2775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/storage" "$APP_DIR/bootstrap/cache"
|
||||||
|
chgrp -R www-data "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
|
||||||
|
find "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" -type d -exec chmod 2775 {} \; || true
|
||||||
|
find "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" -type f -exec chmod 0664 {} \; || true
|
||||||
|
setfacl -R -m u:www-data:rwx,u:${APP_USER}:rwx "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
|
||||||
|
setfacl -dR -m u:www-data:rwx,u:${APP_USER}:rwx "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Update-Wrapper & Sudoers …"
|
||||||
|
|
||||||
|
WRAPPER="/usr/local/sbin/mailwolt-update"
|
||||||
|
LOGFILE="/var/log/mailwolt-update.log"
|
||||||
|
STATEDIR="/var/lib/mailwolt/update"
|
||||||
|
SUDOERS="/etc/sudoers.d/mailwolt-update"
|
||||||
|
VERSION_FILE="/var/lib/mailwolt/version"
|
||||||
|
SUDOERS_SERVICES="/etc/sudoers.d/mailwolt-services"
|
||||||
|
SUDOERS_ARTISAN="/etc/sudoers.d/mailwolt-artisan"
|
||||||
|
|
||||||
|
# Kandidaten: wo liegt update.sh?
|
||||||
|
CANDIDATES=(
|
||||||
|
/opt/mailwolt-installer/scripts/update.sh
|
||||||
|
/mailwolt-installer/scripts/update.sh
|
||||||
|
/usr/local/lib/mailwolt/update.sh
|
||||||
|
)
|
||||||
|
|
||||||
|
# State/Log vorbereiten
|
||||||
|
install -d -m 0755 "$(dirname "$LOGFILE")"
|
||||||
|
install -d -m 0755 "$STATEDIR"
|
||||||
|
: > "$LOGFILE" || true
|
||||||
|
chmod 0644 "$LOGFILE"
|
||||||
|
|
||||||
|
# Wrapper erzeugen
|
||||||
|
cat > "$WRAPPER" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
LOG="/var/log/mailwolt-update.log"
|
||||||
|
STATE_DIR="/var/lib/mailwolt/update"
|
||||||
|
APP_DIR="/var/www/mailwolt"
|
||||||
|
WEB_USER="www-data"
|
||||||
|
|
||||||
|
CANDIDATES=(
|
||||||
|
/opt/mailwolt-installer/scripts/update.sh
|
||||||
|
/mailwolt-installer/scripts/update.sh
|
||||||
|
/usr/local/lib/mailwolt/update.sh
|
||||||
|
)
|
||||||
|
|
||||||
|
install -d -m 0755 "$(dirname "$LOG")" "$STATE_DIR" /var/lib/mailwolt
|
||||||
|
: > "$LOG" || true
|
||||||
|
chmod 0644 "$LOG"
|
||||||
|
|
||||||
|
echo "running" > "$STATE_DIR/state"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "===== $(date -Is) :: Update gestartet ====="
|
||||||
|
|
||||||
|
# --- Update-Script finden --------------------------------------------------
|
||||||
|
SCRIPT=""
|
||||||
|
for p in "${CANDIDATES[@]}"; do
|
||||||
|
if [[ -x "$p" ]]; then SCRIPT="$p"; break; fi
|
||||||
|
if [[ -f "$p" && -r "$p" ]]; then SCRIPT="$p"; break; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$SCRIPT" ]]; then
|
||||||
|
echo "[!] update.sh nicht gefunden (versucht: ${CANDIDATES[*]})"
|
||||||
|
rc=127
|
||||||
|
else
|
||||||
|
echo "[i] benutze: $SCRIPT"
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
echo "[!] Bitte als root ausführen"
|
||||||
|
rc=1
|
||||||
|
else
|
||||||
|
if [[ -x "$SCRIPT" ]]; then
|
||||||
|
ALLOW_DIRTY=1 "$SCRIPT"
|
||||||
|
else
|
||||||
|
ALLOW_DIRTY=1 bash "$SCRIPT"
|
||||||
|
fi
|
||||||
|
rc=$?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "===== $(date -Is) :: Update-Script beendet (rc=$rc) ====="
|
||||||
|
|
||||||
|
# --- Nach dem Update: Assets neu bauen & Laravel optimieren ---------------
|
||||||
|
if [ -d "$APP_DIR" ]; then
|
||||||
|
cd "$APP_DIR" || exit 1
|
||||||
|
|
||||||
|
echo "[i] Führe Composer aus (falls vorhanden) ..."
|
||||||
|
if [ -f composer.json ]; then
|
||||||
|
sudo -u "$WEB_USER" composer install --no-dev --prefer-dist --no-interaction -q || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[i] Baue Frontend-Assets neu ..."
|
||||||
|
if command -v npm >/dev/null 2>&1 && [ -f package.json ]; then
|
||||||
|
sudo -u "$WEB_USER" npm ci --silent || true
|
||||||
|
sudo -u "$WEB_USER" npm run build --silent || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[i] Führe Migrationen & Cache-Optimierungen durch ..."
|
||||||
|
sudo -u "$WEB_USER" php artisan migrate --force || true
|
||||||
|
sudo -u "$WEB_USER" php artisan config:cache || true
|
||||||
|
sudo -u "$WEB_USER" php artisan optimize:clear || true
|
||||||
|
sudo -u "$WEB_USER" php artisan route:cache || true
|
||||||
|
sudo -u "$WEB_USER" php artisan view:cache || true
|
||||||
|
|
||||||
|
echo "[i] Hebe Wartungsmodus auf ..."
|
||||||
|
sudo -u "$WEB_USER" php artisan up >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Version aktualisieren -------------------------------------------------
|
||||||
|
echo "[i] Aktualisiere Version ..."
|
||||||
|
if command -v git >/dev/null 2>&1; then
|
||||||
|
SRC="/var/www/mailwolt"
|
||||||
|
if [ ! -d "$SRC/.git" ]; then
|
||||||
|
SRC="/opt/mailwolt-installer"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config --global --add safe.directory "$SRC" || true
|
||||||
|
|
||||||
|
if [ -f "$SRC/.git/shallow" ]; then
|
||||||
|
git -C "$SRC" fetch --unshallow --quiet || true
|
||||||
|
fi
|
||||||
|
git -C "$SRC" fetch --tags --quiet origin || true
|
||||||
|
|
||||||
|
raw="$(git -C "$SRC" describe --tags --always --dirty 2>/dev/null || echo "unknown")"
|
||||||
|
norm="$(printf '%s' "$raw" | sed -E 's/^[vV]//; s/-.*$//')"
|
||||||
|
|
||||||
|
printf '%s\n' "$raw" > /var/lib/mailwolt/version_raw
|
||||||
|
printf '%s\n' "$norm" > /var/lib/mailwolt/version
|
||||||
|
chmod 0644 /var/lib/mailwolt/version_raw /var/lib/mailwolt/version
|
||||||
|
|
||||||
|
echo "[i] Version aktualisiert: raw=$raw norm=$norm (Quelle: $SRC)"
|
||||||
|
else
|
||||||
|
echo "unknown" > /var/lib/mailwolt/version_raw
|
||||||
|
echo "0.0.0" > /var/lib/mailwolt/version
|
||||||
|
chmod 0644 /var/lib/mailwolt/version_raw /var/lib/mailwolt/version
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Services neu starten --------------------------------------------------
|
||||||
|
echo "[i] Starte MailWolt-Dienste neu ..."
|
||||||
|
sudo -u "$WEB_USER" php artisan mailwolt:restart-services || true
|
||||||
|
|
||||||
|
# --- Abschluss -------------------------------------------------------------
|
||||||
|
printf '%s\n' "$rc" > "$STATE_DIR/rc"
|
||||||
|
echo "done" > "$STATE_DIR/state"
|
||||||
|
echo "===== $(date -Is) :: Update beendet ====="
|
||||||
|
exit "$rc"
|
||||||
|
|
||||||
|
} | tee -a "$LOG"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 0755 "$WRAPPER"
|
||||||
|
chown root:root "$WRAPPER"
|
||||||
|
|
||||||
|
# Sudoers: www-data (Laravel) & mailwolt dürfen den Wrapper laufen lassen
|
||||||
|
cat > "$SUDOERS" <<'EOF'
|
||||||
|
Defaults!/usr/local/sbin/mailwolt-update !requiretty
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown root:root "$SUDOERS"
|
||||||
|
chmod 440 "$SUDOERS"
|
||||||
|
|
||||||
|
if ! visudo -c -f "$SUDOERS" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS – entferne Datei."
|
||||||
|
rm -f "$SUDOERS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$SUDOERS_SERVICES" <<'EOF'
|
||||||
|
Defaults!/usr/bin/systemctl !requiretty
|
||||||
|
|
||||||
|
Cmnd_Alias MW_SERVICES = \
|
||||||
|
/usr/bin/systemctl reload nginx.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart nginx.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart postfix.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart dovecot.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart rspamd.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart opendkim.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart opendmarc.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart clamav-daemon.service, \
|
||||||
|
/usr/bin/systemctl try-reload-or-restart redis-server.service
|
||||||
|
|
||||||
|
www-data ALL=(root) NOPASSWD: MW_SERVICES
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 440 "$SUDOERS_SERVICES"
|
||||||
|
chown root:root "$SUDOERS_SERVICES"
|
||||||
|
|
||||||
|
# Prüfen, ob Syntax gültig ist
|
||||||
|
if ! visudo -c -f "$SUDOERS_SERVICES" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS_SERVICES – entferne Datei."
|
||||||
|
rm -f "$SUDOERS_SERVICES"
|
||||||
|
else
|
||||||
|
echo "[✓] Sudoers für Dienststeuerung angelegt: $SUDOERS_SERVICES"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Version-File initial anlegen, falls nicht existiert
|
||||||
|
if [[ ! -f "$VERSION_FILE" ]]; then
|
||||||
|
echo "unknown" > "$VERSION_FILE"
|
||||||
|
chmod 0644 "$VERSION_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$SUDOERS_ARTISAN" <<'EOF'
|
||||||
|
# mailwolt darf artisan im App-Verzeichnis als www-data ausführen (ohne Passwort)
|
||||||
|
mailwolt ALL=(www-data) NOPASSWD: /usr/bin/php /var/www/mailwolt/artisan *
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown root:root "$SUDOERS_ARTISAN"
|
||||||
|
chmod 440 "$SUDOERS_ARTISAN"
|
||||||
|
|
||||||
|
if ! visudo -c -f "$SUDOERS_ARTISAN" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS_ARTISAN – entferne Datei."
|
||||||
|
rm -f "$SUDOERS_ARTISAN"
|
||||||
|
else
|
||||||
|
echo "[✓] Sudoers für Artisan-Kommandos angelegt: $SUDOERS_ARTISAN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "[✓] Update-Wrapper bereit: $WRAPPER"
|
||||||
|
log "[✓] Version wird unter $VERSION_FILE gespeichert"
|
||||||
|
|
||||||
|
# ─── Installer-Wrapper ────────────────────────────────────────────────────────
|
||||||
|
INSTALL_WRAPPER="/usr/local/sbin/mailwolt-install"
|
||||||
|
INSTALL_SUDOERS="/etc/sudoers.d/mailwolt-install"
|
||||||
|
INSTALL_STATE_DIR="/var/lib/mailwolt/install"
|
||||||
|
INSTALL_LOG="/var/log/mailwolt-install.log"
|
||||||
|
|
||||||
|
# State/Log vorbereiten
|
||||||
|
install -d -m 0755 "$INSTALL_STATE_DIR"
|
||||||
|
: > "$INSTALL_LOG" || true
|
||||||
|
chmod 0644 "$INSTALL_LOG"
|
||||||
|
|
||||||
|
# Installer-Wrapper aus scripts/install-wrapper.sh kopieren
|
||||||
|
INSTALL_SRC=""
|
||||||
|
for candidate in \
|
||||||
|
"$(dirname "$0")/install-wrapper.sh" \
|
||||||
|
/opt/mailwolt-installer/scripts/install-wrapper.sh \
|
||||||
|
/var/www/mailwolt/mailwolt-installer/scripts/install-wrapper.sh; do
|
||||||
|
[[ -f "$candidate" ]] && INSTALL_SRC="$candidate" && break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$INSTALL_SRC" ]]; then
|
||||||
|
cp "$INSTALL_SRC" "$INSTALL_WRAPPER"
|
||||||
|
chmod 0755 "$INSTALL_WRAPPER"
|
||||||
|
chown root:root "$INSTALL_WRAPPER"
|
||||||
|
echo "[✓] Installer-Wrapper angelegt: $INSTALL_WRAPPER"
|
||||||
|
else
|
||||||
|
echo "[!] install-wrapper.sh nicht gefunden – Installer-Wrapper wird übersprungen."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sudoers: www-data & mailwolt dürfen den Installer-Wrapper laufen lassen
|
||||||
|
cat > "$INSTALL_SUDOERS" <<'SUDOEOF'
|
||||||
|
Defaults!/usr/local/sbin/mailwolt-install !requiretty
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install *
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install *
|
||||||
|
SUDOEOF
|
||||||
|
|
||||||
|
chown root:root "$INSTALL_SUDOERS"
|
||||||
|
chmod 440 "$INSTALL_SUDOERS"
|
||||||
|
|
||||||
|
if ! visudo -c -f "$INSTALL_SUDOERS" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in $INSTALL_SUDOERS – entferne Datei."
|
||||||
|
rm -f "$INSTALL_SUDOERS"
|
||||||
|
else
|
||||||
|
echo "[✓] Sudoers für Installer angelegt: $INSTALL_SUDOERS"
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "systemd Units (Reverb / Scheduler / Queue / Mail) …"
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/${APP_USER}-ws.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=${APP_NAME} WebSocket Backend
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Environment=NODE_ENV=production WS_PORT=8080
|
||||||
|
User=${APP_USER}
|
||||||
|
Group=${APP_GROUP}
|
||||||
|
WorkingDirectory=${APP_DIR}
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
|
||||||
|
ExecStart=/usr/bin/php artisan reverb:start --host=127.0.0.1 --port=8080 --no-interaction
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=append:/var/log/${APP_USER}-ws.log
|
||||||
|
StandardError=append:/var/log/${APP_USER}-ws.log
|
||||||
|
KillSignal=SIGINT
|
||||||
|
TimeoutStopSec=15
|
||||||
|
UMask=0002
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/${APP_USER}-schedule.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=${APP_NAME} Laravel Scheduler
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${APP_USER}
|
||||||
|
Group=${APP_GROUP}
|
||||||
|
WorkingDirectory=${APP_DIR}
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
|
||||||
|
ExecStart=/usr/bin/php artisan schedule:work
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=append:/var/log/${APP_USER}-schedule.log
|
||||||
|
StandardError=append:/var/log/${APP_USER}-schedule.log
|
||||||
|
KillSignal=SIGINT
|
||||||
|
TimeoutStopSec=15
|
||||||
|
UMask=0002
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/${APP_USER}-queue.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=${APP_NAME} Queue Worker
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${APP_USER}
|
||||||
|
Group=${APP_GROUP}
|
||||||
|
WorkingDirectory=${APP_DIR}
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
|
||||||
|
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
|
||||||
|
ExecStart=/usr/bin/php artisan queue:work --queue=default,notify --tries=1
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=append:/var/log/${APP_USER}-queue.log
|
||||||
|
StandardError=append:/var/log/${APP_USER}-queue.log
|
||||||
|
KillSignal=SIGINT
|
||||||
|
TimeoutStopSec=15
|
||||||
|
UMask=0002
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown root:root /etc/systemd/system/${APP_USER}-*.service
|
||||||
|
chmod 644 /etc/systemd/system/${APP_USER}-*.service
|
||||||
|
touch /var/log/${APP_USER}-ws.log /var/log/${APP_USER}-schedule.log /var/log/${APP_USER}-queue.log
|
||||||
|
chown ${APP_USER}:${APP_GROUP} /var/log/${APP_USER}-*.log
|
||||||
|
chmod 664 /var/log/${APP_USER}-*.log
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# App-Dienste
|
||||||
|
if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan list --no-ansi | grep -qE '(^| )reverb:start( |$)'"; then
|
||||||
|
systemctl enable --now ${APP_USER}-ws
|
||||||
|
else
|
||||||
|
systemctl disable --now ${APP_USER}-ws >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
systemctl enable --now ${APP_USER}-schedule
|
||||||
|
systemctl enable --now ${APP_USER}-queue
|
||||||
|
|
||||||
|
# Mail-Dienste starten
|
||||||
|
systemctl enable --now rspamd opendkim postfix dovecot || true
|
||||||
|
|
||||||
|
# PHP-FPM: Unit erkennen, enable + (re)load
|
||||||
|
enable_and_touch_php_fpm() {
|
||||||
|
for u in php8.3-fpm php8.2-fpm php8.1-fpm php8.0-fpm php7.4-fpm php-fpm; do
|
||||||
|
if systemctl list-unit-files | grep -q "^${u}\.service"; then
|
||||||
|
systemctl enable --now "$u" || true
|
||||||
|
systemctl reload "$u" || systemctl restart "$u" || true
|
||||||
|
echo "[i] PHP-FPM unit: $u"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "[!] Keine passende php-fpm Unit gefunden."
|
||||||
|
}
|
||||||
|
enable_and_touch_php_fpm
|
||||||
|
|
||||||
|
# Falls in 80-app.sh DKIM installiert wurde: jetzt einmal reloaden
|
||||||
|
if [[ -e /run/mailwolt.need-opendkim-reload ]]; then
|
||||||
|
systemctl reload opendkim || true
|
||||||
|
rm -f /run/mailwolt.need-opendkim-reload || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Falls Zert-Fix markiert ist: Dovecot neu laden
|
||||||
|
if [[ -e /run/mailwolt.need-dovecot-reload ]]; then
|
||||||
|
systemctl reload dovecot || true
|
||||||
|
rm -f /run/mailwolt.need-dovecot-reload || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Falls DB-Migration schon durch: einmal reload
|
||||||
|
db_ready(){ mysql -u"${DB_USER}" -p"${DB_PASS}" -h 127.0.0.1 -D "${DB_NAME}" -e "SHOW TABLES LIKE 'migrations'\G" >/dev/null 2>&1; }
|
||||||
|
if db_ready; then
|
||||||
|
systemctl reload postfix || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mini-Portcheck (hilft beim Installer-Output)
|
||||||
|
echo "Listening (25/465/587):"
|
||||||
|
ss -ltnp | awk '$4 ~ /:(25|465|587)$/ {print " " $0}'
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Sudoers: npm-Build ohne Passwort für user 'mailwolt' …"
|
||||||
|
|
||||||
|
# 1) npm-Binary ermitteln (normal: /usr/bin/npm)
|
||||||
|
NPM_BIN="$(command -v npm || true)"
|
||||||
|
|
||||||
|
if [[ -z "$NPM_BIN" ]]; then
|
||||||
|
warn "npm wurde nicht gefunden – sudoers wird vorbereitet, aber ohne Validierung. Stelle sicher, dass Node/npm installiert ist."
|
||||||
|
# Fallback – die meisten Distros legen hier an
|
||||||
|
NPM_BIN="/usr/bin/npm"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUDOERS_FILE="/etc/sudoers.d/mailwolt-npm"
|
||||||
|
|
||||||
|
# 2) Sudoers-Datei schreiben
|
||||||
|
cat > "$SUDOERS_FILE" <<EOF
|
||||||
|
Defaults!${NPM_BIN} !requiretty
|
||||||
|
mailwolt ALL=(root) NOPASSWD: ${NPM_BIN}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown root:root "$SUDOERS_FILE"
|
||||||
|
chmod 440 "$SUDOERS_FILE"
|
||||||
|
|
||||||
|
# 3) Validieren
|
||||||
|
if visudo -c -f "$SUDOERS_FILE" >/dev/null 2>&1; then
|
||||||
|
log "[✓] sudoers OK: ${SUDOERS_FILE} erlaubt 'mailwolt' → ${NPM_BIN} ohne Passwort."
|
||||||
|
else
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_FILE} – entferne Datei."
|
||||||
|
rm -f "$SUDOERS_FILE"
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Backup/Restore – Tools, Config & Timer (installer.env) …"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 1) installer.env laden (ENV > installer.env > Defaults)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
if [[ -f /etc/mailwolt/installer.env ]]; then
|
||||||
|
# automatisch exportieren, damit ${VAR} später überall wirkt
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source /etc/mailwolt/installer.env
|
||||||
|
set +a
|
||||||
|
else
|
||||||
|
log "[i] /etc/mailwolt/installer.env nicht gefunden – nutze Defaults."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 2) Pfade & Defaults (werden durch ENV/installer.env überschrieben)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
CONF_DIR="/etc/mailwolt"
|
||||||
|
CONF_FILE="${CONF_DIR}/backup.conf"
|
||||||
|
BIN_DIR="/usr/local/sbin"
|
||||||
|
UNIT_DIR="/etc/systemd/system"
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/var/www/mailwolt}"
|
||||||
|
|
||||||
|
# DB-Parameter aus installer.env (bzw. ENV) oder Fallbacks
|
||||||
|
DB_HOST="${DB_HOST:-127.0.0.1}"
|
||||||
|
DB_NAME="${DB_NAME:-mailwolt}"
|
||||||
|
DB_USER="${DB_USER:-mailwolt}"
|
||||||
|
DB_PASS="${DB_PASS:-}"
|
||||||
|
|
||||||
|
# Backup-Settings aus installer.env (bzw. ENV)
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/var/backups/mailwolt}"
|
||||||
|
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"
|
||||||
|
BACKUP_USE_ZSTD="${BACKUP_USE_ZSTD:-1}"
|
||||||
|
BACKUP_ENABLED="${BACKUP_ENABLED:-0}" # 0|1
|
||||||
|
BACKUP_INTERVAL="${BACKUP_INTERVAL:-daily}" # daily|weekly|monthly
|
||||||
|
|
||||||
|
install -d -m 0755 "$CONF_DIR" "$BACKUP_DIR"
|
||||||
|
|
||||||
|
|
||||||
|
SUDOERS_BACKUP_FILE="/etc/sudoers.d/mailwolt-backup"
|
||||||
|
# 2) Sudoers-Datei schreiben
|
||||||
|
cat > "${SUDOERS_BACKUP_FILE} " <<EOF
|
||||||
|
Defaults!/usr/local/sbin/mailwolt-backup !requiretty
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-backup
|
||||||
|
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-backup
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chown root:root "${SUDOERS_BACKUP_FILE}"
|
||||||
|
chmod 440 "${SUDOERS_BACKUP_FILE}"
|
||||||
|
if ! visudo -c -f "${SUDOERS_BACKUP_FILE}" >/dev/null 2>&1; then
|
||||||
|
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_BACKUP_FILE} – entferne Datei."
|
||||||
|
rm -f "${SUDOERS_BACKUP_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 3) /etc/mailwolt/backup.conf (von UI/APP überschreibbar)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
cat > "$CONF_FILE" <<EOF
|
||||||
|
# MailWolt Backup Konfiguration (UI kann überschreiben)
|
||||||
|
APP_DIR="$APP_DIR"
|
||||||
|
BACKUP_DIR="$BACKUP_DIR"
|
||||||
|
RETENTION_DAYS="$BACKUP_RETENTION_DAYS"
|
||||||
|
USE_ZSTD="$BACKUP_USE_ZSTD"
|
||||||
|
|
||||||
|
# DB-Parameter
|
||||||
|
MYSQL_DB="$DB_NAME"
|
||||||
|
MYSQL_USER="$DB_USER"
|
||||||
|
MYSQL_PASS="$DB_PASS"
|
||||||
|
MYSQL_HOST="$DB_HOST"
|
||||||
|
MYSQL_PORT="3306"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 0644 "$CONF_FILE"
|
||||||
|
log "[✓] config geschrieben: $CONF_FILE"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 4) /usr/local/sbin/mailwolt-backup (schreibt backup.status)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
cat > "${BIN_DIR}/mailwolt-backup" <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log(){ echo "[$(date -Is)] $*"; }
|
||||||
|
|
||||||
|
# Konfiguration laden (ENV > Datei)
|
||||||
|
CONF="/etc/mailwolt/backup.conf"
|
||||||
|
[[ -f "$CONF" ]] && # shellcheck disable=SC1090
|
||||||
|
source "$CONF"
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/var/www/mailwolt}"
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/var/backups/mailwolt}"
|
||||||
|
RETENTION_DAYS="${RETENTION_DAYS:-7}"
|
||||||
|
USE_ZSTD="${USE_ZSTD:-1}"
|
||||||
|
|
||||||
|
MYSQL_DB="${MYSQL_DB:-mailwolt}"
|
||||||
|
MYSQL_USER="${MYSQL_USER:-mailwolt}"
|
||||||
|
MYSQL_PASS="${MYSQL_PASS:-}"
|
||||||
|
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
|
||||||
|
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||||
|
|
||||||
|
STATE_DIR="/var/lib/mailwolt"
|
||||||
|
STATUS_FILE="${STATE_DIR}/backup.status"
|
||||||
|
install -d -m 0755 "$STATE_DIR" "$BACKUP_DIR"
|
||||||
|
|
||||||
|
START_TS="$(date +%s)"
|
||||||
|
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
TMP="$(mktemp -d /tmp/mwbackup.XXXXXX)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
fail(){
|
||||||
|
local msg="${1:-backup failed}"
|
||||||
|
local now="$(date -Is)"
|
||||||
|
{
|
||||||
|
echo "time=${now}"
|
||||||
|
echo "size=0"
|
||||||
|
echo "dur=$(( $(date +%s) - START_TS ))s"
|
||||||
|
echo "ok=0"
|
||||||
|
echo "error=${msg}"
|
||||||
|
} > "$STATUS_FILE"
|
||||||
|
echo "[$now] ${msg}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
trap 'fail "unexpected error (exit $?)"' ERR
|
||||||
|
|
||||||
|
OUT="${BACKUP_DIR}/mailwolt-${TS}.tar"
|
||||||
|
log "⇒ starte Backup in $OUT …"
|
||||||
|
|
||||||
|
# 1) DB
|
||||||
|
log " • mysqldump …"
|
||||||
|
MYSQL_PWD="$MYSQL_PASS" mysqldump \
|
||||||
|
-h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" \
|
||||||
|
--single-transaction --routines --events --triggers \
|
||||||
|
"$MYSQL_DB" > "$TMP/mysql.sql"
|
||||||
|
|
||||||
|
# 2) Maildir
|
||||||
|
log " • Maildir …"
|
||||||
|
tar -C / -cf "$TMP/mail.tar" var/mail/vhosts 2>/dev/null || true
|
||||||
|
|
||||||
|
# 3) App (ohne heavy dirs)
|
||||||
|
log " • App …"
|
||||||
|
tar -C / -cf "$TMP/app.tar" \
|
||||||
|
--exclude='var/www/mailwolt/vendor' \
|
||||||
|
--exclude='var/www/mailwolt/node_modules' \
|
||||||
|
--exclude='var/www/mailwolt/public/build' \
|
||||||
|
var/www/mailwolt
|
||||||
|
|
||||||
|
# 4) Configs
|
||||||
|
log " • Configs …"
|
||||||
|
mkdir -p "$TMP/files"
|
||||||
|
cp -a /etc/mailwolt "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/postfix "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/dovecot "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/opendkim "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/opendmarc "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/rspamd "$TMP/files/" 2>/dev/null || true
|
||||||
|
cp -a /etc/ssl/ui "$TMP/files/" 2>/dev/null || true
|
||||||
|
tar -C "$TMP" -cf "$TMP/files.tar" files
|
||||||
|
|
||||||
|
# 5) Paket
|
||||||
|
log " • Archiviere …"
|
||||||
|
tar -C "$TMP" -cf "$OUT" mysql.sql mail.tar app.tar files.tar
|
||||||
|
|
||||||
|
# 6) Komprimieren (optional)
|
||||||
|
if [[ "${USE_ZSTD:-1}" = "1" ]] && command -v zstd >/dev/null 2>&1; then
|
||||||
|
log " • komprimiere (zstd) …"
|
||||||
|
zstd -f --rm -19 "$OUT"
|
||||||
|
OUT="${OUT}.zst"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 7) Retention
|
||||||
|
if [[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]]; then
|
||||||
|
log " • Retention: lösche älter als ${RETENTION_DAYS} Tage …"
|
||||||
|
find "$BACKUP_DIR" -type f -mtime +"$RETENTION_DAYS" -name 'mailwolt-*' -delete || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 8) Statusfile fürs UI
|
||||||
|
SIZE_BYTES="$(stat -c '%s' "$OUT" 2>/dev/null || echo 0)"
|
||||||
|
{
|
||||||
|
echo "time=$(date -Is)"
|
||||||
|
echo "size=${SIZE_BYTES}"
|
||||||
|
echo "dur=$(( $(date +%s) - START_TS ))s"
|
||||||
|
echo "ok=1"
|
||||||
|
echo "file=${OUT}"
|
||||||
|
} > "$STATUS_FILE"
|
||||||
|
chmod 0644 "$STATUS_FILE" 2>/dev/null || true
|
||||||
|
|
||||||
|
log "[✓] Backup fertig: $OUT"
|
||||||
|
EOSH
|
||||||
|
chmod 0755 "${BIN_DIR}/mailwolt-backup"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 5) /usr/local/sbin/mailwolt-restore
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
cat > "${BIN_DIR}/mailwolt-restore" <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
log(){ echo "[$(date -Is)] $*"; }
|
||||||
|
|
||||||
|
ARCHIVE="${1:-}"
|
||||||
|
[[ -n "$ARCHIVE" ]] || { echo "Usage: mailwolt-restore <backup.tar[.zst]>"; exit 1; }
|
||||||
|
[[ -f "$ARCHIVE" ]] || { echo "Backup nicht gefunden: $ARCHIVE"; exit 1; }
|
||||||
|
|
||||||
|
TMP="$(mktemp -d /tmp/mwrestore.XXXXXX)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
case "$ARCHIVE" in
|
||||||
|
*.zst) zstd -d -c "$ARCHIVE" > "$TMP/backup.tar" ;;
|
||||||
|
*) cp -a "$ARCHIVE" "$TMP/backup.tar" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log "⇒ entpacke …"
|
||||||
|
tar -C "$TMP" -xf "$TMP/backup.tar"
|
||||||
|
|
||||||
|
# Reihenfolge: DB → App → Mail → Config
|
||||||
|
if [[ -f "$TMP/mysql.sql" ]]; then
|
||||||
|
log " • MySQL wiederherstellen …"
|
||||||
|
mysql < "$TMP/mysql.sql"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$TMP/app.tar" ]]; then
|
||||||
|
log " • App → /var/www/mailwolt …"
|
||||||
|
tar -C / -xf "$TMP/app.tar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$TMP/mail.tar" ]]; then
|
||||||
|
log " • Maildir → /var/mail/vhosts …"
|
||||||
|
tar -C / -xf "$TMP/mail.tar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$TMP/files.tar" ]]; then
|
||||||
|
log " • Configs → /etc/* …"
|
||||||
|
tar -C / -xf "$TMP/files.tar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "[✓] Restore abgeschlossen."
|
||||||
|
EOSH
|
||||||
|
chmod 0755 "${BIN_DIR}/mailwolt-restore"
|
||||||
|
|
||||||
|
log "[✓] Tools installiert: ${BIN_DIR}/mailwolt-backup, mailwolt-restore"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 6) systemd Service + Timer (Timer default via installer.env)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
cat > "${UNIT_DIR}/mailwolt-backup.service" <<'EOSVC'
|
||||||
|
[Unit]
|
||||||
|
Description=MailWolt Backup
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/mailwolt-backup
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=best-effort
|
||||||
|
IOSchedulingPriority=7
|
||||||
|
EOSVC
|
||||||
|
|
||||||
|
cat > "${UNIT_DIR}/mailwolt-backup.timer" <<EOTIM
|
||||||
|
[Unit]
|
||||||
|
Description=MailWolt Backup Timer
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=${BACKUP_ONCALENDAR:-*-*-* 03:00:00}
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOTIM
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
if [[ "${BACKUP_ENABLED}" = "1" ]]; then
|
||||||
|
log "Aktiviere Backup-Timer (${BACKUP_ONCALENDAR}) …"
|
||||||
|
systemctl enable --now mailwolt-backup.timer
|
||||||
|
else
|
||||||
|
log "Timer bleibt deaktiviert (BACKUP_ENABLED=0)."
|
||||||
|
systemctl disable --now mailwolt-backup.timer >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "[✓] Backup-Setup abgeschlossen."
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "Monit konfigurieren …"
|
||||||
|
cat > /etc/monit/monitrc <<'EOF'
|
||||||
|
set daemon 60
|
||||||
|
set logfile syslog facility log_daemon
|
||||||
|
|
||||||
|
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
||||||
|
start program = "/bin/systemctl start postfix"
|
||||||
|
stop program = "/bin/systemctl stop postfix"
|
||||||
|
if failed port 25 protocol smtp then restart
|
||||||
|
if failed port 465 type tcp ssl then restart
|
||||||
|
if failed port 587 type tcp then restart
|
||||||
|
|
||||||
|
check process dovecot with pidfile /run/dovecot/master.pid
|
||||||
|
start program = "/bin/systemctl start dovecot"
|
||||||
|
stop program = "/bin/systemctl stop dovecot"
|
||||||
|
if failed port 143 type tcp then restart
|
||||||
|
if failed port 993 type tcp ssl then restart
|
||||||
|
|
||||||
|
check process mariadb with pidfile /run/mysqld/mysqld.pid
|
||||||
|
start program = "/bin/systemctl start mariadb"
|
||||||
|
stop program = "/bin/systemctl stop mariadb"
|
||||||
|
if failed port 3306 type tcp then restart
|
||||||
|
|
||||||
|
check process redis-server with pidfile /run/redis/redis-server.pid
|
||||||
|
start program = "/bin/systemctl start redis-server"
|
||||||
|
stop program = "/bin/systemctl stop redis-server"
|
||||||
|
if failed port 6379 type tcp then restart
|
||||||
|
|
||||||
|
check process nginx with pidfile /run/nginx.pid
|
||||||
|
start program = "/bin/systemctl start nginx"
|
||||||
|
stop program = "/bin/systemctl stop nginx"
|
||||||
|
if failed port 80 type tcp then restart
|
||||||
|
if failed port 443 type tcp ssl then restart
|
||||||
|
EOF
|
||||||
|
chmod 600 /etc/monit/monitrc
|
||||||
|
monit -t && systemctl enable --now monit
|
||||||
|
monit reload || true
|
||||||
|
|
||||||
|
log "[✓] Monit konfiguriert und gestartet"
|
||||||
|
|
||||||
|
# ── mailwolt-update ins System kopieren ─────────────────────────────
|
||||||
|
install -m 0750 -o root -g root scripts/update.sh /usr/local/sbin/mailwolt-update
|
||||||
|
log "[✓] mailwolt-update installiert → ausführbar via 'sudo mailwolt-update'"
|
||||||
|
|
@ -0,0 +1,491 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "WoltGuard (Monit + Self-Heal) einrichten …"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Env nur nachladen, wenn Flags nicht bereits exportiert sind
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
: "${CLAMAV_ENABLE:=}" ; : "${OPENDMARC_ENABLE:=}" ; : "${FAIL2BAN_ENABLE:=}"
|
||||||
|
if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$INSTALLER_ENV"
|
||||||
|
fi
|
||||||
|
CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
|
||||||
|
OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-0}"
|
||||||
|
FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Monit installieren & aktivieren
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
command -v monit >/dev/null || { apt-get update -qq; apt-get install -y monit; }
|
||||||
|
systemctl enable --now monit
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Helper-Skripte (laufen später eigenständig → Env selbst laden)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
install -d -m 0755 /usr/local/sbin
|
||||||
|
|
||||||
|
# Redis-Ping (nimmt REDIS_PASSWORD aus installer.env oder .env)
|
||||||
|
cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
REDIS_PASS="${REDIS_PASS:-}" # Legacy
|
||||||
|
|
||||||
|
# Installer-Env (falls vorhanden)
|
||||||
|
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
|
||||||
|
|
||||||
|
# Falls .env existiert: Werte ergänzen, die noch leer sind
|
||||||
|
if [[ -r "$APP_ENV" ]]; then
|
||||||
|
[[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 -E '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
[[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 -E '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
[[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 -E '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Legacy-Fallback: wenn PASSWORD leer, aber PASS gesetzt → übernehmen
|
||||||
|
[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
|
||||||
|
|
||||||
|
# Quotes strippen
|
||||||
|
strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/'; }
|
||||||
|
REDIS_HOST="$(strip "${REDIS_HOST:-}")"
|
||||||
|
REDIS_PORT="$(strip "${REDIS_PORT:-}")"
|
||||||
|
REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
|
||||||
|
|
||||||
|
# redis-cli muss vorhanden sein
|
||||||
|
command -v redis-cli >/dev/null 2>&1 || exit 1
|
||||||
|
|
||||||
|
BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
|
||||||
|
if [[ -n "$REDIS_PASSWORD" ]]; then
|
||||||
|
CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping)
|
||||||
|
else
|
||||||
|
CMD=("${BASE[@]}" ping)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erfolgreich nur bei exakt "PONG"
|
||||||
|
[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
|
||||||
|
EOSH
|
||||||
|
chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
||||||
|
|
||||||
|
# Rspamd-Heal (setzt Laufzeitverzeichnis, leert alte Socke, restarts rspamd)
|
||||||
|
cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
|
||||||
|
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
|
||||||
|
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
|
||||||
|
if [[ -z "${REDIS_PASSWORD}" && -r "$APP_ENV" ]]; then
|
||||||
|
REDIS_PASSWORD="$(grep -E '^REDIS_PASSWORD=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rspamd Runtime fixen
|
||||||
|
install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
||||||
|
[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
|
||||||
|
|
||||||
|
# Neustart
|
||||||
|
systemctl restart rspamd
|
||||||
|
|
||||||
|
# Mini-Healthcheck
|
||||||
|
sleep 2
|
||||||
|
ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
EOSH
|
||||||
|
chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# WoltGuard Wrapper + Unit
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
cat >/usr/local/bin/woltguard <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
case "${1:-status}" in
|
||||||
|
start) systemctl enable --now monit ;;
|
||||||
|
stop) systemctl stop monit ;;
|
||||||
|
status) monit summary || systemctl status monit || true ;;
|
||||||
|
heal) monit reload || true; sleep 1; monit restart all || true ;;
|
||||||
|
monitor) monit monitor all || true ;;
|
||||||
|
unmonitor) monit unmonitor all || true ;;
|
||||||
|
*) echo "Usage: woltguard {start|stop|status|heal|monitor|unmonitor}"; exit 2;;
|
||||||
|
esac
|
||||||
|
EOSH
|
||||||
|
chmod 0755 /usr/local/bin/woltguard
|
||||||
|
|
||||||
|
cat >/etc/systemd/system/woltguard.service <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=WoltGuard – Self-Healing Monitor for MailWolt
|
||||||
|
After=network.target
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/woltguard start
|
||||||
|
ExecStop=/usr/local/bin/woltguard stop
|
||||||
|
RemainAfterExit=yes
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now woltguard
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Monit Basis + includes
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
||||||
|
grep -q 'include /etc/monit/conf.d/*' /etc/monit/monitrc || echo 'include /etc/monit/conf.d/*' >>/etc/monit/monitrc
|
||||||
|
install -d -m 0755 /etc/monit/conf.d
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Monit Checks
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# 10 – Redis zuerst (abhängig für rspamd)
|
||||||
|
cat >/etc/monit/conf.d/10-redis.conf <<'EOF'
|
||||||
|
check process redis with pidfile /run/redis/redis-server.pid
|
||||||
|
start program = "/bin/systemctl start redis-server"
|
||||||
|
stop program = "/bin/systemctl stop redis-server"
|
||||||
|
if failed host 127.0.0.1 port 6379 for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
|
||||||
|
check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
||||||
|
if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 20 – Rspamd (hängt von Redis ab), robust über process-matching
|
||||||
|
cat >/etc/monit/conf.d/20-rspamd.conf <<'EOF'
|
||||||
|
check process rspamd matching "/usr/bin/rspamd"
|
||||||
|
start program = "/bin/systemctl start rspamd"
|
||||||
|
stop program = "/bin/systemctl stop rspamd"
|
||||||
|
depends on redis
|
||||||
|
if failed host 127.0.0.1 port 11333 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
if failed host 127.0.0.1 port 11334 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 30 – Maildienste
|
||||||
|
cat >/etc/monit/conf.d/30-postfix.conf <<'EOF'
|
||||||
|
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
||||||
|
start program = "/bin/systemctl start postfix"
|
||||||
|
stop program = "/bin/systemctl stop postfix"
|
||||||
|
if failed port 25 protocol smtp then restart
|
||||||
|
if failed port 465 type tcpssl then restart
|
||||||
|
if failed port 587 type tcp then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat >/etc/monit/conf.d/30-dovecot.conf <<'EOF'
|
||||||
|
check process dovecot with pidfile /run/dovecot/master.pid
|
||||||
|
start program = "/bin/systemctl start dovecot"
|
||||||
|
stop program = "/bin/systemctl stop dovecot"
|
||||||
|
if failed port 993 type tcpssl for 2 cycles then restart
|
||||||
|
if failed port 24 protocol lmtp for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 40 – Web/PHP
|
||||||
|
cat >/etc/monit/conf.d/40-nginx.conf <<'EOF'
|
||||||
|
check process nginx with pidfile /run/nginx.pid
|
||||||
|
start program = "/bin/systemctl start nginx"
|
||||||
|
stop program = "/bin/systemctl stop nginx"
|
||||||
|
if failed port 80 type tcp then restart
|
||||||
|
if failed port 443 type tcpssl then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 50 – DKIM/DMARC
|
||||||
|
cat >/etc/monit/conf.d/50-opendkim.conf <<'EOF'
|
||||||
|
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
||||||
|
start program = "/bin/systemctl start opendkim"
|
||||||
|
stop program = "/bin/systemctl stop opendkim"
|
||||||
|
if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# optional: OpenDMARC
|
||||||
|
if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
||||||
|
cat >/etc/monit/conf.d/55-opendmarc.conf <<'EOF'
|
||||||
|
check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
||||||
|
start program = "/bin/systemctl start opendmarc"
|
||||||
|
stop program = "/bin/systemctl stop opendmarc"
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
rm -f /etc/monit/conf.d/55-opendmarc.conf || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 60 – optional: ClamAV
|
||||||
|
if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
||||||
|
cat >/etc/monit/conf.d/60-clamav.conf <<'EOF'
|
||||||
|
check process clamd with pidfile /run/clamav/clamd.pid
|
||||||
|
start program = "/bin/systemctl start clamav-daemon"
|
||||||
|
stop program = "/bin/systemctl stop clamav-daemon"
|
||||||
|
if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then timeout
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
rm -f /etc/monit/conf.d/60-clamav.conf || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 70 – Fail2Ban (optional, standardmäßig aktiv)
|
||||||
|
if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
||||||
|
cat >/etc/monit/conf.d/70-fail2ban.conf <<'EOF'
|
||||||
|
check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
||||||
|
start program = "/bin/systemctl start fail2ban"
|
||||||
|
stop program = "/bin/systemctl stop fail2ban"
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
rm -f /etc/monit/conf.d/70-fail2ban.conf || true
|
||||||
|
fi
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Monit neu laden
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
monit -t
|
||||||
|
systemctl reload monit || systemctl restart monit
|
||||||
|
systemctl status monit --no-pager || true
|
||||||
|
log "[✓] WoltGuard aktiv."
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#source ./lib.sh
|
||||||
|
#
|
||||||
|
#log "WoltGuard (Monit + Self-Heal) einrichten …"
|
||||||
|
#
|
||||||
|
#set +u
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
#set -u
|
||||||
|
#CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
|
||||||
|
#OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-0}"
|
||||||
|
#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
#
|
||||||
|
## Pakete sicherstellen
|
||||||
|
#command -v monit >/dev/null || { apt-get update -qq; apt-get install -y monit; }
|
||||||
|
#systemctl enable --now monit
|
||||||
|
#
|
||||||
|
## Helper-Skripte
|
||||||
|
#install -d -m 0755 /usr/local/sbin
|
||||||
|
#cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#PASS=""
|
||||||
|
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env || true
|
||||||
|
#if command -v redis-cli >/dev/null 2>&1; then
|
||||||
|
# [[ -n "${REDIS_PASS:-}" ]] \
|
||||||
|
# && redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" ping | grep -q PONG \
|
||||||
|
# || redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG
|
||||||
|
#else
|
||||||
|
# exit 1
|
||||||
|
#fi
|
||||||
|
#EOSH
|
||||||
|
#chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
||||||
|
#
|
||||||
|
#cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
#REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
#REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
#
|
||||||
|
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
#APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
#REDIS_CLI="$(command -v redis-cli || true)"
|
||||||
|
#SYSTEMCTL="$(command -v systemctl || true)"
|
||||||
|
#RSPAMD_SERVICE="rspamd"
|
||||||
|
#
|
||||||
|
#if [ -r "$INSTALLER_ENV" ]; then . "$INSTALLER_ENV"; fi
|
||||||
|
#if [ -z "${REDIS_PASSWORD}" ] && [ -r "$APP_ENV" ]; then
|
||||||
|
# REDIS_PASSWORD="$(grep -E '^REDIS_PASSWORD=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#if [ -n "$REDIS_CLI" ]; then
|
||||||
|
# echo "[INFO] Prüfe Redis Verbindung..."
|
||||||
|
# if [ -n "${REDIS_PASSWORD}" ]; then
|
||||||
|
# if ! "$REDIS_CLI" -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_PASSWORD" ping | grep -q '^PONG$'; then
|
||||||
|
# echo "[WARN] Redis antwortet nicht oder Passwort falsch!"
|
||||||
|
# else
|
||||||
|
# echo "[OK] Redis antwortet (auth ok)."
|
||||||
|
# fi
|
||||||
|
# else
|
||||||
|
# if ! "$REDIS_CLI" -h "$REDIS_HOST" -p "$REDIS_PORT" ping | grep -q '^PONG$'; then
|
||||||
|
# echo "[WARN] Redis antwortet nicht (ohne Passwort)."
|
||||||
|
# else
|
||||||
|
# echo "[OK] Redis antwortet (kein Passwort)."
|
||||||
|
# fi
|
||||||
|
# fi
|
||||||
|
#else
|
||||||
|
# echo "[WARN] redis-cli nicht gefunden – überspringe Test."
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#echo "[INFO] Prüfe Rspamd Socket & Verzeichnis..."
|
||||||
|
#install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
||||||
|
#[ -S /var/lib/rspamd/rspamd.sock ] && rm -f /var/lib/rspamd/rspamd.sock || true
|
||||||
|
#
|
||||||
|
#echo "[INFO] Starte Rspamd neu..."
|
||||||
|
#if [ -n "$SYSTEMCTL" ]; then
|
||||||
|
# "$SYSTEMCTL" restart "$RSPAMD_SERVICE"
|
||||||
|
# echo "[OK] Rspamd erfolgreich neu gestartet."
|
||||||
|
#else
|
||||||
|
# echo "[ERROR] systemctl nicht gefunden – kein Neustart möglich."
|
||||||
|
# exit 1
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#echo "[INFO] Healthcheck (Port 11334)..."
|
||||||
|
#sleep 3
|
||||||
|
#if ss -tln | grep -q ':11334'; then
|
||||||
|
# echo "[OK] Rspamd Controller läuft auf Port 11334."
|
||||||
|
#else
|
||||||
|
# echo "[WARN] Rspamd Controller Port 11334 nicht erreichbar."
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#echo "[DONE] Mailwolt Rspamd-Heal abgeschlossen."
|
||||||
|
#exit 0
|
||||||
|
#EOSH
|
||||||
|
#chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
||||||
|
#
|
||||||
|
## WoltGuard Wrapper + Unit
|
||||||
|
#cat >/usr/local/bin/woltguard <<'EOSH'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#case "${1:-status}" in
|
||||||
|
# start) systemctl enable --now monit ;;
|
||||||
|
# stop) systemctl stop monit ;;
|
||||||
|
# status) monit summary || systemctl status monit || true ;;
|
||||||
|
# heal) monit reload || true; sleep 1; monit restart all || true ;;
|
||||||
|
# monitor) monit monitor all || true ;;
|
||||||
|
# unmonitor) monit unmonitor all || true ;;
|
||||||
|
# *) echo "Usage: woltguard {start|stop|status|heal|monitor|unmonitor}"; exit 2;;
|
||||||
|
#esac
|
||||||
|
#EOSH
|
||||||
|
#chmod 0755 /usr/local/bin/woltguard
|
||||||
|
#
|
||||||
|
#cat >/etc/systemd/system/woltguard.service <<'EOF'
|
||||||
|
#[Unit]
|
||||||
|
#Description=WoltGuard – Self-Healing Monitor for MailWolt
|
||||||
|
#After=network.target
|
||||||
|
#[Service]
|
||||||
|
#Type=oneshot
|
||||||
|
#ExecStart=/usr/local/bin/woltguard start
|
||||||
|
#ExecStop=/usr/local/bin/woltguard stop
|
||||||
|
#RemainAfterExit=yes
|
||||||
|
#[Install]
|
||||||
|
#WantedBy=multi-user.target
|
||||||
|
#EOF
|
||||||
|
#systemctl daemon-reload
|
||||||
|
#systemctl enable --now woltguard
|
||||||
|
#
|
||||||
|
## Monit Basis + include
|
||||||
|
#sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
||||||
|
#grep -q 'include /etc/monit/conf.d/*' /etc/monit/monitrc || echo 'include /etc/monit/conf.d/*' >>/etc/monit/monitrc
|
||||||
|
#install -d -m 0755 /etc/monit/conf.d
|
||||||
|
#
|
||||||
|
## Checks
|
||||||
|
#cat >/etc/monit/conf.d/postfix.conf <<'EOF'
|
||||||
|
#check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
||||||
|
# start program = "/bin/systemctl start postfix"
|
||||||
|
# stop program = "/bin/systemctl stop postfix"
|
||||||
|
# if failed port 25 protocol smtp then restart
|
||||||
|
# if failed port 465 type tcpssl then restart
|
||||||
|
# if failed port 587 type tcp then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#cat >/etc/monit/conf.d/dovecot.conf <<'EOF'
|
||||||
|
#check process dovecot with pidfile /run/dovecot/master.pid
|
||||||
|
# start program = "/bin/systemctl start dovecot"
|
||||||
|
# stop program = "/bin/systemctl stop dovecot"
|
||||||
|
# if failed port 993 type tcpssl for 2 cycles then restart
|
||||||
|
# if failed port 24 protocol lmtp for 2 cycles then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#cat >/etc/monit/conf.d/nginx.conf <<'EOF'
|
||||||
|
#check process nginx with pidfile /run/nginx.pid
|
||||||
|
# start program = "/bin/systemctl start nginx"
|
||||||
|
# stop program = "/bin/systemctl stop nginx"
|
||||||
|
# if failed port 80 type tcp then restart
|
||||||
|
# if failed port 443 type tcpssl then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#cat >/etc/monit/conf.d/redis.conf <<'EOF'
|
||||||
|
#check process redis with pidfile /run/redis/redis-server.pid
|
||||||
|
# start program = "/bin/systemctl start redis-server"
|
||||||
|
# stop program = "/bin/systemctl stop redis-server"
|
||||||
|
# if failed host 127.0.0.1 port 6379 for 2 cycles then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#
|
||||||
|
#check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
||||||
|
# if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#cat >/etc/monit/conf.d/rspamd.conf <<'EOF'
|
||||||
|
#check process rspamd with pidfile /run/rspamd/rspamd.pid
|
||||||
|
# start program = "/bin/systemctl start rspamd"
|
||||||
|
# stop program = "/bin/systemctl stop rspamd"
|
||||||
|
# if failed port 11333 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
# if failed port 11334 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#cat >/etc/monit/conf.d/opendkim.conf <<'EOF'
|
||||||
|
#check process opendkim with pidfile /run/opendkim/opendkim.pid
|
||||||
|
# start program = "/bin/systemctl start opendkim"
|
||||||
|
# stop program = "/bin/systemctl stop opendkim"
|
||||||
|
# if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## optional: OpenDMARC
|
||||||
|
#if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/conf.d/opendmarc.conf <<'EOF'
|
||||||
|
#check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
||||||
|
# start program = "/bin/systemctl start opendmarc"
|
||||||
|
# stop program = "/bin/systemctl stop opendmarc"
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/conf.d/opendmarc.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## optional: ClamAV
|
||||||
|
#if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/conf.d/clamav.conf <<'EOF'
|
||||||
|
#check process clamd with pidfile /run/clamav/clamd.pid
|
||||||
|
# start program = "/bin/systemctl start clamav-daemon"
|
||||||
|
# stop program = "/bin/systemctl stop clamav-daemon"
|
||||||
|
# if failed unixsocket /run/clamav/clamd.ctl then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/conf.d/clamav.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## optional: Fail2Ban
|
||||||
|
#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/conf.d/fail2ban.conf <<'EOF'
|
||||||
|
#check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
||||||
|
# start program = "/bin/systemctl start fail2ban"
|
||||||
|
# stop program = "/bin/systemctl stop fail2ban"
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/conf.d/fail2ban.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#monit -t
|
||||||
|
#systemctl reload monit || systemctl restart monit
|
||||||
|
#systemctl status monit --no-pager || true
|
||||||
|
#log "[✓] WoltGuard aktiv."
|
||||||
|
|
@ -0,0 +1,439 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Flags laden (falls vorhanden)
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
: "${CLAMAV_ENABLE:=}"; : "${OPENDMARC_ENABLE:=}"; : "${FAIL2BAN_ENABLE:=}"; : "${MONIT_HTTP:=}"
|
||||||
|
if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
|
||||||
|
. "$INSTALLER_ENV"
|
||||||
|
fi
|
||||||
|
CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
||||||
|
OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
||||||
|
FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
MONIT_HTTP="${MONIT_HTTP:-1}"
|
||||||
|
|
||||||
|
# ── Monit so konfigurieren, dass NUR monitrc.d/* geladen wird ────────────────
|
||||||
|
install -d -m 0755 /etc/monit/monitrc.d
|
||||||
|
install -d -m 0755 /etc/monit/conf.d # passiver Ablageort (NICHT includiert)
|
||||||
|
|
||||||
|
# Poll-Intervall (30s)
|
||||||
|
sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
||||||
|
# alle alten include-Zeilen raus und monitrc.d setzen
|
||||||
|
sed -i 's|^#\?\s*include .*$||g' /etc/monit/monitrc
|
||||||
|
grep -q '^include /etc/monit/monitrc.d/\*' /etc/monit/monitrc \
|
||||||
|
|| echo 'include /etc/monit/monitrc.d/*' >> /etc/monit/monitrc
|
||||||
|
|
||||||
|
# Optional: HTTP-UI nur einschalten, wenn explizit gewünscht
|
||||||
|
if [[ "$MONIT_HTTP" = "1" ]]; then
|
||||||
|
grep -q '^set httpd port 2812' /etc/monit/monitrc || cat >>/etc/monit/monitrc <<'HTTP'
|
||||||
|
set httpd port 2812 and
|
||||||
|
use address localhost
|
||||||
|
allow localhost
|
||||||
|
HTTP
|
||||||
|
fi
|
||||||
|
|
||||||
|
# KEIN Löschen mehr der Dateien – wir verschieben je nach Status
|
||||||
|
# (vorher stand hier rm -rf /etc/monit/monitrc.d/* und rm -f /etc/monit/conf.d/*.conf)
|
||||||
|
|
||||||
|
# ── Helper-Skripte ──────────────────────────────────────────────────────────
|
||||||
|
install -d -m 0755 /usr/local/sbin
|
||||||
|
|
||||||
|
# Redis-Ping (Password: REDIS_PASSWORD aus installer.env oder .env)
|
||||||
|
cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
REDIS_PASS="${REDIS_PASS:-}"
|
||||||
|
|
||||||
|
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
|
||||||
|
if [[ -r "$APP_ENV" ]]; then
|
||||||
|
[[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
[[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
[[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
fi
|
||||||
|
[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
|
||||||
|
|
||||||
|
strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'; }
|
||||||
|
REDIS_HOST="$(strip "${REDIS_HOST:-}")"
|
||||||
|
REDIS_PORT="$(strip "${REDIS_PORT:-}")"
|
||||||
|
REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
|
||||||
|
|
||||||
|
command -v redis-cli >/dev/null 2>&1 || exit 1
|
||||||
|
BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
|
||||||
|
[[ -n "$REDIS_PASSWORD" ]] && CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping) || CMD=("${BASE[@]}" ping)
|
||||||
|
[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
|
||||||
|
EOSH
|
||||||
|
chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
||||||
|
|
||||||
|
# Rspamd-Heal (Socke aufräumen, restart, Mini-Port-Check)
|
||||||
|
cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
|
||||||
|
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
REDIS_PASS="${REDIS_PASS:-}"
|
||||||
|
|
||||||
|
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
|
||||||
|
if [[ -z "${REDIS_PASS}" && -r "$APP_ENV" ]]; then
|
||||||
|
REDIS_PASS="$(grep -E '^REDIS_PASS=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rspamd Runtime fixen
|
||||||
|
install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
||||||
|
[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
|
||||||
|
|
||||||
|
echo "$(date '+%F %T') heal run" >> /var/log/rspamd-heal.log
|
||||||
|
|
||||||
|
# Neustart
|
||||||
|
systemctl restart rspamd
|
||||||
|
|
||||||
|
# Mini-Healthcheck
|
||||||
|
sleep 2
|
||||||
|
ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
EOSH
|
||||||
|
chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
||||||
|
|
||||||
|
# ── Monit-Checks (nummeriert) – fixe Dienste immer aktiv ────────────────────
|
||||||
|
# 10 – Redis
|
||||||
|
cat >/etc/monit/monitrc.d/10-redis.conf <<'EOF'
|
||||||
|
check process redis with pidfile /run/redis/redis-server.pid
|
||||||
|
start program = "/bin/systemctl start redis-server"
|
||||||
|
stop program = "/bin/systemctl stop redis-server"
|
||||||
|
if failed host 127.0.0.1 port 6379 for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
|
||||||
|
check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
||||||
|
if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 20 – Rspamd (robust via process-matching + Heal)
|
||||||
|
cat >/etc/monit/monitrc.d/20-rspamd.conf <<'EOF'
|
||||||
|
check process rspamd matching "rspamd: main process"
|
||||||
|
start program = "/bin/systemctl start rspamd" with timeout 120 seconds
|
||||||
|
stop program = "/bin/systemctl stop rspamd"
|
||||||
|
depends on redis
|
||||||
|
if failed host 127.0.0.1 port 11333 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
if failed host 127.0.0.1 port 11334 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
if does not exist for 2 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then unmonitor
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 30 – Postfix
|
||||||
|
cat >/etc/monit/monitrc.d/30-postfix.conf <<'EOF'
|
||||||
|
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
||||||
|
start program = "/bin/systemctl start postfix"
|
||||||
|
stop program = "/bin/systemctl stop postfix"
|
||||||
|
if failed host 127.0.0.1 port 25 type tcp with timeout 15 seconds for 3 cycles then restart
|
||||||
|
if failed host 127.0.0.1 port 465 type tcpssl with timeout 10 seconds then restart
|
||||||
|
if failed host 127.0.0.1 port 587 type tcp with timeout 10 seconds then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 30 – Dovecot (IMAPS; LMTP oft Unix-Socket → kein TCP-Fehlalarm)
|
||||||
|
cat >/etc/monit/monitrc.d/30-dovecot.conf <<'EOF'
|
||||||
|
check process dovecot with pidfile /run/dovecot/master.pid
|
||||||
|
start program = "/bin/systemctl start dovecot"
|
||||||
|
stop program = "/bin/systemctl stop dovecot"
|
||||||
|
if failed port 993 type tcpssl for 3 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 40 – Nginx
|
||||||
|
cat >/etc/monit/monitrc.d/40-nginx.conf <<'EOF'
|
||||||
|
check process nginx with pidfile /run/nginx.pid
|
||||||
|
start program = "/bin/systemctl start nginx"
|
||||||
|
stop program = "/bin/systemctl stop nginx"
|
||||||
|
if failed port 80 type tcp then restart
|
||||||
|
if failed port 443 type tcpssl then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 50 – OpenDKIM
|
||||||
|
cat >/etc/monit/monitrc.d/50-opendkim.conf <<'EOF'
|
||||||
|
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
||||||
|
start program = "/bin/systemctl start opendkim"
|
||||||
|
stop program = "/bin/systemctl stop opendkim"
|
||||||
|
if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
move_monit_conf() {
|
||||||
|
local name="$1" # z.B. 55-opendmarc
|
||||||
|
local enabled="$2" # "0" oder "1"
|
||||||
|
local src="/etc/monit/conf.d/${name}.conf"
|
||||||
|
local dst="/etc/monit/monitrc.d/${name}.conf"
|
||||||
|
|
||||||
|
mkdir -p /etc/monit/conf.d /etc/monit/monitrc.d
|
||||||
|
|
||||||
|
# Falls Datei nirgends existiert → in conf.d anlegen (lesbare Quelle)
|
||||||
|
if [[ ! -f "$src" && ! -f "$dst" ]]; then
|
||||||
|
cat >"$src" <<'EOF_PAYLOAD'
|
||||||
|
__PAYLOAD__
|
||||||
|
EOF_PAYLOAD
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$enabled" = "1" ]]; then
|
||||||
|
# Aktiv: in monitrc.d haben
|
||||||
|
if [[ -f "$src" && ! -f "$dst" ]]; then
|
||||||
|
mv -f "$src" "$dst"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Inaktiv: in conf.d haben
|
||||||
|
if [[ -f "$dst" && ! -f "$src" ]]; then
|
||||||
|
mv -f "$dst" "$src"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
move_monit_conf "55-opendmarc" "${OPENDMARC_ENABLE:-0}" <<'EOF'
|
||||||
|
check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
||||||
|
start program = "/bin/systemctl start opendmarc"
|
||||||
|
stop program = "/bin/systemctl stop opendmarc"
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
move_monit_conf "60-clamav" "${CLAMAV_ENABLE:-0}" <<'EOF'
|
||||||
|
check process clamd matching "clamd"
|
||||||
|
start program = "/bin/systemctl start clamav-daemon"
|
||||||
|
stop program = "/bin/systemctl stop clamav-daemon"
|
||||||
|
if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then unmonitor
|
||||||
|
EOF
|
||||||
|
|
||||||
|
move_monit_conf "70-fail2ban" "${FAIL2BAN_ENABLE:-0}" <<'EOF'
|
||||||
|
check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
||||||
|
start program = "/bin/systemctl start fail2ban"
|
||||||
|
stop program = "/bin/systemctl stop fail2ban"
|
||||||
|
if 5 restarts within 5 cycles then alert
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── Monit neu laden ─────────────────────────────────────────────────────────
|
||||||
|
monit -t
|
||||||
|
systemctl reload monit || systemctl restart monit
|
||||||
|
|
||||||
|
# Optionaler Sichttest (CLI funktioniert auch ohne HTTP-UI)
|
||||||
|
# sleep 2
|
||||||
|
# monit summary || true
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
## Flags laden (falls vorhanden)
|
||||||
|
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
#: "${CLAMAV_ENABLE:=}"; : "${OPENDMARC_ENABLE:=}"; : "${FAIL2BAN_ENABLE:=}"; : "${MONIT_HTTP:=}"
|
||||||
|
#if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
|
||||||
|
# . "$INSTALLER_ENV"
|
||||||
|
#fi
|
||||||
|
#CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
||||||
|
#OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
||||||
|
#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
#MONIT_HTTP="${MONIT_HTTP:-1}"
|
||||||
|
#
|
||||||
|
## ── Monit so konfigurieren, dass NUR monitrc.d/* geladen wird ────────────────
|
||||||
|
#install -d -m 0755 /etc/monit/monitrc.d
|
||||||
|
## Poll-Intervall (30s)
|
||||||
|
#sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
||||||
|
## alle alten include-Zeilen raus und monitrc.d setzen
|
||||||
|
#sed -i 's|^#\?\s*include .*$||g' /etc/monit/monitrc
|
||||||
|
#grep -q '^include /etc/monit/monitrc.d/\*' /etc/monit/monitrc \
|
||||||
|
# || echo 'include /etc/monit/monitrc.d/*' >> /etc/monit/monitrc
|
||||||
|
#
|
||||||
|
## Optional: HTTP-UI nur einschalten, wenn explizit gewünscht
|
||||||
|
#if [[ "$MONIT_HTTP" = "1" ]]; then
|
||||||
|
# grep -q '^set httpd port 2812' /etc/monit/monitrc || cat >>/etc/monit/monitrc <<'HTTP'
|
||||||
|
#set httpd port 2812 and
|
||||||
|
# use address localhost
|
||||||
|
# allow localhost
|
||||||
|
#HTTP
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#sudo mkdir -p /etc/monit/monitrc.d
|
||||||
|
#sudo rm -rf /etc/monit/monitrc.d/* 2>/dev/null || true
|
||||||
|
#sudo rm -f /etc/monit/conf.d/*.conf 2>/dev/null || true
|
||||||
|
#
|
||||||
|
## ── Helper-Skripte ──────────────────────────────────────────────────────────
|
||||||
|
#install -d -m 0755 /usr/local/sbin
|
||||||
|
#
|
||||||
|
## Redis-Ping (Password: REDIS_PASSWORD aus installer.env oder .env)
|
||||||
|
#cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
#APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
#REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
#REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
#REDIS_PASS="${REDIS_PASS:-}"
|
||||||
|
#
|
||||||
|
#[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
|
||||||
|
#if [[ -r "$APP_ENV" ]]; then
|
||||||
|
# [[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
# [[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
# [[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
|
||||||
|
#fi
|
||||||
|
#[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
|
||||||
|
#
|
||||||
|
#strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'; }
|
||||||
|
#REDIS_HOST="$(strip "${REDIS_HOST:-}")"
|
||||||
|
#REDIS_PORT="$(strip "${REDIS_PORT:-}")"
|
||||||
|
#REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
|
||||||
|
#
|
||||||
|
#command -v redis-cli >/dev/null 2>&1 || exit 1
|
||||||
|
#BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
|
||||||
|
#[[ -n "$REDIS_PASSWORD" ]] && CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping) || CMD=("${BASE[@]}" ping)
|
||||||
|
#[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
|
||||||
|
#EOSH
|
||||||
|
#chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
||||||
|
#
|
||||||
|
## Rspamd-Heal (Socke aufräumen, restart, Mini-Port-Check)
|
||||||
|
#cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
||||||
|
#APP_ENV="/var/www/mailwolt/.env"
|
||||||
|
#
|
||||||
|
#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
||||||
|
#REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
#REDIS_PASS="${REDIS_PASS:-}"
|
||||||
|
#
|
||||||
|
#[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
|
||||||
|
#if [[ -z "${REDIS_PASS}" && -r "$APP_ENV" ]]; then
|
||||||
|
# REDIS_PASS="$(grep -E '^REDIS_PASS=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## Rspamd Runtime fixen
|
||||||
|
#install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
||||||
|
#[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
|
||||||
|
#
|
||||||
|
#echo "$(date '+%F %T') heal run" >> /var/log/rspamd-heal.log
|
||||||
|
#
|
||||||
|
## Neustart
|
||||||
|
#systemctl restart rspamd
|
||||||
|
#
|
||||||
|
## Mini-Healthcheck
|
||||||
|
#sleep 2
|
||||||
|
#ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
|
||||||
|
#
|
||||||
|
#exit 0
|
||||||
|
#EOSH
|
||||||
|
#chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
||||||
|
#
|
||||||
|
## ── Monit-Checks (nummeriert) ───────────────────────────────────────────────
|
||||||
|
## 10 – Redis
|
||||||
|
#cat >/etc/monit/monitrc.d/10-redis.conf <<'EOF'
|
||||||
|
#check process redis with pidfile /run/redis/redis-server.pid
|
||||||
|
# start program = "/bin/systemctl start redis-server"
|
||||||
|
# stop program = "/bin/systemctl stop redis-server"
|
||||||
|
# if failed host 127.0.0.1 port 6379 for 2 cycles then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#
|
||||||
|
#check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
||||||
|
# if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 20 – Rspamd (robust via process-matching + Heal)
|
||||||
|
#cat >/etc/monit/monitrc.d/20-rspamd.conf <<'EOF'
|
||||||
|
#check process rspamd matching "rspamd: main process"
|
||||||
|
# start program = "/bin/systemctl start rspamd" with timeout 120 seconds
|
||||||
|
# stop program = "/bin/systemctl stop rspamd"
|
||||||
|
# depends on redis
|
||||||
|
# if failed host 127.0.0.1 port 11333 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
# if failed host 127.0.0.1 port 11334 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
||||||
|
# if does not exist for 2 cycles then restart
|
||||||
|
# if 5 restarts within 10 cycles then unmonitor
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 30 – Postfix
|
||||||
|
#cat >/etc/monit/monitrc.d/30-postfix.conf <<'EOF'
|
||||||
|
#check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
||||||
|
# start program = "/bin/systemctl start postfix"
|
||||||
|
# stop program = "/bin/systemctl stop postfix"
|
||||||
|
# if failed host 127.0.0.1 port 25 type tcp with timeout 15 seconds for 3 cycles then restart
|
||||||
|
# if failed host 127.0.0.1 port 465 type tcpssl with timeout 10 seconds then restart
|
||||||
|
# if failed host 127.0.0.1 port 587 type tcp with timeout 10 seconds then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 30 – Dovecot (IMAPS; LMTP oft Unix-Socket → kein TCP-Fehlalarm)
|
||||||
|
#cat >/etc/monit/monitrc.d/30-dovecot.conf <<'EOF'
|
||||||
|
#check process dovecot with pidfile /run/dovecot/master.pid
|
||||||
|
# start program = "/bin/systemctl start dovecot"
|
||||||
|
# stop program = "/bin/systemctl stop dovecot"
|
||||||
|
# if failed port 993 type tcpssl for 3 cycles then restart
|
||||||
|
# if 5 restarts within 10 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 40 – Nginx
|
||||||
|
#cat >/etc/monit/monitrc.d/40-nginx.conf <<'EOF'
|
||||||
|
#check process nginx with pidfile /run/nginx.pid
|
||||||
|
# start program = "/bin/systemctl start nginx"
|
||||||
|
# stop program = "/bin/systemctl stop nginx"
|
||||||
|
# if failed port 80 type tcp then restart
|
||||||
|
# if failed port 443 type tcpssl then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 50 – OpenDKIM
|
||||||
|
#cat >/etc/monit/monitrc.d/50-opendkim.conf <<'EOF'
|
||||||
|
#check process opendkim with pidfile /run/opendkim/opendkim.pid
|
||||||
|
# start program = "/bin/systemctl start opendkim"
|
||||||
|
# stop program = "/bin/systemctl stop opendkim"
|
||||||
|
# if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
## 55 – OpenDMARC (optional)
|
||||||
|
#if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/monitrc.d/55-opendmarc.conf <<'EOF'
|
||||||
|
#check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
||||||
|
# start program = "/bin/systemctl start opendmarc"
|
||||||
|
# stop program = "/bin/systemctl stop opendmarc"
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/monitrc.d/55-opendmarc.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## 60 – ClamAV (über Socket)
|
||||||
|
#if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/monitrc.d/60-clamav.conf <<'EOF'
|
||||||
|
#check process clamd matching "clamd"
|
||||||
|
# start program = "/bin/systemctl start clamav-daemon"
|
||||||
|
# stop program = "/bin/systemctl stop clamav-daemon"
|
||||||
|
# if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
|
||||||
|
# if 5 restarts within 10 cycles then unmonitor
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/monitrc.d/60-clamav.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## 70 – Fail2Ban (optional)
|
||||||
|
#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
||||||
|
# cat >/etc/monit/monitrc.d/70-fail2ban.conf <<'EOF'
|
||||||
|
#check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
||||||
|
# start program = "/bin/systemctl start fail2ban"
|
||||||
|
# stop program = "/bin/systemctl stop fail2ban"
|
||||||
|
# if 5 restarts within 5 cycles then alert
|
||||||
|
#EOF
|
||||||
|
#else
|
||||||
|
# rm -f /etc/monit/monitrc.d/70-fail2ban.conf || true
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
## ── Monit neu laden ─────────────────────────────────────────────────────────
|
||||||
|
#monit -t
|
||||||
|
#systemctl reload monit || systemctl restart monit
|
||||||
|
#
|
||||||
|
## Optionaler Sichttest (CLI funktioniert auch ohne HTTP-UI)
|
||||||
|
##sleep 2
|
||||||
|
##monit summary || true
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
log "MOTD installieren …"
|
||||||
|
install -d /usr/local/bin
|
||||||
|
|
||||||
|
cat >/usr/local/bin/mw-motd <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# MOTD – MailWolt
|
||||||
|
# bewusst KEIN "set -e" und KEIN pipefail; MOTD darf nie hart abbrechen
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# ---------- Farben ----------
|
||||||
|
NC="\033[0m"; WH="\033[1;37m"; CY="\033[1;36m"; GY="\033[0;90m"
|
||||||
|
GR="\033[1;32m"; YE="\033[1;33m"; RD="\033[1;31m"
|
||||||
|
|
||||||
|
# ---------- Breite / Zentrierung ----------
|
||||||
|
W=110
|
||||||
|
term_cols=$(tput cols 2>/dev/null || echo $W)
|
||||||
|
[ "$term_cols" -gt "$W" ] && pad=$(( (term_cols - W)/2 )) || pad=0
|
||||||
|
sp(){ [ "$1" -gt 0 ] && printf "%${1}s" " " || true; }
|
||||||
|
center() { local s="$1"; local n=$(( (W - ${#s})/2 )); sp $((pad+n)); printf "%s\n" "$s"; }
|
||||||
|
rule(){ sp "$pad"; printf "%0.s=" $(seq 1 "$W"); printf "\n"; }
|
||||||
|
title(){ sp "$pad"; local t="$1"; local lf=$(( (W - ${#t} - 2)/2 )); local rf=$(( W - ${#t} - 2 - lf )); \
|
||||||
|
printf "%s" "$(printf '─%.0s' $(seq 1 $lf))"; printf " %s " "$t"; printf "%s\n" "$(printf '─%.0s' $(seq 1 $rf))"; }
|
||||||
|
kv(){ sp "$pad"; printf "%-12s: %s\n" "$1" "$2"; }
|
||||||
|
|
||||||
|
# ---------- Installer-/App-Variablen ----------
|
||||||
|
UI_HOST=""; WEBMAIL_HOST=""; MAIL_HOSTNAME=""
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env || true
|
||||||
|
|
||||||
|
# ---------- Systemdaten ----------
|
||||||
|
now="$(date '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || echo '-')"
|
||||||
|
upt="$(uptime -p 2>/dev/null || echo '-')"
|
||||||
|
cores="$(nproc 2>/dev/null || echo 1)"
|
||||||
|
load_raw="$(awk '{printf "%s / %s / %s",$1,$2,$3}' /proc/loadavg 2>/dev/null || echo '0.00 / 0.00 / 0.00')"
|
||||||
|
load1="$(awk '{print $1}' /proc/loadavg 2>/dev/null || echo 0)"
|
||||||
|
|
||||||
|
# RAM/SWAP
|
||||||
|
mem_total="$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||||
|
mem_avail="$(awk '/MemAvailable/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||||
|
mem_used=$(( mem_total - mem_avail ))
|
||||||
|
swap_total="$(awk '/SwapTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||||
|
swap_free="$(awk '/SwapFree/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||||
|
swap_used=$(( swap_total - swap_free ))
|
||||||
|
|
||||||
|
pct(){ local u="$1" t="$2"; [ "$t" -gt 0 ] || { echo 0; return; }; awk -v u="$u" -v t="$t" 'BEGIN{printf "%d",(u*100)/t}' ; }
|
||||||
|
ram_pct=$(pct "$mem_used" "$mem_total")
|
||||||
|
swap_pct=$(pct "$swap_used" "$swap_total")
|
||||||
|
|
||||||
|
# Disks
|
||||||
|
df_line(){ df -hP "$1" 2>/dev/null | awk 'NR==2{printf "%s / %s (%s)",$3,$2,$5}'; }
|
||||||
|
df_pct(){ df -P "$1" 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5+0}'; }
|
||||||
|
disk_root="$(df_line /)"; pct_root="$(df_pct /)"
|
||||||
|
disk_var="$(df_line /var 2>/dev/null)"; [ -n "$disk_var" ] || disk_var="-"
|
||||||
|
pct_var="$(df_pct /var 2>/dev/null)"; [ -n "$pct_var" ] || pct_var=0
|
||||||
|
|
||||||
|
# IPs (int/ext)
|
||||||
|
ipv4_int="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i!~/:/){print $i;exit}}')"
|
||||||
|
ipv6_int="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i~/:/){print $i;exit}}')"
|
||||||
|
ipv4_ext="$(curl -4fsS --max-time 1 https://ifconfig.me 2>/dev/null || true)"
|
||||||
|
ipv6_ext="$(curl -6fsS --max-time 1 https://ifconfig.me 2>/dev/null || true)"
|
||||||
|
|
||||||
|
# ---------- Status-Farben ----------
|
||||||
|
mark(){ # value thresholdY thresholdR
|
||||||
|
local v="$1" y="$2" r="$3"
|
||||||
|
if [ "$v" -ge "$r" ]; then printf "${RD}[HIGH]${NC}"
|
||||||
|
elif [ "$v" -ge "$y" ]; then printf "${YE}[WARN]${NC}"
|
||||||
|
else printf "${GR}[OK]${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
# Load/CPU-Schwellen (pro Core)
|
||||||
|
load_pct=$(awk -v l="$load1" -v c="$cores" 'BEGIN{if(c<1)c=1; printf "%d", (l/c)*100}')
|
||||||
|
m_load="$(mark "$load_pct" 70 100)"
|
||||||
|
m_ram="$(mark "$ram_pct" 75 90)"
|
||||||
|
m_swap="$(mark "$swap_pct" 10 50)"
|
||||||
|
m_root="$(mark "$pct_root" 75 90)"
|
||||||
|
m_var="$(mark "$pct_var" 75 90)"
|
||||||
|
|
||||||
|
# ---------- Header ----------
|
||||||
|
rule
|
||||||
|
center ""
|
||||||
|
center ":::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::"
|
||||||
|
center ":+:+:+ :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: "
|
||||||
|
center ":+: +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ "
|
||||||
|
center "+#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+ "
|
||||||
|
center "+#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+ "
|
||||||
|
center "#+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+# "
|
||||||
|
center "### ### ### ### ########### ########## ### ### ######## ########## ### "
|
||||||
|
center ""
|
||||||
|
rule
|
||||||
|
|
||||||
|
# ---------- System ----------
|
||||||
|
kv "Date / Time" "${YE}${now}${NC}"
|
||||||
|
sp "$pad"; printf "%-12s: int %-40s ext %s\n" "IPv4" "${ipv4_int:--}" "${ipv4_ext:--}"
|
||||||
|
sp "$pad"; printf "%-12s: int %-40s ext %s\n" "IPv6" "${ipv6_int:--}" "${ipv6_ext:--}"
|
||||||
|
kv "Uptime" "$upt"
|
||||||
|
sp "$pad"; printf "%-12s: %s cores, load %s %b\n" "CPU" "$cores" "$load_raw" "$m_load"
|
||||||
|
sp "$pad"; printf "%-12s: %s MiB / %s MiB (%d%%) %b %-5s %s MiB / %s MiB (%d%%) %b\n" \
|
||||||
|
"RAM" "$mem_used" "$mem_total" "$ram_pct" "$m_ram" "SWAP:" "$swap_used" "$swap_total" "$swap_pct" "$m_swap"
|
||||||
|
sp "$pad"; printf "%-12s: / %s %b %-5s %s %b\n" \
|
||||||
|
"Disk" "$disk_root" "$m_root" "/var:" "$disk_var" "$m_var"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ---------- Domains ----------
|
||||||
|
title "Domains"
|
||||||
|
[ -n "${UI_HOST:-}" ] && kv "UI" "${UI_HOST}"
|
||||||
|
[ -n "${WEBMAIL_HOST:-}" ] && kv "Webmail" "${WEBMAIL_HOST}"
|
||||||
|
[ -n "${MAIL_HOSTNAME:-}" ]&& kv "MX" "${MAIL_HOSTNAME}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ---------- Services (4 Spalten, bündig) ----------
|
||||||
|
title "Services"
|
||||||
|
svc_state(){ systemctl is-active --quiet "$1" && printf "${GR}[OK]${NC}" || printf "${RD}[FAIL]${NC}"; }
|
||||||
|
SVC=( nginx mariadb redis-server postfix dovecot rspamd opendkim opendmarc clamav-daemon fail2ban mailwolt-ws mailwolt-queue mailwolt-schedule )
|
||||||
|
|
||||||
|
i=0; line=""
|
||||||
|
for s in "${SVC[@]}"; do
|
||||||
|
st="$(svc_state "$s")"
|
||||||
|
seg="$(printf "%-18s %-7s" "$s" "$st")"
|
||||||
|
line="$line$seg"
|
||||||
|
i=$((i+1))
|
||||||
|
if [ $((i%4)) -eq 0 ]; then sp "$pad"; echo "$line"; line=""; else line="$line "; fi
|
||||||
|
done
|
||||||
|
[ -n "$line" ] && { sp "$pad"; echo "$line"; }
|
||||||
|
echo
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
|
||||||
|
chmod 755 /usr/local/bin/mw-motd
|
||||||
|
|
||||||
|
# update-motd Hook
|
||||||
|
if [[ -d /etc/update-motd.d ]]; then
|
||||||
|
cat >/etc/update-motd.d/10-mailwolt <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
/usr/local/bin/mw-motd
|
||||||
|
SH
|
||||||
|
chmod +x /etc/update-motd.d/10-mailwolt
|
||||||
|
[[ -f /etc/update-motd.d/50-motd-news ]] && chmod -x /etc/update-motd.d/50-motd-news || true
|
||||||
|
else
|
||||||
|
# Fallback für Systeme ohne dynamic MOTD
|
||||||
|
cat >/etc/profile.d/10-mailwolt-motd.sh <<'SH'
|
||||||
|
case "$-" in *i*) /usr/local/bin/mw-motd ;; esac
|
||||||
|
SH
|
||||||
|
fi
|
||||||
|
|
||||||
|
: > /etc/motd 2>/dev/null || true
|
||||||
|
log "[✓] MOTD installiert."
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source ./lib.sh
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# MailWolt – Abschluss / Summary (Dienste, Zertifikate, Smoke-Test)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Farben & Deko
|
||||||
|
NC="\033[0m"; BOLD="\033[1m"; DIM="\033[2m"
|
||||||
|
GREEN="\033[1;32m"; RED="\033[1;31m"; YELLOW="\033[1;33m"; CYAN="\033[1;36m"; GREY="\033[0;90m"
|
||||||
|
OKS="${GREEN}OK${NC}"; FAILS="${RED}FAIL${NC}"
|
||||||
|
bar(){ printf "${CYAN}%s${NC}\n" "──────────────────────────────────────────────────────────────────────────────"; }
|
||||||
|
ok(){ printf " [${OKS}]\n"; }
|
||||||
|
fail(){ printf " [${FAILS}]\n"; }
|
||||||
|
|
||||||
|
# Installer-Variablen laden (falls vorhanden)
|
||||||
|
set +u
|
||||||
|
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# Defaults / Umgebung
|
||||||
|
APP_USER="${APP_USER:-mailwolt}"
|
||||||
|
APP_GROUP="${APP_GROUP:-www-data}"
|
||||||
|
APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
|
||||||
|
|
||||||
|
BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
||||||
|
UI_HOST="${UI_HOST:-}"
|
||||||
|
WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
||||||
|
MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
||||||
|
|
||||||
|
APP_ENV="${APP_ENV:-production}"
|
||||||
|
PROXY_MODE="${PROXY_MODE:-}" # leer = nicht anzeigen; "1"=Proxy, "dev"=Dev, sonst "nein"
|
||||||
|
NPM_IP="${NPM_IP:-}"
|
||||||
|
|
||||||
|
LE_EMAIL="${LE_EMAIL:-admin@${BASE_DOMAIN}}"
|
||||||
|
ACME_WEBROOT="/var/www/letsencrypt"
|
||||||
|
|
||||||
|
# Zert-Pfade (werden via Hook nach /etc/ssl/* verlinkt)
|
||||||
|
UI_CERT="/etc/ssl/ui/fullchain.pem"
|
||||||
|
UI_KEY="/etc/ssl/ui/privkey.pem"
|
||||||
|
WEBMAIL_CERT="/etc/ssl/webmail/fullchain.pem"
|
||||||
|
MAIL_CERT="/etc/ssl/mail/fullchain.pem"
|
||||||
|
|
||||||
|
# IPs (aus lib.sh)
|
||||||
|
SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-$(detect_ip)}"
|
||||||
|
SERVER_PUBLIC_IPV6="${SERVER_PUBLIC_IPV6:-$(detect_ipv6)}"
|
||||||
|
|
||||||
|
# URLs (https nur, wenn UI-Cert+Key vorhanden)
|
||||||
|
SCHEME="http"
|
||||||
|
[[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
|
||||||
|
APP_URL="${SCHEME}://${UI_HOST:-$SERVER_PUBLIC_IPV4}"
|
||||||
|
WEBMAIL_URL="${SCHEME}://${WEBMAIL_HOST:-$SERVER_PUBLIC_IPV4}"
|
||||||
|
|
||||||
|
# Ziel eines Symlinks auflösen
|
||||||
|
real_target(){ readlink -f -- "$1" 2>/dev/null || true; }
|
||||||
|
|
||||||
|
# "LE" werten, wenn live/* ODER archive/* (auch fullchainN.pem) getroffen wird
|
||||||
|
is_le_path(){
|
||||||
|
local p="$1"
|
||||||
|
[[ "$p" == /etc/letsencrypt/live/*/fullchain.pem || "$p" == /etc/letsencrypt/archive/*/fullchain*.pem ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
UI_CERT_TARGET="$(real_target "$UI_CERT")"
|
||||||
|
WEBMAIL_CERT_TARGET="$(real_target "$WEBMAIL_CERT")"
|
||||||
|
MAIL_CERT_TARGET="$(real_target "$MAIL_CERT")"
|
||||||
|
|
||||||
|
is_le_path() {
|
||||||
|
case "$1" in
|
||||||
|
/etc/letsencrypt/live/*) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# robust gegen set -u: immer ${var:-}
|
||||||
|
UI_LE="self-signed/none"
|
||||||
|
if [ -s "${UI_CERT:-}" ] && [ -n "${UI_CERT_TARGET:-}" ] && is_le_path "${UI_CERT_TARGET:-}"; then
|
||||||
|
UI_LE="LE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
WEBMAIL_LE="self-signed/none"
|
||||||
|
if [ -s "${WEBMAIL_CERT:-}" ] && [ -n "${WEBMAIL_CERT_TARGET:-}" ] && is_le_path "${WEBMAIL_CERT_TARGET:-}"; then
|
||||||
|
WEBMAIL_LE="LE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
MAIL_LE="self-signed/none"
|
||||||
|
if [ -s "${MAIL_CERT:-}" ] && [ -n "${MAIL_CERT_TARGET:-}" ] && is_le_path "${MAIL_CERT_TARGET:-}"; then
|
||||||
|
MAIL_LE="LE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
bar
|
||||||
|
printf " %s\n" "✔ MailWolt Bootstrap fertig"
|
||||||
|
bar
|
||||||
|
|
||||||
|
# Kopf-Infos
|
||||||
|
printf " %-14s %s\n" "Aufruf UI:" "${APP_URL}"
|
||||||
|
printf " %-14s %s\n" "Webmail:" "${WEBMAIL_URL}"
|
||||||
|
printf " %-14s %s\n" "App Root:" "${APP_DIR}"
|
||||||
|
printf " %-14s %s\n" "Mail-FQDN:" "${MAIL_HOSTNAME:-$SERVER_PUBLIC_IPV4}"
|
||||||
|
printf " %-14s %s\n" "BASE_DOMAIN:" "${BASE_DOMAIN}"
|
||||||
|
printf " %-14s %s\n" "LE-Email:" "${LE_EMAIL}"
|
||||||
|
printf " %-14s %s\n" "APP_ENV:" "${APP_ENV}"
|
||||||
|
# Proxy-Block nur anzeigen, wenn Variable gesetzt ist
|
||||||
|
if [[ -n "$PROXY_MODE" ]]; then
|
||||||
|
if [[ "$PROXY_MODE" == "1" ]]; then
|
||||||
|
printf " %-14s %s\n" "Proxy-Mode:" "ja (NPM: ${NPM_IP:-unbekannt})"
|
||||||
|
elif [[ "$PROXY_MODE" == "dev" ]]; then
|
||||||
|
printf " %-14s %s\n" "Proxy-Mode:" "Entwicklungsmodus"
|
||||||
|
else
|
||||||
|
printf " %-14s %s\n" "Proxy-Mode:" "nein"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
printf " %-14s %s\n" "Server IPv6:" "${SERVER_PUBLIC_IPV6:-–}"
|
||||||
|
printf " %-14s %s\n" "ACME Webroot:" "${ACME_WEBROOT}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
printf " %-14s UI=%s, Webmail=%s, MX=%s\n" "Zertifikate:" "$UI_LE" "$WEBMAIL_LE" "$MAIL_LE"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo " Anmeldung: Keine vordefinierten Admin-Daten."
|
||||||
|
echo " Bitte zuerst registrieren (Erst-User wird Admin, danach"
|
||||||
|
echo " wird die Registrierung automatisch gesperrt)."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Dienste ────────────────────────────────────────────────────────────────
|
||||||
|
bar
|
||||||
|
echo " Services"
|
||||||
|
bar
|
||||||
|
|
||||||
|
OK_LIST=()
|
||||||
|
FAIL_LIST=()
|
||||||
|
|
||||||
|
svc(){
|
||||||
|
local unit="$1" label="${2:-$1}"
|
||||||
|
printf " • %-18s … " "$label"
|
||||||
|
if systemctl is-active --quiet "$unit"; then
|
||||||
|
ok
|
||||||
|
OK_LIST+=("$label")
|
||||||
|
else
|
||||||
|
fail
|
||||||
|
FAIL_LIST+=("$label")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kern-Services
|
||||||
|
svc nginx
|
||||||
|
svc mariadb
|
||||||
|
svc redis-server
|
||||||
|
svc postfix
|
||||||
|
svc dovecot
|
||||||
|
# App-Worker (tolerant)
|
||||||
|
svc "${APP_USER}-ws" "mailwolt-ws" || true
|
||||||
|
svc "${APP_USER}-schedule" "mailwolt-schedule" || true
|
||||||
|
svc "${APP_USER}-queue" "mailwolt-queue" || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
if ((${#OK_LIST[@]})); then
|
||||||
|
printf " ${GREEN}OK:${NC} %s\n" "$(IFS=', '; echo "${OK_LIST[*]}")"
|
||||||
|
fi
|
||||||
|
if ((${#FAIL_LIST[@]})); then
|
||||||
|
printf " ${RED}FAIL:${NC} %s\n" "$(IFS=', '; echo "${FAIL_LIST[*]}")"
|
||||||
|
echo " ${YELLOW}Hinweis:${NC} Details mit: journalctl -u <dienst> -b --no-pager"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Smoke-Test ─────────────────────────────────────────────────────────────
|
||||||
|
bar
|
||||||
|
echo " Smoke-Test (SMTP/IMAP/POP3 mit/ohne TLS)"
|
||||||
|
bar
|
||||||
|
|
||||||
|
check_port(){
|
||||||
|
local tag="$1" cmd="$2" desc="$3"
|
||||||
|
printf " [%-3s] %-35s … " "$tag" "$desc"
|
||||||
|
if timeout 8s bash -lc "$cmd" >/dev/null 2>&1; then ok; else fail; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# kleines Delay nach Erststart
|
||||||
|
sleep 6 || true
|
||||||
|
|
||||||
|
# SMTP
|
||||||
|
check_port "25" 'printf "EHLO x\r\nQUIT\r\n" | nc -w 3 127.0.0.1 25' \
|
||||||
|
"SMTP (EHLO)"
|
||||||
|
check_port "465" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:465 -quiet -ign_eof' \
|
||||||
|
"SMTPS (TLS + EHLO)"
|
||||||
|
check_port "587" 'printf "EHLO x\r\nSTARTTLS\r\nQUIT\r\n" | openssl s_client -starttls smtp -connect 127.0.0.1:587 -quiet -ign_eof' \
|
||||||
|
"Submission (STARTTLS)"
|
||||||
|
|
||||||
|
# POP/IMAP
|
||||||
|
check_port "110" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 110' \
|
||||||
|
"POP3 (QUIT)"
|
||||||
|
check_port "995" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:995 -quiet -ign_eof' \
|
||||||
|
"POP3S (TLS + QUIT)"
|
||||||
|
check_port "143" 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | nc -w 3 127.0.0.1 143' \
|
||||||
|
"IMAP (CAPABILITY/LOGOUT)"
|
||||||
|
check_port "993" 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | openssl s_client -connect 127.0.0.1:993 -quiet -ign_eof' \
|
||||||
|
"IMAPS (TLS + CAPABILITY/LOGOUT)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Hinweise nur ausgeben, wenn wirklich kein LE für UI/Webmail
|
||||||
|
if [[ "$UI_LE" != "LE" || "$WEBMAIL_LE" != "LE" ]]; then
|
||||||
|
echo -e " ${YELLOW}Hinweis:${NC} UI/Webmail verwenden noch kein Let's-Encrypt-Zertifikat."
|
||||||
|
echo -e " Prüfe Symlinks unter /etc/ssl/{ui,webmail} und den LE-Hook (21/75-Skripte)."
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Proxy-Info (optional)
|
||||||
|
if [[ "$PROXY_MODE" == "1" ]]; then
|
||||||
|
echo -e " ${GREY}Proxy-Hinweis:${NC} App erwartet TLS am Proxy (Backend ohne https-Redirects)."
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,633 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Farbschema für whiptail (libnewt) – hohe Lesbarkeit (dunkler Input, schwarze Schrift) ---
|
||||||
|
export NEWT_COLORS='
|
||||||
|
root=,blue
|
||||||
|
border=black,lightgray
|
||||||
|
window=black,lightgray
|
||||||
|
textbox=black,lightgray
|
||||||
|
label=black,lightgray
|
||||||
|
entry=black,cyan
|
||||||
|
button=black,cyan
|
||||||
|
actlistbox=black,cyan
|
||||||
|
actsellistbox=black,cyan
|
||||||
|
'
|
||||||
|
|
||||||
|
# optionales Backtitle (erscheint oben)
|
||||||
|
export DIALOGOPTS="--backtitle MailWolt Setup"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# MailWolt – Interaktiver Bootstrap (whiptail + Fallback)
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
DEV_MODE=0
|
||||||
|
PROXY_MODE=0
|
||||||
|
NPM_IP=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-dev) DEV_MODE=1 ;;
|
||||||
|
-proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
|
||||||
|
APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
|
||||||
|
export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
|
||||||
|
|
||||||
|
DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
export DB_PASS REDIS_PASS
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
source ./lib.sh
|
||||||
|
require_root
|
||||||
|
header
|
||||||
|
|
||||||
|
# ── Defaults ──────────────────────────────────────────────────
|
||||||
|
APP_NAME="${APP_NAME:-MailWolt}"
|
||||||
|
APP_USER="${APP_USER:-mailwolt}"
|
||||||
|
APP_GROUP="${APP_GROUP:-www-data}"
|
||||||
|
APP_USER_PREFIX="${APP_USER_PREFIX:-mw}"
|
||||||
|
APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
|
||||||
|
|
||||||
|
BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
||||||
|
UI_SUB="${UI_SUB:-ui}"
|
||||||
|
WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}"
|
||||||
|
MTA_SUB="${MTA_SUB:-mx}"
|
||||||
|
|
||||||
|
DB_NAME="${DB_NAME:-${APP_USER}}"
|
||||||
|
DB_USER="${DB_USER:-${APP_USER}}"
|
||||||
|
|
||||||
|
SERVER_PUBLIC_IPV4="$(detect_ip)"
|
||||||
|
SERVER_PUBLIC_IPV6="$(detect_ipv6)"
|
||||||
|
DEFAULT_TZ="$(detect_timezone)"
|
||||||
|
DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")"
|
||||||
|
|
||||||
|
echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}"
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────
|
||||||
|
have_whiptail(){ command -v whiptail >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
#valid_fqdn(){
|
||||||
|
# [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+[a-z]{2,}$ ]]
|
||||||
|
#}
|
||||||
|
|
||||||
|
# ── Host-Validierung & DEV-Erkennung ────────────────────────────────────────
|
||||||
|
valid_fqdn_prod(){ [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+[a-z]{2,}$ ]]; }
|
||||||
|
valid_host_dev(){
|
||||||
|
# erlaubt: single-label (ui, webmail), FQDNs, IPv4
|
||||||
|
[[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9-]+)*$ ]] || [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
|
||||||
|
}
|
||||||
|
is_local_like(){
|
||||||
|
local h="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
[[ "$h" =~ \.local$ || "$h" =~ \.loc$ || "$h" =~ \.dev$ || "$h" =~ \.test$ || "$h" = "localhost" ]] && return 0
|
||||||
|
[[ "$h" =~ ^10\. || "$h" =~ ^192\.168\. || "$h" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. || "$h" =~ ^127\. ]] && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
normalize_host(){
|
||||||
|
# $1=input $2=default (nutzt DEV_MODE für die passende Prüflogik)
|
||||||
|
local inp="$1" def="$2"
|
||||||
|
if [[ "${DEV_MODE}" = "1" ]]; then
|
||||||
|
valid_host_dev "$inp" && { echo "$inp"; return; }
|
||||||
|
else
|
||||||
|
valid_fqdn_prod "$inp" && { echo "$inp"; return; }
|
||||||
|
fi
|
||||||
|
echo "$def"
|
||||||
|
}
|
||||||
|
|
||||||
|
ask_tty_domain(){
|
||||||
|
local label="$1" example="$2" def="$3" outvar="$4" inp
|
||||||
|
echo -e "${CYAN}${label}${NC}"
|
||||||
|
echo -e " z.B. ${YELLOW}${example}${NC}"
|
||||||
|
echo -e " Default: ${GREY}${def}${NC}"
|
||||||
|
read -r -p " Eingabe (Enter=Default): " inp || true
|
||||||
|
inp="${inp:-$def}"
|
||||||
|
if ! valid_fqdn "$inp"; then
|
||||||
|
echo -e "${YELLOW}[!] Ungültiger FQDN, nehme Default: ${def}${NC}"
|
||||||
|
inp="$def"
|
||||||
|
fi
|
||||||
|
eval "$outvar='$inp'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Interaktive Eingaben (whiptail oder Fallback) ─────────────
|
||||||
|
MTA_DEFAULT="${MTA_SUB}.${BASE_DOMAIN}"
|
||||||
|
UI_DEFAULT="${UI_SUB}.${BASE_DOMAIN}"
|
||||||
|
WEBMAIL_DEFAULT="${WEBMAIL_SUB}.${BASE_DOMAIN}"
|
||||||
|
|
||||||
|
CLAMAV_ENABLE=1
|
||||||
|
OPENDMARC_ENABLE=1
|
||||||
|
FAIL2BAN_ENABLE=1
|
||||||
|
|
||||||
|
if command -v whiptail >/dev/null 2>&1; then
|
||||||
|
TITLE="MailWolt Setup"
|
||||||
|
|
||||||
|
# Hinweise zu erlaubten DEV-Hosts
|
||||||
|
MSG_SUFFIX="\n\nHinweis: Im DEV-Modus sind auch single-label Hosts (z.B. ui, webmail), *.local/*.dev und IPs erlaubt."
|
||||||
|
|
||||||
|
_mta_in="$(whiptail --title "$TITLE" --inputbox "Mailserver-Host (MX)\nBeispiele: mx.domain.tld | mx.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$MTA_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
_ui_in="$(whiptail --title "$TITLE" --inputbox "UI / Admin-Panel Host\nBeispiele: ui.domain.tld | ui.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$UI_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
_wm_in="$(whiptail --title "$TITLE" --inputbox "Webmail Host\nBeispiele: webmail.domain.tld | web.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$WEBMAIL_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
|
||||||
|
# ZUERST provisorisch prüfen, ob „lokal“ → DEV erzwingen
|
||||||
|
if is_local_like "$_mta_in" || is_local_like "$_ui_in" || is_local_like "$_wm_in"; then
|
||||||
|
DEV_MODE=1; APP_ENV="local"; APP_DEBUG="true"
|
||||||
|
fi
|
||||||
|
export DEV_MODE APP_ENV APP_DEBUG
|
||||||
|
|
||||||
|
# Jetzt mit passender Logik normalisieren
|
||||||
|
MTA_FQDN="$(normalize_host "$_mta_in" "$MTA_DEFAULT")"
|
||||||
|
UI_FQDN="$(normalize_host "$_ui_in" "$UI_DEFAULT")"
|
||||||
|
WEBMAIL_FQDN="$(normalize_host "$_wm_in" "$WEBMAIL_DEFAULT")"
|
||||||
|
|
||||||
|
CHOICES="$(whiptail --title "$TITLE" --checklist "Optionale Dienste aktivieren" 15 70 6 \
|
||||||
|
"ClamAV" "Virenscan (clamd/clamav-daemon)" ON \
|
||||||
|
"OpenDMARC" "DMARC-Auswertung" ON \
|
||||||
|
"Fail2Ban" "Brute-Force-Schutz" ON \
|
||||||
|
3>&1 1>&2 2>&3)" || true
|
||||||
|
CLAMAV_ENABLE=0; [[ "$CHOICES" == *"ClamAV"* ]] && CLAMAV_ENABLE=1
|
||||||
|
OPENDMARC_ENABLE=0; [[ "$CHOICES" == *"OpenDMARC"* ]] && OPENDMARC_ENABLE=1
|
||||||
|
FAIL2BAN_ENABLE=0; [[ "$CHOICES" == *"Fail2Ban"* ]] && FAIL2BAN_ENABLE=1
|
||||||
|
|
||||||
|
else
|
||||||
|
echo -e "${GREY}[i] whiptail nicht gefunden – TTY-Fallback.${NC}\n"
|
||||||
|
read -r -p "Mailserver-Host (MX) [${MTA_DEFAULT}]: " _mta_in; _mta_in="${_mta_in:-$MTA_DEFAULT}"
|
||||||
|
read -r -p "UI / Admin-Panel Host [${UI_DEFAULT}]: " _ui_in; _ui_in="${_ui_in:-$UI_DEFAULT}"
|
||||||
|
read -r -p "Webmail Host [${WEBMAIL_DEFAULT}]: " _wm_in; _wm_in="${_wm_in:-$WEBMAIL_DEFAULT}"
|
||||||
|
|
||||||
|
if is_local_like "$_mta_in" || is_local_like "$_ui_in" || is_local_like "$_wm_in"; then
|
||||||
|
DEV_MODE=1; APP_ENV="local"; APP_DEBUG="true"
|
||||||
|
fi
|
||||||
|
export DEV_MODE APP_ENV APP_DEBUG
|
||||||
|
|
||||||
|
MTA_FQDN="$(normalize_host "$_mta_in" "$MTA_DEFAULT")"
|
||||||
|
UI_FQDN="$(normalize_host "$_ui_in" "$UI_DEFAULT")"
|
||||||
|
WEBMAIL_FQDN="$(normalize_host "$_wm_in" "$WEBMAIL_DEFAULT")"
|
||||||
|
|
||||||
|
read -r -p "ClamAV aktivieren? (1/0, Enter=1): " CLAMAV_ENABLE; CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
||||||
|
read -r -p "OpenDMARC aktivieren? (1/0, Enter=1): " OPENDMARC_ENABLE; OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
||||||
|
read -r -p "Fail2Ban aktivieren? (1/0, Enter=1): " FAIL2BAN_ENABLE; FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#if have_whiptail; then
|
||||||
|
# TITLE="MailWolt Setup"
|
||||||
|
#
|
||||||
|
# MTA_FQDN="$(whiptail --title "$TITLE" --inputbox "Mailserver-FQDN (MX)\nBeispiel: mx.domain.tld" 11 70 "$MTA_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
# valid_fqdn "$MTA_FQDN" || MTA_FQDN="$MTA_DEFAULT"
|
||||||
|
#
|
||||||
|
# UI_FQDN="$(whiptail --title "$TITLE" --inputbox "UI / Admin-Panel FQDN\nBeispiel: ui.domain.tld" 11 70 "$UI_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
# valid_fqdn "$UI_FQDN" || UI_FQDN="$UI_DEFAULT"
|
||||||
|
#
|
||||||
|
# WEBMAIL_FQDN="$(whiptail --title "$TITLE" --inputbox "Webmail FQDN\nBeispiel: webmail.domain.tld" 11 70 "$WEBMAIL_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
||||||
|
# valid_fqdn "$WEBMAIL_FQDN" || WEBMAIL_FQDN="$WEBMAIL_DEFAULT"
|
||||||
|
#
|
||||||
|
# CHOICES="$(whiptail --title "$TITLE" --checklist "Optionale Dienste aktivieren" 15 70 6 \
|
||||||
|
# "ClamAV" "Virenscan (clamd/clamav-daemon)" ON \
|
||||||
|
# "OpenDMARC" "DMARC-Auswertung" ON \
|
||||||
|
# "Fail2Ban" "Brute-Force-Schutz" ON \
|
||||||
|
# 3>&1 1>&2 2>&3)" || true
|
||||||
|
#
|
||||||
|
# CLAMAV_ENABLE=0; [[ "$CHOICES" == *"ClamAV"* ]] && CLAMAV_ENABLE=1
|
||||||
|
# OPENDMARC_ENABLE=0; [[ "$CHOICES" == *"OpenDMARC"* ]] && OPENDMARC_ENABLE=1
|
||||||
|
# FAIL2BAN_ENABLE=0; [[ "$CHOICES" == *"Fail2Ban"* ]] && FAIL2BAN_ENABLE=1
|
||||||
|
#
|
||||||
|
# whiptail --title "$TITLE" --msgbox "Zusammenfassung:
|
||||||
|
#
|
||||||
|
#MX : $MTA_FQDN
|
||||||
|
#UI : $UI_FQDN
|
||||||
|
#Webmail : $WEBMAIL_FQDN
|
||||||
|
#
|
||||||
|
#ClamAV : $([[ $CLAMAV_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
|
||||||
|
#OpenDMARC : $([[ $OPENDMARC_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
|
||||||
|
#Fail2Ban : $([[ $FAIL2BAN_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
|
||||||
|
#" 16 70
|
||||||
|
#
|
||||||
|
#else
|
||||||
|
# echo -e "${GREY}[i] whiptail nicht gefunden – nutze TTY-Prompts.${NC}\n"
|
||||||
|
# ask_tty_domain "Mailserver-FQDN (MX)" "mx.domain.tld" "$MTA_DEFAULT" MTA_FQDN
|
||||||
|
# ask_tty_domain "UI / Admin-Panel FQDN" "ui.domain.tld" "$UI_DEFAULT" UI_FQDN
|
||||||
|
# ask_tty_domain "Webmail FQDN" "webmail.domain.tld" "$WEBMAIL_DEFAULT" WEBMAIL_FQDN
|
||||||
|
#
|
||||||
|
# read -r -p "ClamAV aktivieren? (1/0, Enter=1): " CLAMAV_ENABLE; CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
||||||
|
# read -r -p "OpenDMARC aktivieren? (1/0, Enter=1): " OPENDMARC_ENABLE; OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
||||||
|
# read -r -p "Fail2Ban aktivieren? (1/0, Enter=1): " FAIL2BAN_ENABLE; FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
||||||
|
#fi
|
||||||
|
|
||||||
|
# ── Defaults/Kompatibilität ──────────────────────────────────
|
||||||
|
MTA_FQDN="${MTA_FQDN:-${MTA_DEFAULT}}"
|
||||||
|
UI_FQDN="${UI_FQDN:-${UI_DEFAULT}}"
|
||||||
|
WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_DEFAULT}}"
|
||||||
|
DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
||||||
|
DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
||||||
|
DKIM_GENERATE="${DKIM_GENERATE:-1}"
|
||||||
|
|
||||||
|
# BASE_DOMAIN/Subs aus FQDNs ableiten
|
||||||
|
if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then MTA_SUB="${BASH_REMATCH[1]}"; BASE_DOMAIN="${BASH_REMATCH[2]}"; fi
|
||||||
|
if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then UI_SUB="${BASH_REMATCH[1]}"; fi
|
||||||
|
if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then WEBMAIL_SUB="${BASH_REMATCH[1]}"; fi
|
||||||
|
|
||||||
|
SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
|
||||||
|
SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
|
||||||
|
|
||||||
|
MAIL_HOSTNAME="${MTA_FQDN}"
|
||||||
|
UI_HOST="${UI_FQDN}"
|
||||||
|
WEBMAIL_HOST="${WEBMAIL_FQDN}"
|
||||||
|
|
||||||
|
APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
|
||||||
|
APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
|
||||||
|
|
||||||
|
# ── Export & persist ─────────────────────────────────────────
|
||||||
|
export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
|
||||||
|
export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
|
||||||
|
export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
|
||||||
|
export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
|
||||||
|
export DB_NAME DB_USER
|
||||||
|
export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
|
||||||
|
export CLAMAV_ENABLE OPENDMARC_ENABLE FAIL2BAN_ENABLE
|
||||||
|
|
||||||
|
install -d -m 0755 /etc/mailwolt
|
||||||
|
cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
||||||
|
SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
||||||
|
APP_TZ=${APP_TZ}
|
||||||
|
APP_LOCALE=${APP_LOCALE}
|
||||||
|
BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
MTA_SUB=${MTA_SUB}
|
||||||
|
UI_SUB=${UI_SUB}
|
||||||
|
WEBMAIL_SUB=${WEBMAIL_SUB}
|
||||||
|
|
||||||
|
MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
UI_HOST=${UI_HOST}
|
||||||
|
WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
|
||||||
|
LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
||||||
|
SYSMAIL_SUB=${SYSMAIL_SUB}
|
||||||
|
SYSMAIL_DOMAIN=${SYSMAIL_DOMAIN}
|
||||||
|
|
||||||
|
DKIM_ENABLE=${DKIM_ENABLE}
|
||||||
|
DKIM_SELECTOR=${DKIM_SELECTOR}
|
||||||
|
DKIM_GENERATE=${DKIM_GENERATE}
|
||||||
|
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_NAME=${DB_NAME}
|
||||||
|
DB_USER=${DB_USER}
|
||||||
|
DB_PASS=${DB_PASS}
|
||||||
|
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASS=${REDIS_PASS}
|
||||||
|
|
||||||
|
SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
||||||
|
SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
||||||
|
APP_ENV=${APP_ENV}
|
||||||
|
|
||||||
|
CLAMAV_ENABLE=${CLAMAV_ENABLE}
|
||||||
|
OPENDMARC_ENABLE=${OPENDMARC_ENABLE}
|
||||||
|
FAIL2BAN_ENABLE=${FAIL2BAN_ENABLE}
|
||||||
|
|
||||||
|
BACKUP_ONCALENDAR="${BACKUP_ONCALENDAR:-*-*-* 03:00:00}"
|
||||||
|
BACKUP_ENABLED=0
|
||||||
|
BACKUP_INTERVAL=daily
|
||||||
|
BACKUP_RETENTION_DAYS=7
|
||||||
|
BACKUP_DIR=/var/backups/mailwolt
|
||||||
|
BACKUP_USE_ZSTD=1
|
||||||
|
EOF
|
||||||
|
chmod 600 /etc/mailwolt/installer.env
|
||||||
|
|
||||||
|
# ── Installer-Sequenz ────────────────────────────────────────
|
||||||
|
for STEP in \
|
||||||
|
10-provision \
|
||||||
|
20-ssl 21-le-deploy-hook 22-dkim-helper \
|
||||||
|
30-db 40-postfix 50-dovecot \
|
||||||
|
60-rspamd-opendkim 61-opendmarc 62-clamav 63-fail2ban 64-apply-milters \
|
||||||
|
70-nginx 75-le-issue 80-app 88-update-wrapper 90-services \
|
||||||
|
92-sudoers-npm 93-backup-tools 95-woltguard 98-motd 99-summary
|
||||||
|
do
|
||||||
|
log ">>> Running ${STEP}.sh"
|
||||||
|
bash "./${STEP}.sh"
|
||||||
|
done
|
||||||
|
|
||||||
|
##!/usr/bin/env bash
|
||||||
|
#set -euo pipefail
|
||||||
|
#
|
||||||
|
## --- Flags / Modi ---
|
||||||
|
#DEV_MODE=0
|
||||||
|
#PROXY_MODE=0
|
||||||
|
#NPM_IP=""
|
||||||
|
#
|
||||||
|
#while [[ $# -gt 0 ]]; do
|
||||||
|
# case "$1" in
|
||||||
|
# -dev) DEV_MODE=1 ;;
|
||||||
|
# -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
|
||||||
|
# esac
|
||||||
|
# shift
|
||||||
|
#done
|
||||||
|
#
|
||||||
|
#APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
|
||||||
|
#APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
|
||||||
|
#export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
|
||||||
|
#
|
||||||
|
#DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
#REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
#
|
||||||
|
#export DB_PASS REDIS_PASS
|
||||||
|
#
|
||||||
|
#cd "$(dirname "$0")"
|
||||||
|
#source ./lib.sh
|
||||||
|
#require_root
|
||||||
|
#header
|
||||||
|
#
|
||||||
|
## ── Defaults ────────────────────────────────────────────────────────────────
|
||||||
|
#APP_NAME="${APP_NAME:-MailWolt}"
|
||||||
|
#APP_USER="${APP_USER:-mailwolt}"
|
||||||
|
#APP_GROUP="${APP_GROUP:-www-data}"
|
||||||
|
#APP_USER_PREFIX="${APP_USER_PREFIX:-mw}"
|
||||||
|
#APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
|
||||||
|
#
|
||||||
|
#BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
||||||
|
#UI_SUB="${UI_SUB:-ui}"
|
||||||
|
#WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}"
|
||||||
|
#MTA_SUB="${MTA_SUB:-mx}"
|
||||||
|
#
|
||||||
|
#DB_NAME="${DB_NAME:-${APP_USER}}"
|
||||||
|
#DB_USER="${DB_USER:-${APP_USER}}"
|
||||||
|
#
|
||||||
|
#SERVER_PUBLIC_IPV4="$(detect_ip)"
|
||||||
|
#SERVER_PUBLIC_IPV6="$(detect_ipv6)"
|
||||||
|
#DEFAULT_TZ="$(detect_timezone)"
|
||||||
|
#DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")"
|
||||||
|
#
|
||||||
|
#echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}"
|
||||||
|
#
|
||||||
|
## ── Schöne, farbige Abfragen ────────────────────────────────────────────────
|
||||||
|
#echo -e "${CYAN}"
|
||||||
|
#echo "──────────────────────────────────────────────"
|
||||||
|
#echo -e " 📧 MailWolt Setup – Domain Konfiguration"
|
||||||
|
#echo "──────────────────────────────────────────────"
|
||||||
|
#echo -e "${NC}"
|
||||||
|
#
|
||||||
|
#MTA_DEFAULT="${MTA_SUB}.${BASE_DOMAIN}"
|
||||||
|
#UI_DEFAULT="${UI_SUB}.${BASE_DOMAIN}"
|
||||||
|
#WEBMAIL_DEFAULT="${WEBMAIL_SUB}.${BASE_DOMAIN}"
|
||||||
|
#
|
||||||
|
#ask_domain() {
|
||||||
|
# local __outvar="$1" label="$2" example="$3" defval="$4" input=""
|
||||||
|
# echo -e "${GREEN}[?]${NC} ${label}"
|
||||||
|
# echo -e " z.B. ${YELLOW}${example}${NC}"
|
||||||
|
# echo -e " Default: ${CYAN}${defval}${NC}"
|
||||||
|
# echo -ne " → Eingabe: ${CYAN}"
|
||||||
|
# read -r input
|
||||||
|
# echo -e "${NC}"
|
||||||
|
# if [[ -z "$input" ]]; then
|
||||||
|
# eval "$__outvar='$defval'"
|
||||||
|
# else
|
||||||
|
# eval "$__outvar='$input'"
|
||||||
|
# fi
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#ask_toggle() {
|
||||||
|
# local __outvar="$1" label="$2" defval="${3:-1}" input=""
|
||||||
|
# echo -ne "${GREEN}[?]${NC} ${label} (${CYAN}1${NC}=Ja / ${YELLOW}0${NC}=Nein) [Enter=${defval}]: "
|
||||||
|
# read -r input
|
||||||
|
# input="${input:-$defval}"
|
||||||
|
# case "$input" in
|
||||||
|
# 1|0) ;;
|
||||||
|
# *) echo -e "${YELLOW}Ungültig, nehme Default=${defval}.${NC}"; input="$defval" ;;
|
||||||
|
# esac
|
||||||
|
# eval "$__outvar='$input'"
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#ask_domain "MTA_FQDN" "Mailserver-FQDN (MX)" "mx.domain.tld" "$MTA_DEFAULT"
|
||||||
|
#ask_domain "UI_FQDN" "UI / Admin-Panel" "ui.domain.tld" "$UI_DEFAULT"
|
||||||
|
#ask_domain "WEBMAIL_FQDN" "Webmail-FQDN" "webmail.domain.tld" "$WEBMAIL_DEFAULT"
|
||||||
|
#
|
||||||
|
#echo -e "${CYAN}"
|
||||||
|
#echo "──────────────────────────────────────────────"
|
||||||
|
#echo -e " 🛡 Optionale Dienste"
|
||||||
|
#echo "──────────────────────────────────────────────"
|
||||||
|
#echo -e "${NC}"
|
||||||
|
#
|
||||||
|
#ask_toggle "CLAMAV_ENABLE" "ClamAV Virenscan aktivieren?" 1
|
||||||
|
#ask_toggle "OPENDMARC_ENABLE" "OpenDMARC auswerten?" 1
|
||||||
|
#ask_toggle "FAIL2BAN_ENABLE" "Fail2Ban aktivieren?" 1
|
||||||
|
#echo
|
||||||
|
#
|
||||||
|
## Defaults, wenn Enter gedrückt (Abwärtskompatibilität)
|
||||||
|
#MTA_FQDN="${MTA_FQDN:-${MTA_SUB}.${BASE_DOMAIN}}"
|
||||||
|
#UI_FQDN="${UI_FQDN:-${UI_SUB}.${BASE_DOMAIN}}"
|
||||||
|
#WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_SUB}.${BASE_DOMAIN}}"
|
||||||
|
#DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
||||||
|
#DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
||||||
|
#DKIM_GENERATE="${DKIM_GENERATE:-1}"
|
||||||
|
#
|
||||||
|
## BASE_DOMAIN und Sub-Labels aus MTA/UI/WEBMAIL ableiten (robust)
|
||||||
|
#if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
# MTA_SUB="${BASH_REMATCH[1]}"
|
||||||
|
# BASE_DOMAIN="${BASH_REMATCH[2]}"
|
||||||
|
#fi
|
||||||
|
#if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
# UI_SUB="${BASH_REMATCH[1]}"
|
||||||
|
#fi
|
||||||
|
#if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
# WEBMAIL_SUB="${BASH_REMATCH[1]}"
|
||||||
|
#fi
|
||||||
|
#
|
||||||
|
#SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
|
||||||
|
#SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
|
||||||
|
## Kanonische Host-Variablen (NIE wieder zusammenbauen – nimm die FQDNs)
|
||||||
|
#MAIL_HOSTNAME="${MTA_FQDN}"
|
||||||
|
#UI_HOST="${UI_FQDN}"
|
||||||
|
#WEBMAIL_HOST="${WEBMAIL_FQDN}"
|
||||||
|
#
|
||||||
|
## Zeitzone/Locale sinnvoll setzen
|
||||||
|
#APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
|
||||||
|
#APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
|
||||||
|
#
|
||||||
|
## ── Variablen exportieren ───────────────────────────────────────────────────
|
||||||
|
#export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
|
||||||
|
#export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
|
||||||
|
#export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
|
||||||
|
#export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
|
||||||
|
#export DB_NAME DB_USER
|
||||||
|
#export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
|
||||||
|
#export CLAMAV_ENABLE OPENDMARC_ENABLE FAIL2BAN_ENABLE
|
||||||
|
#
|
||||||
|
#install -d -m 0755 /etc/mailwolt
|
||||||
|
#cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
#BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
#MTA_SUB=${MTA_SUB}
|
||||||
|
#UI_SUB=${UI_SUB}
|
||||||
|
#WEBMAIL_SUB=${WEBMAIL_SUB}
|
||||||
|
#
|
||||||
|
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
#UI_HOST=${UI_HOST}
|
||||||
|
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
#
|
||||||
|
#SYSMAIL_SUB=${SYSMAIL_SUB}
|
||||||
|
#SYSMAIL_DOMAIN=${SYSMAIL_DOMAIN}
|
||||||
|
#
|
||||||
|
#DKIM_ENABLE=${DKIM_ENABLE}
|
||||||
|
#DKIM_SELECTOR=${DKIM_SELECTOR}
|
||||||
|
#DKIM_GENERATE=${DKIM_GENERATE}
|
||||||
|
#
|
||||||
|
#DB_HOST=127.0.0.1
|
||||||
|
#DB_NAME=${DB_NAME}
|
||||||
|
#DB_USER=${DB_USER}
|
||||||
|
#DB_PASS=${DB_PASS}
|
||||||
|
#REDIS_PASS=${REDIS_PASS}
|
||||||
|
#
|
||||||
|
#SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
||||||
|
#SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
||||||
|
#APP_ENV=${APP_ENV}
|
||||||
|
#
|
||||||
|
#CLAMAV_ENABLE=${CLAMAV_ENABLE}
|
||||||
|
#OPENDMARC_ENABLE=${OPENDMARC_ENABLE}
|
||||||
|
#FAIL2BAN_ENABLE=${FAIL2BAN_ENABLE}
|
||||||
|
#EOF
|
||||||
|
#
|
||||||
|
#chmod 600 /etc/mailwolt/installer.env
|
||||||
|
#
|
||||||
|
## ── Sequenz ────────────────────────────────────────────────────────────────
|
||||||
|
#for STEP in 10-provision 20-ssl 21-le-deploy-hook 22-dkim-helper 30-db 40-postfix 50-dovecot 60-rspamd-opendkim 61-opendmarc 62-clamav 63-fail2ban 70-nginx 75-le-issue 80-app 90-services 95-woltguard 98-motd 99-summary
|
||||||
|
#do
|
||||||
|
# log ">>> Running ${STEP}.sh"
|
||||||
|
# bash "./${STEP}.sh"
|
||||||
|
#done
|
||||||
|
###!/usr/bin/env bash
|
||||||
|
##set -euo pipefail
|
||||||
|
##
|
||||||
|
### --- Flags / Modi ---
|
||||||
|
##DEV_MODE=0
|
||||||
|
##PROXY_MODE=0
|
||||||
|
##NPM_IP=""
|
||||||
|
##
|
||||||
|
##while [[ $# -gt 0 ]]; do
|
||||||
|
## case "$1" in
|
||||||
|
## -dev) DEV_MODE=1 ;;
|
||||||
|
## -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
|
||||||
|
## esac
|
||||||
|
## shift
|
||||||
|
##done
|
||||||
|
##
|
||||||
|
##APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
|
||||||
|
##APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
|
||||||
|
##export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
|
||||||
|
##
|
||||||
|
##DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
##REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
|
||||||
|
##
|
||||||
|
##export DB_PASS REDIS_PASS
|
||||||
|
##
|
||||||
|
##cd "$(dirname "$0")"
|
||||||
|
##source ./lib.sh
|
||||||
|
##require_root
|
||||||
|
##header
|
||||||
|
##
|
||||||
|
### ── Defaults ────────────────────────────────────────────────────────────────
|
||||||
|
##APP_NAME="${APP_NAME:-MailWolt}"
|
||||||
|
##APP_USER="${APP_USER:-mailwolt}"
|
||||||
|
##APP_GROUP="${APP_GROUP:-www-data}"
|
||||||
|
##APP_USER_PREFIX="${APP_USER_PREFIX:-mw}"
|
||||||
|
##APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
|
||||||
|
##
|
||||||
|
##BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
||||||
|
##UI_SUB="${UI_SUB:-ui}"
|
||||||
|
##WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}"
|
||||||
|
##MTA_SUB="${MTA_SUB:-mx}"
|
||||||
|
##
|
||||||
|
##DB_NAME="${DB_NAME:-${APP_USER}}"
|
||||||
|
##DB_USER="${DB_USER:-${APP_USER}}"
|
||||||
|
##
|
||||||
|
##SERVER_PUBLIC_IPV4="$(detect_ip)"
|
||||||
|
##SERVER_PUBLIC_IPV6="$(detect_ipv6)"
|
||||||
|
##DEFAULT_TZ="$(detect_timezone)"
|
||||||
|
##DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")"
|
||||||
|
##
|
||||||
|
##echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}"
|
||||||
|
##
|
||||||
|
### ── FQDNs abfragen ───────────────────────────────────────────────────────────
|
||||||
|
##read -r -p "Mailserver FQDN (MX, z.B. mx.domain.tld) [Enter=${MTA_SUB}.${BASE_DOMAIN}]: " MTA_FQDN
|
||||||
|
##read -r -p "UI / Admin-Panel FQDN (z.B. ui.domain.tld) [Enter=${UI_SUB}.${BASE_DOMAIN}]: " UI_FQDN
|
||||||
|
##read -r -p "Webmail FQDN (z.B. webmail.domain.tld) [Enter=${WEBMAIL_SUB}.${BASE_DOMAIN}]: " WEBMAIL_FQDN
|
||||||
|
##
|
||||||
|
### Defaults, wenn Enter gedrückt
|
||||||
|
##MTA_FQDN="${MTA_FQDN:-${MTA_SUB}.${BASE_DOMAIN}}"
|
||||||
|
##UI_FQDN="${UI_FQDN:-${UI_SUB}.${BASE_DOMAIN}}"
|
||||||
|
##WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_SUB}.${BASE_DOMAIN}}"
|
||||||
|
##DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
||||||
|
##DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
||||||
|
##DKIM_GENERATE="${DKIM_GENERATE:-1}"
|
||||||
|
##
|
||||||
|
### BASE_DOMAIN und Sub-Labels aus MTA/UI/WEBMAIL ableiten (robust)
|
||||||
|
##if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
## MTA_SUB="${BASH_REMATCH[1]}"
|
||||||
|
## BASE_DOMAIN="${BASH_REMATCH[2]}"
|
||||||
|
##fi
|
||||||
|
##if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
## UI_SUB="${BASH_REMATCH[1]}"
|
||||||
|
##fi
|
||||||
|
##if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
||||||
|
## WEBMAIL_SUB="${BASH_REMATCH[1]}"
|
||||||
|
##fi
|
||||||
|
##
|
||||||
|
##SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
|
||||||
|
##SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
|
||||||
|
### Kanonische Host-Variablen (NIE wieder zusammenbauen – nimm die FQDNs)
|
||||||
|
##MAIL_HOSTNAME="${MTA_FQDN}"
|
||||||
|
##UI_HOST="${UI_FQDN}"
|
||||||
|
##WEBMAIL_HOST="${WEBMAIL_FQDN}"
|
||||||
|
##
|
||||||
|
### Zeitzone/Locale sinnvoll setzen (könntest du auch noch abfragen)
|
||||||
|
##APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
|
||||||
|
##APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
|
||||||
|
##
|
||||||
|
### ── Variablen exportieren ───────────────────────────────────────────────────
|
||||||
|
##export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
|
||||||
|
##export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
|
||||||
|
##export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
|
||||||
|
##export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
|
||||||
|
##export DB_NAME DB_USER
|
||||||
|
##export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
|
||||||
|
##
|
||||||
|
##install -d -m 0755 /etc/mailwolt
|
||||||
|
##cat >/etc/mailwolt/installer.env <<EOF
|
||||||
|
##BASE_DOMAIN=${BASE_DOMAIN}
|
||||||
|
##MTA_SUB=${MTA_SUB}
|
||||||
|
##UI_SUB=${UI_SUB}
|
||||||
|
##WEBMAIL_SUB=${WEBMAIL_SUB}
|
||||||
|
##
|
||||||
|
##MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
||||||
|
##UI_HOST=${UI_HOST}
|
||||||
|
##WEBMAIL_HOST=${WEBMAIL_HOST}
|
||||||
|
##
|
||||||
|
##SYSMAIL_SUB=${SYSMAIL_SUB}
|
||||||
|
##SYSMAIL_DOMAIN=${SYSMAIL_DOMAIN}
|
||||||
|
##
|
||||||
|
##DKIM_ENABLE=${DKIM_ENABLE}
|
||||||
|
##DKIM_SELECTOR=${DKIM_SELECTOR}
|
||||||
|
##DKIM_GENERATE=${DKIM_GENERATE}
|
||||||
|
##
|
||||||
|
##DB_HOST=127.0.0.1
|
||||||
|
##DB_NAME=${DB_NAME}
|
||||||
|
##DB_USER=${DB_USER}
|
||||||
|
##DB_PASS=${DB_PASS}
|
||||||
|
##REDIS_PASS=${REDIS_PASS}
|
||||||
|
##
|
||||||
|
##SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
||||||
|
##SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
||||||
|
##APP_ENV=${APP_ENV}
|
||||||
|
##
|
||||||
|
##CLAMAV_ENABLE=1
|
||||||
|
##OPENDMARC_ENABLE=1
|
||||||
|
##FAIL2BAN_ENABLE=1
|
||||||
|
##EOF
|
||||||
|
##
|
||||||
|
##chmod 600 /etc/mailwolt/installer.env
|
||||||
|
##
|
||||||
|
### ── Sequenz ────────────────────────────────────────────────────────────────
|
||||||
|
##for STEP in 10-provision 20-ssl 21-le-deploy-hook 22-dkim-helper 30-db 40-postfix 50-dovecot 60-rspamd-opendkim 61-opendmarc 62-clamav 63-fail2ban 70-nginx 75-le-issue 80-app 90-services 95-woltguard 98-motd 99-summary
|
||||||
|
##do
|
||||||
|
## log ">>> Running ${STEP}.sh"
|
||||||
|
## bash "./${STEP}.sh"
|
||||||
|
##done
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Mailwolt Installer-Wrapper
|
||||||
|
# Deploy to: /usr/local/sbin/mailwolt-install
|
||||||
|
# Permissions: chmod 0755, chown root:root
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
LOG="/var/log/mailwolt-install.log"
|
||||||
|
STATE_DIR="/var/lib/mailwolt/install"
|
||||||
|
INSTALLER_SCRIPT="/var/www/mailwolt/mailwolt-installer/install.sh"
|
||||||
|
APP_DIR="/var/www/mailwolt"
|
||||||
|
|
||||||
|
install -d -m 0755 "$STATE_DIR" /var/lib/mailwolt
|
||||||
|
: > "$LOG"
|
||||||
|
chmod 0644 "$LOG"
|
||||||
|
|
||||||
|
echo "running" > "$STATE_DIR/state"
|
||||||
|
: > "$STATE_DIR/rc"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "===== $(date -Is) :: Installation gestartet ====="
|
||||||
|
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
echo "[!] Muss als root laufen"
|
||||||
|
printf '1\n' > "$STATE_DIR/rc"
|
||||||
|
echo "done" > "$STATE_DIR/state"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Komponente aus $1, falls übergeben (z.B. "nginx", "postfix", "dovecot", "all")
|
||||||
|
COMPONENT="${1:-all}"
|
||||||
|
echo "[i] Komponente: $COMPONENT"
|
||||||
|
|
||||||
|
RC=0
|
||||||
|
if [[ -f "$INSTALLER_SCRIPT" ]]; then
|
||||||
|
APP_DIR="$APP_DIR" COMPONENT="$COMPONENT" bash "$INSTALLER_SCRIPT" || RC=$?
|
||||||
|
else
|
||||||
|
echo "[!] installer script nicht gefunden: $INSTALLER_SCRIPT"
|
||||||
|
RC=127
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "===== $(date -Is) :: Installation beendet (rc=$RC) ====="
|
||||||
|
printf '%s\n' "$RC" > "$STATE_DIR/rc"
|
||||||
|
echo "done" > "$STATE_DIR/state"
|
||||||
|
exit "$RC"
|
||||||
|
} 2>&1 | tee -a "$LOG"
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ -f /etc/mailwolt/installer.env ]; then
|
||||||
|
set -a
|
||||||
|
. /etc/mailwolt/installer.env
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
# ── Styling ────────────────────────────────────────────────────────────────
|
||||||
|
GREEN="$(printf '\033[1;32m')"; YELLOW="$(printf '\033[1;33m')"
|
||||||
|
RED="$(printf '\033[1;31m')"; CYAN="$(printf '\033[1;36m')"
|
||||||
|
GREY="$(printf '\033[0;90m')"; NC="$(printf '\033[0m')"
|
||||||
|
BAR="──────────────────────────────────────────────────────────────────────────────"
|
||||||
|
log(){ echo -e "${GREEN}[+]${NC} $*"; }
|
||||||
|
warn(){ echo -e "${YELLOW}[!]${NC} $*"; }
|
||||||
|
err(){ echo -e "${RED}[x]${NC} $*"; }
|
||||||
|
die(){ err "$*"; exit 1; }
|
||||||
|
require_root(){ [[ "$(id -u)" -eq 0 ]] || die "Bitte als root ausführen."; }
|
||||||
|
|
||||||
|
# --- Defaults, nur wenn noch nicht gesetzt ---------------------------------
|
||||||
|
: "${APP_USER:=mailwolt}"
|
||||||
|
: "${APP_GROUP:=www-data}"
|
||||||
|
: "${APP_DIR:=/var/www/${APP_USER}}"
|
||||||
|
|
||||||
|
: "${APP_NAME:=MailWolt}"
|
||||||
|
|
||||||
|
: "${BASE_DOMAIN:=example.com}"
|
||||||
|
: "${UI_SUB:=ui}"
|
||||||
|
: "${WEBMAIL_SUB:=webmail}"
|
||||||
|
: "${MTA_SUB:=mx}"
|
||||||
|
|
||||||
|
# DB / Redis (werden später durch .env überschrieben)
|
||||||
|
: "${DB_NAME:=${APP_USER}}"
|
||||||
|
: "${DB_USER:=${APP_USER}}"
|
||||||
|
: "${DB_PASS:=}"
|
||||||
|
: "${REDIS_PASS:=}"
|
||||||
|
|
||||||
|
# Stabile Zert-Pfade (UI/WEBMAIL/MX → symlinked via 20-ssl.sh)
|
||||||
|
: "${MAIL_SSL_DIR:=/etc/ssl/mail}"
|
||||||
|
: "${UI_SSL_DIR:=/etc/ssl/ui}"
|
||||||
|
: "${WEBMAIL_SSL_DIR:=/etc/ssl/webmail}"
|
||||||
|
: "${UI_CERT:=${UI_SSL_DIR}/fullchain.pem}"
|
||||||
|
: "${UI_KEY:=${UI_SSL_DIR}/privkey.pem}"
|
||||||
|
|
||||||
|
# Optional: E-Mail für LE
|
||||||
|
: "${LE_EMAIL:=admin@${BASE_DOMAIN}}"
|
||||||
|
|
||||||
|
load_env_file(){
|
||||||
|
local f="$1"
|
||||||
|
[[ -f "$f" ]] || return 0
|
||||||
|
while IFS='=' read -r k v; do
|
||||||
|
[[ "$k" =~ ^[A-Z0-9_]+$ ]] || continue
|
||||||
|
export "$k=$v"
|
||||||
|
done < <(grep -E '^[A-Z0-9_]+=' "$f")
|
||||||
|
}
|
||||||
|
|
||||||
|
header(){ echo -e "${CYAN}${BAR}${NC}
|
||||||
|
${CYAN} :::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::
|
||||||
|
${CYAN} +:+:+: :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
|
||||||
|
${CYAN} +:+ +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
|
||||||
|
${CYAN} +#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+
|
||||||
|
${CYAN} +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+
|
||||||
|
${CYAN} #+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+#
|
||||||
|
${CYAN} ### ### ### ### ########### ########## ### ### ######## ########## ###
|
||||||
|
${CYAN} ${CYAN}${BAR}${NC}\n"; }
|
||||||
|
|
||||||
|
#header(){ echo -e "${CYAN}${BAR}${NC}
|
||||||
|
#${CYAN} 888b d888 d8b 888 888 888 888 888
|
||||||
|
#${CYAN} 8888b d8888 Y8P 888 888 o 888 888 888
|
||||||
|
#${CYAN} 88888b.d88888 888 888 d8b 888 888 888
|
||||||
|
#${CYAN} 888Y88888P888 8888b. 888 888 888 d888b 888 .d88b. 888 888888
|
||||||
|
#${CYAN} 888 Y888P 888 '88b 888 888 888d88888b888 d88''88b 888 888
|
||||||
|
#${CYAN} 888 Y8P 888 .d888888 888 888 88888P Y88888 888 888 888 888
|
||||||
|
#${CYAN} 888 ' 888 888 888 888 888 8888P Y8888 Y88..88P 888 Y88b.
|
||||||
|
#${CYAN} 888 888 'Y888888 888 888 888P Y888 'Y88P' 888 'Y888
|
||||||
|
#${CYAN}${BAR}${NC}\n"; }
|
||||||
|
|
||||||
|
detect_ip(){
|
||||||
|
local ip
|
||||||
|
ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')" || true
|
||||||
|
[[ -n "${ip:-}" ]] || ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||||
|
[[ -n "${ip:-}" ]] || die "Konnte Server-IP nicht ermitteln."
|
||||||
|
echo "$ip"
|
||||||
|
}
|
||||||
|
detect_ipv4() {
|
||||||
|
local ext=""
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
ext="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
|
||||||
|
[[ "$ext" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || ext=""
|
||||||
|
fi
|
||||||
|
echo "$ext"
|
||||||
|
}
|
||||||
|
detect_ipv6(){
|
||||||
|
local ip6
|
||||||
|
ip6="$(ip -6 addr show scope global 2>/dev/null | awk '/inet6/{print $2}' | cut -d/ -f1 | head -n1)" || true
|
||||||
|
[[ -n "${ip6:-}" ]] || ip6="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i ~ /:/){print $i; exit}}')" || true
|
||||||
|
echo "${ip6:-}"
|
||||||
|
}
|
||||||
|
detect_timezone(){
|
||||||
|
local tz
|
||||||
|
if command -v timedatectl >/dev/null 2>&1; then
|
||||||
|
tz="$(timedatectl show -p Timezone --value 2>/dev/null | tr -d '[:space:]')" || true
|
||||||
|
[[ -n "${tz:-}" && "$tz" == */* ]] && { echo "$tz"; return; }
|
||||||
|
fi
|
||||||
|
[[ -r /etc/timezone ]] && { tz="$(sed -n '1p' /etc/timezone | tr -d '[:space:]')" || true; [[ "$tz" == */* ]] && { echo "$tz"; return; }; }
|
||||||
|
if [[ -L /etc/localtime ]]; then
|
||||||
|
tz="$(readlink -f /etc/localtime 2>/dev/null || true)"; tz="${tz#/usr/share/zoneinfo/}"
|
||||||
|
[[ "$tz" == */* ]] && { echo "$tz"; return; }
|
||||||
|
fi
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
tz="$(curl -fsSL --max-time 3 https://ipapi.co/timezone 2>/dev/null || true)"; [[ "$tz" == */* ]] && { echo "$tz"; return; }
|
||||||
|
fi
|
||||||
|
echo "UTC"
|
||||||
|
}
|
||||||
|
guess_locale_from_tz(){ case "${1:-UTC}" in
|
||||||
|
Europe/Berlin|Europe/Vienna|Europe/Zurich|Europe/Luxembourg|Europe/Brussels|Europe/Amsterdam) echo "de";;
|
||||||
|
*) echo "en";; esac; }
|
||||||
|
|
||||||
|
resolve_ok(){ local host="$1"; getent ahosts "$host" | awk '{print $1}' | sort -u | grep -q -F "${SERVER_PUBLIC_IPV4:-}" ; }
|
||||||
|
join_host(){ local sub="$1" base="$2"; [[ -z "$sub" ]] && echo "$base" || echo "$sub.$base"; }
|
||||||
|
|
||||||
|
# dns_preflight HOST [HOST2 ...]
|
||||||
|
# Prüft: A-Record → SERVER_PUBLIC_IPV4, MX (nur wenn HOST == MAIL_HOSTNAME), PTR.
|
||||||
|
# Gibt strukturierte Zeilen aus: OK|WARN|FAIL <host> <check> <detail>
|
||||||
|
# Rückgabe 0 = alles OK; 1 = mind. ein FAIL.
|
||||||
|
dns_preflight(){
|
||||||
|
local overall=0
|
||||||
|
local server_ip="${SERVER_PUBLIC_IPV4:-}"
|
||||||
|
|
||||||
|
_dns_line(){ local level="$1" host="$2" check="$3" detail="$4"
|
||||||
|
case "$level" in
|
||||||
|
OK) echo -e "${GREEN}[DNS OK ]${NC} ${host} ${GREY}${check}${NC} → ${detail}" ;;
|
||||||
|
WARN) echo -e "${YELLOW}[DNS WARN]${NC} ${host} ${GREY}${check}${NC} → ${detail}" ;;
|
||||||
|
FAIL) echo -e "${RED}[DNS FAIL]${NC} ${host} ${GREY}${check}${NC} → ${detail}"; overall=1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
for host in "$@"; do
|
||||||
|
[[ -z "$host" || "$host" == "example.com" ]] && continue
|
||||||
|
|
||||||
|
# A-Record
|
||||||
|
local a_ip
|
||||||
|
a_ip="$(dig +short A "$host" @1.1.1.1 2>/dev/null | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -n1)"
|
||||||
|
if [[ -z "$a_ip" ]]; then
|
||||||
|
_dns_line FAIL "$host" "A-Record" "kein Eintrag gefunden"
|
||||||
|
elif [[ -n "$server_ip" && "$a_ip" != "$server_ip" ]]; then
|
||||||
|
_dns_line FAIL "$host" "A-Record" "${a_ip} ≠ ${server_ip} (Server-IP)"
|
||||||
|
else
|
||||||
|
_dns_line OK "$host" "A-Record" "${a_ip}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# MX (nur für MAIL_HOSTNAME)
|
||||||
|
if [[ "$host" == "${MAIL_HOSTNAME:-}" ]]; then
|
||||||
|
local mx
|
||||||
|
mx="$(dig +short MX "$host" @1.1.1.1 2>/dev/null | awk '{print $2}' | head -n1)"
|
||||||
|
if [[ -z "$mx" ]]; then
|
||||||
|
_dns_line WARN "$host" "MX-Record" "kein Eintrag – ausgehende Mail ggf. eingeschränkt"
|
||||||
|
else
|
||||||
|
_dns_line OK "$host" "MX-Record" "${mx}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PTR (nur wenn IP bekannt)
|
||||||
|
if [[ -n "$server_ip" && -n "$a_ip" && "$a_ip" == "$server_ip" ]]; then
|
||||||
|
local ptr
|
||||||
|
ptr="$(dig +short -x "$server_ip" @1.1.1.1 2>/dev/null | head -n1 | sed 's/\.$//')"
|
||||||
|
if [[ -z "$ptr" ]]; then
|
||||||
|
_dns_line WARN "$host" "PTR-Record" "kein Reverse-DNS – kann Spam-Score erhöhen"
|
||||||
|
elif [[ "$ptr" != "$host" && "$ptr" != "${MAIL_HOSTNAME:-}" ]]; then
|
||||||
|
_dns_line WARN "$host" "PTR-Record" "${ptr} (zeigt nicht auf ${host})"
|
||||||
|
else
|
||||||
|
_dns_line OK "$host" "PTR-Record" "${ptr}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return $overall
|
||||||
|
}
|
||||||
|
|
||||||
|
upsert_env(){ # upsert in $ENV_FILE
|
||||||
|
local k="$1" v="$2" ek ev
|
||||||
|
ek="$(printf '%s' "$k" | sed -e 's/[.[\*^$(){}+?|/]/\\&/g')"
|
||||||
|
ev="$(printf '%s' "$v" | sed -e 's/[&/]/\\&/g')"
|
||||||
|
if grep -qE "^[#[:space:]]*${ek}=" "$ENV_FILE" 2>/dev/null; then
|
||||||
|
sed -Ei "s|^[#[:space:]]*${ek}=.*|${k}=${ev}|g" "$ENV_FILE"
|
||||||
|
else
|
||||||
|
printf '%s=%s\n' "$k" "$v" >> "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Phase 2: Go-Live — DNS-Check → LE-Zertifikate → Nginx → Postfix/Dovecot aktualisieren
|
||||||
|
# Muss als root ausgeführt werden, NACHDEM die DNS-Einträge gesetzt wurden.
|
||||||
|
# Liest Konfiguration aus /etc/mailwolt/installer.env (durch Phase 1 geschrieben).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "${SCRIPT_DIR}/lib.sh"
|
||||||
|
|
||||||
|
require_root
|
||||||
|
|
||||||
|
ENV_FILE="/etc/mailwolt/installer.env"
|
||||||
|
[[ -f "$ENV_FILE" ]] || die "Phase 1 noch nicht abgeschlossen – ${ENV_FILE} fehlt."
|
||||||
|
|
||||||
|
set -a; . "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
# ── Pflichtfelder ─────────────────────────────────────────────────────────────
|
||||||
|
: "${BASE_DOMAIN:?BASE_DOMAIN fehlt in ${ENV_FILE}}"
|
||||||
|
: "${UI_HOST:?UI_HOST fehlt in ${ENV_FILE}}"
|
||||||
|
: "${WEBMAIL_HOST:?WEBMAIL_HOST fehlt in ${ENV_FILE}}"
|
||||||
|
: "${MAIL_HOSTNAME:?MAIL_HOSTNAME fehlt in ${ENV_FILE}}"
|
||||||
|
: "${SERVER_PUBLIC_IPV4:?SERVER_PUBLIC_IPV4 fehlt in ${ENV_FILE}}"
|
||||||
|
|
||||||
|
header
|
||||||
|
echo -e "${CYAN} Phase 2 – Go-Live${NC}"
|
||||||
|
echo -e "${CYAN}${BAR}${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Schritt 1: DNS Preflight ──────────────────────────────────────────────────
|
||||||
|
log "DNS-Vorab-Check …"
|
||||||
|
|
||||||
|
SKIP_DNS="${SKIP_DNS:-0}"
|
||||||
|
dns_ok=0
|
||||||
|
|
||||||
|
if [[ "$SKIP_DNS" != "1" ]]; then
|
||||||
|
if dns_preflight "$UI_HOST" "$WEBMAIL_HOST" "$MAIL_HOSTNAME"; then
|
||||||
|
dns_ok=1
|
||||||
|
else
|
||||||
|
warn "Einige DNS-Einträge zeigen noch nicht auf diesen Server."
|
||||||
|
echo
|
||||||
|
echo -e " Möglichkeiten:"
|
||||||
|
echo -e " a) DNS reparieren und dieses Skript erneut ausführen."
|
||||||
|
echo -e " b) Trotzdem fortfahren: SKIP_DNS=1 bash phase2-go-live.sh"
|
||||||
|
echo
|
||||||
|
read -rp "Trotzdem fortfahren? [j/N] " _ans
|
||||||
|
[[ "${_ans,,}" == "j" ]] || die "Abgebrochen."
|
||||||
|
dns_ok=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "DNS-Check übersprungen (SKIP_DNS=1)."
|
||||||
|
dns_ok=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Schritt 2: Let's Encrypt Zertifikate ─────────────────────────────────────
|
||||||
|
log "Let's Encrypt Zertifikate ausstellen …"
|
||||||
|
bash "${SCRIPT_DIR}/75-le-issue.sh"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Schritt 3: Nginx-Konfiguration neu schreiben (TLS) ───────────────────────
|
||||||
|
log "Nginx-Konfiguration aktualisieren (TLS) …"
|
||||||
|
# Nginx-Builder aus 70-nginx.sh wiederverwenden
|
||||||
|
source "${SCRIPT_DIR}/70-nginx.sh" || true # sourcing setzt Variablen und führt aus
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Schritt 4: Postfix hostname + TLS-Zertifikate aktualisieren ──────────────
|
||||||
|
log "Postfix: myhostname = ${MAIL_HOSTNAME} …"
|
||||||
|
postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
||||||
|
postconf -e "myorigin = \$myhostname"
|
||||||
|
postconf -e "smtpd_tls_cert_file = /etc/ssl/mail/fullchain.pem"
|
||||||
|
postconf -e "smtpd_tls_key_file = /etc/ssl/mail/privkey.pem"
|
||||||
|
systemctl reload postfix || true
|
||||||
|
|
||||||
|
# ── Schritt 5: Dovecot TLS ───────────────────────────────────────────────────
|
||||||
|
log "Dovecot: TLS-Zertifikate aktualisieren …"
|
||||||
|
if [[ -f /etc/dovecot/conf.d/10-ssl.conf ]]; then
|
||||||
|
sed -i "s|^ssl_cert =.*|ssl_cert = </etc/ssl/mail/fullchain.pem|" /etc/dovecot/conf.d/10-ssl.conf || true
|
||||||
|
sed -i "s|^ssl_key =.*|ssl_key = </etc/ssl/mail/privkey.pem|" /etc/dovecot/conf.d/10-ssl.conf || true
|
||||||
|
systemctl reload dovecot || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Schritt 6: App-URL in .env aktualisieren ──────────────────────────────────
|
||||||
|
APP_ENV_FILE="${APP_DIR}/.env"
|
||||||
|
if [[ -f "$APP_ENV_FILE" ]]; then
|
||||||
|
log "Laravel APP_URL aktualisieren → https://${UI_HOST} …"
|
||||||
|
ENV_FILE="$APP_ENV_FILE"
|
||||||
|
upsert_env APP_URL "https://${UI_HOST}"
|
||||||
|
upsert_env APP_HOST "${UI_HOST}"
|
||||||
|
upsert_env SESSION_SECURE_COOKIE "true"
|
||||||
|
sudo -u "${APP_USER}" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Fertig ────────────────────────────────────────────────────────────────────
|
||||||
|
echo
|
||||||
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
|
echo -e "${GREEN} ✔ Phase 2 abgeschlossen!${NC}"
|
||||||
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
|
echo -e " UI: ${CYAN}https://${UI_HOST}${NC}"
|
||||||
|
echo -e " Webmail: ${CYAN}https://${WEBMAIL_HOST}${NC}"
|
||||||
|
echo -e " MX: ${GREY}${MAIL_HOSTNAME}${NC}"
|
||||||
|
echo
|
||||||
|
|
@ -3,17 +3,13 @@ set -euo pipefail
|
||||||
|
|
||||||
# -------- Konfiguration -------------------------------------------------------
|
# -------- Konfiguration -------------------------------------------------------
|
||||||
APP_USER="${APP_USER:-mailwolt}"
|
APP_USER="${APP_USER:-mailwolt}"
|
||||||
APP_GROUP="${APP_GROUP:-www-data}"
|
APP_GROUP="${APP_GROUP:-www-data}" # Fallback; setz das in deiner Umgebung falls anders
|
||||||
APP_DIR="${APP_DIR:-/var/www/mailwolt}"
|
APP_DIR="${APP_DIR:-/var/www/mailwolt}"
|
||||||
BRANCH="${BRANCH:-main}"
|
BRANCH="${BRANCH:-main}" # nur relevant bei UPDATE_MODE=branch
|
||||||
MODE="${UPDATE_MODE:-tags}"
|
MODE="${UPDATE_MODE:-tags}" # tags | branch
|
||||||
ALLOW_DIRTY="${ALLOW_DIRTY:-0}"
|
ALLOW_DIRTY="${ALLOW_DIRTY:-0}" # 1 = Dirty-Working-Tree zulassen
|
||||||
|
|
||||||
STATE_DIR="/var/lib/mailwolt/update"
|
# npm / CI Defaults für weniger Lärm
|
||||||
LOCK_FILE="/var/run/mailwolt-update.lock"
|
|
||||||
LOG_FILE="/var/log/mailwolt-update.log"
|
|
||||||
|
|
||||||
# npm / CI Defaults
|
|
||||||
export CI=1
|
export CI=1
|
||||||
export NPM_CONFIG_FUND=false
|
export NPM_CONFIG_FUND=false
|
||||||
export NPM_CONFIG_AUDIT=false
|
export NPM_CONFIG_AUDIT=false
|
||||||
|
|
@ -72,16 +68,12 @@ get_version(){
|
||||||
as_app "cd ${APP_DIR} && (git describe --tags --always 2>/dev/null || git rev-parse --short=7 HEAD)"
|
as_app "cd ${APP_DIR} && (git describe --tags --always 2>/dev/null || git rev-parse --short=7 HEAD)"
|
||||||
}
|
}
|
||||||
|
|
||||||
write_version_files(){
|
write_build_info(){
|
||||||
local ver="$1" rev="$2"
|
local ver="$1" rev="$2"
|
||||||
install -d -m 0755 /etc/mailwolt /var/lib/mailwolt || true
|
install -d -m 0755 /etc/mailwolt || true
|
||||||
printf "version=%s\nrev=%s\nupdated=%s\n" "$ver" "$rev" "$(date -Is)" > /etc/mailwolt/build.info || true
|
printf "version=%s\nrev=%s\nupdated=%s\n" "$ver" "$rev" "$(date -Is)" > /etc/mailwolt/build.info || true
|
||||||
printf '%s\n' "${ver#v}" > /var/lib/mailwolt/version || true
|
|
||||||
printf '%s\n' "$ver" > /var/lib/mailwolt/version_raw || true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
write_build_info(){ write_version_files "$@"; }
|
|
||||||
|
|
||||||
# --- Frontend build (quiet & robust) ------------------------------------------
|
# --- Frontend build (quiet & robust) ------------------------------------------
|
||||||
frontend_build_quiet() {
|
frontend_build_quiet() {
|
||||||
local LOG="/var/log/mailwolt-frontend-build.log"
|
local LOG="/var/log/mailwolt-frontend-build.log"
|
||||||
|
|
@ -132,35 +124,6 @@ frontend_build_quiet() {
|
||||||
[[ "$(id -u)" -eq 0 ]] || { echo "[!] Bitte als root ausführen"; exit 1; }
|
[[ "$(id -u)" -eq 0 ]] || { echo "[!] Bitte als root ausführen"; exit 1; }
|
||||||
[[ -d "$APP_DIR/.git" ]] || { echo "[!] $APP_DIR scheint kein Git-Repo zu sein"; exit 1; }
|
[[ -d "$APP_DIR/.git" ]] || { echo "[!] $APP_DIR scheint kein Git-Repo zu sein"; exit 1; }
|
||||||
|
|
||||||
# -------- Lock ----------------------------------------------------------------
|
|
||||||
mkdir -p "$STATE_DIR"
|
|
||||||
if [[ -f "$LOCK_FILE" ]]; then
|
|
||||||
LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null || true)
|
|
||||||
if [[ -n "$LOCK_PID" ]] && kill -0 "$LOCK_PID" 2>/dev/null; then
|
|
||||||
echo "[!] Update läuft bereits (PID $LOCK_PID) — Abbruch."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rm -f "$LOCK_FILE"
|
|
||||||
fi
|
|
||||||
echo $$ > "$LOCK_FILE"
|
|
||||||
|
|
||||||
# -------- State & Cleanup-Trap -----------------------------------------------
|
|
||||||
MAINTENANCE_ACTIVE=0
|
|
||||||
_cleanup() {
|
|
||||||
local rc=$?
|
|
||||||
[[ "$MAINTENANCE_ACTIVE" -eq 1 ]] && artisan_up
|
|
||||||
mkdir -p "$STATE_DIR"
|
|
||||||
echo "done" > "$STATE_DIR/state"
|
|
||||||
echo "$rc" > "$STATE_DIR/rc"
|
|
||||||
rm -f "$LOCK_FILE"
|
|
||||||
}
|
|
||||||
trap '_cleanup' EXIT INT TERM
|
|
||||||
|
|
||||||
# Update als laufend markieren
|
|
||||||
echo "running" > "$STATE_DIR/state"
|
|
||||||
rm -f "$STATE_DIR/rc"
|
|
||||||
: > "$LOG_FILE"
|
|
||||||
|
|
||||||
git_safe
|
git_safe
|
||||||
git_dirty_check
|
git_dirty_check
|
||||||
|
|
||||||
|
|
@ -173,7 +136,7 @@ NEW_REV="$OLD_REV"
|
||||||
if [[ "$MODE" = "tags" ]]; then
|
if [[ "$MODE" = "tags" ]]; then
|
||||||
# → Neueste Tags holen
|
# → Neueste Tags holen
|
||||||
as_app "git -C ${APP_DIR} fetch --quiet origin && git -C ${APP_DIR} fetch --tags --quiet origin || true"
|
as_app "git -C ${APP_DIR} fetch --quiet origin && git -C ${APP_DIR} fetch --tags --quiet origin || true"
|
||||||
LATEST_TAG="$(as_app "git -C ${APP_DIR} describe --tags --abbrev=0 \$(git -C ${APP_DIR} rev-list --tags --max-count=1 2>/dev/null) 2>/dev/null || echo ''")"
|
LATEST_TAG="$(as_app "git -C ${APP_DIR} tag --list | sort -V | tail -n1")"
|
||||||
if [[ -z "$LATEST_TAG" ]]; then
|
if [[ -z "$LATEST_TAG" ]]; then
|
||||||
echo "[!] Keine Tags gefunden – falle auf origin/${BRANCH} zurück"
|
echo "[!] Keine Tags gefunden – falle auf origin/${BRANCH} zurück"
|
||||||
as_app "git -C ${APP_DIR} checkout -q ${BRANCH} && git -C ${APP_DIR} pull --ff-only origin ${BRANCH}"
|
as_app "git -C ${APP_DIR} checkout -q ${BRANCH} && git -C ${APP_DIR} pull --ff-only origin ${BRANCH}"
|
||||||
|
|
@ -246,6 +209,15 @@ if [[ $NEED_FRONTEND -eq 1 ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# -------- Wartungsmodus: Migrations + Cache + PHP-FPM reload -----------------
|
# -------- Wartungsmodus: Migrations + Cache + PHP-FPM reload -----------------
|
||||||
|
# Trap stellt sicher dass artisan up auch bei Fehler läuft
|
||||||
|
MAINTENANCE_ACTIVE=0
|
||||||
|
cleanup_maintenance(){
|
||||||
|
if [[ "$MAINTENANCE_ACTIVE" -eq 1 ]]; then
|
||||||
|
artisan_up
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup_maintenance EXIT INT TERM
|
||||||
|
|
||||||
if [[ $NEED_MIGRATIONS -eq 1 || $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 ]]; then
|
if [[ $NEED_MIGRATIONS -eq 1 || $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 ]]; then
|
||||||
artisan_down
|
artisan_down
|
||||||
MAINTENANCE_ACTIVE=1
|
MAINTENANCE_ACTIVE=1
|
||||||
|
|
@ -282,8 +254,8 @@ if [[ "$MAINTENANCE_ACTIVE" -eq 1 ]]; then
|
||||||
MAINTENANCE_ACTIVE=0
|
MAINTENANCE_ACTIVE=0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# -------- Version + Build-Info ablegen ----------------------------------------
|
# -------- Build-Info ablegen --------------------------------------------------
|
||||||
NEW_VER="$(get_version)"
|
NEW_VER="$(get_version)"
|
||||||
write_version_files "$NEW_VER" "$NEW_REV"
|
write_build_info "$NEW_VER" "$NEW_REV"
|
||||||
|
|
||||||
echo "[✓] Update abgeschlossen: ${OLD_VER} → ${NEW_VER} (${OLD_REV:0:7} → ${NEW_REV:0:7})"
|
echo "[✓] Update abgeschlossen: ${OLD_REV:0:7} → ${NEW_REV:0:7} (Version: ${NEW_VER})"
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MSG=""
|
||||||
|
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)"
|
||||||
|
REMOTE="${REMOTE:-origin}"
|
||||||
|
|
||||||
|
usage(){ echo "Usage: $0 -m \"commit message\" [-b branch]"; exit 1; }
|
||||||
|
|
||||||
|
while getopts ":m:b:" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
m) MSG="$OPTARG" ;;
|
||||||
|
b) BRANCH="$OPTARG" ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
shift $((OPTIND-1))
|
||||||
|
|
||||||
|
[[ -n "$MSG" ]] || usage
|
||||||
|
git rev-parse --git-dir >/dev/null 2>&1 || { echo "[x] kein Git-Repo"; exit 1; }
|
||||||
|
|
||||||
|
echo "[i] Add/Commit → $BRANCH"
|
||||||
|
git add -A
|
||||||
|
# Kein Commit, wenn nichts geändert ist
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
git commit -m "$MSG"
|
||||||
|
else
|
||||||
|
echo "[i] Nichts zu committen (Index leer) – pushe nur."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[i] Push → $REMOTE/$BRANCH"
|
||||||
|
git push "$REMOTE" "$BRANCH"
|
||||||
|
echo "[✓] Done."
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REMOTE="${REMOTE:-origin}"
|
||||||
|
BRANCH="${BRANCH:-main}"
|
||||||
|
ALLOW_DIRTY=0
|
||||||
|
VERSION=""
|
||||||
|
MSG=""
|
||||||
|
|
||||||
|
usage(){
|
||||||
|
echo "Usage: $0 <X.Y.Z> [-m \"release notes\"] [-b branch] [--allow-dirty]"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Args
|
||||||
|
[[ $# -ge 1 ]] || usage
|
||||||
|
VERSION="$1"; shift || true
|
||||||
|
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "[x] Ungültige Version: $VERSION (erwarte SemVer X.Y.Z)"; exit 1; }
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-m) MSG="$2"; shift 2 ;;
|
||||||
|
-b) BRANCH="$2"; shift 2 ;;
|
||||||
|
--allow-dirty) ALLOW_DIRTY=1; shift ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
git rev-parse --git-dir >/dev/null 2>&1 || { echo "[x] kein Git-Repo"; exit 1; }
|
||||||
|
|
||||||
|
# Clean tree?
|
||||||
|
if [[ $ALLOW_DIRTY -eq 0 ]]; then
|
||||||
|
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||||
|
echo "[x] Arbeitsbaum nicht sauber. Committe oder nutze --allow-dirty."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aktuellen Stand holen
|
||||||
|
git fetch --quiet "$REMOTE" --tags
|
||||||
|
# Branch check-out/sync
|
||||||
|
git checkout -q "$BRANCH"
|
||||||
|
git pull --ff-only "$REMOTE" "$BRANCH"
|
||||||
|
|
||||||
|
TAG="v${VERSION}"
|
||||||
|
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
||||||
|
echo "[x] Tag ${TAG} existiert bereits."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)"
|
||||||
|
if [[ -n "$LAST_TAG" ]]; then
|
||||||
|
CHANGELOG="$(git log --pretty=format:'- %s (%h)' "${LAST_TAG}..HEAD" || true)"
|
||||||
|
else
|
||||||
|
CHANGELOG="$(git log --pretty=format:'- %s (%h)' || true)"
|
||||||
|
fi
|
||||||
|
[[ -n "$CHANGELOG" ]] || CHANGELOG="- initial release content"
|
||||||
|
|
||||||
|
# VERSION bumpen
|
||||||
|
echo -n "$VERSION" > VERSION
|
||||||
|
git add VERSION
|
||||||
|
git commit -m "chore(release): v${VERSION}"
|
||||||
|
|
||||||
|
# Tag-Message bauen
|
||||||
|
if [[ -z "$MSG" ]]; then
|
||||||
|
read -r -d '' MSG <<EOF || true
|
||||||
|
MailWolt v${VERSION}
|
||||||
|
|
||||||
|
Changes since ${LAST_TAG:-repo start}:
|
||||||
|
${CHANGELOG}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
git tag -a "$TAG" -m "$MSG"
|
||||||
|
|
||||||
|
# Push commit + tag
|
||||||
|
git push "$REMOTE" "$BRANCH"
|
||||||
|
git push "$REMOTE" "$TAG"
|
||||||
|
|
||||||
|
echo "[✓] Release ${TAG} erstellt & gepusht."
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"dev": "bash -c 'trap \"rm -f public/hot\" EXIT INT TERM; rm -f public/hot && vite'"
|
"dev": "vite"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
@import "../fonts/Space/font.css";
|
@import "../fonts/Space/font.css";
|
||||||
|
|
||||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||||
@source '../../vendor/wire-elements/modal/resources/views/*.blade.php';
|
|
||||||
@source '../../storage/framework/views/*.php';
|
@source '../../storage/framework/views/*.php';
|
||||||
@source '../**/*.blade.php';
|
@source '../**/*.blade.php';
|
||||||
@source '../**/*.js';
|
@source '../**/*.js';
|
||||||
|
|
@ -100,19 +99,6 @@
|
||||||
.mw-divider {
|
.mw-divider {
|
||||||
@apply border-t border-white/10 my-4;
|
@apply border-t border-white/10 my-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Header utility classes ── */
|
|
||||||
.btn-ghost {
|
|
||||||
@apply inline-flex items-center justify-center rounded-lg
|
|
||||||
text-white/60 hover:text-white hover:bg-white/5
|
|
||||||
transition-colors duration-150 cursor-pointer;
|
|
||||||
padding: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup {
|
|
||||||
@apply rounded-xl border border-white/10 shadow-xl z-50
|
|
||||||
bg-[#0f172a]/95 backdrop-blur-xl text-white/80;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============ BOOT-STATE: keine Sprünge ============ */
|
/* ============ BOOT-STATE: keine Sprünge ============ */
|
||||||
|
|
@ -1107,125 +1093,40 @@ select {
|
||||||
/* ── Main ── */
|
/* ── Main ── */
|
||||||
.mw-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
|
.mw-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
|
||||||
|
|
||||||
/* ════════════════════════════════════════
|
/* ── Topbar ── */
|
||||||
Topbar — Redesign
|
|
||||||
════════════════════════════════════════ */
|
|
||||||
.mw-topbar {
|
.mw-topbar {
|
||||||
height: 54px;
|
height: 50px; padding: 0 22px; border-bottom: 1px solid var(--mw-b1);
|
||||||
padding: 0 20px;
|
background: var(--mw-bg); display: flex; align-items: center;
|
||||||
border-bottom: 1px solid var(--mw-b1);
|
justify-content: space-between; flex-shrink: 0;
|
||||||
background: var(--mw-bg);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-shrink: 0;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
.mw-breadcrumb { display: flex; align-items: center; gap: 5px; font-size: 13px; color: var(--mw-t4); }
|
||||||
|
.mw-breadcrumb b { color: var(--mw-t2); font-weight: 500; }
|
||||||
|
.mw-topbar-right { display: flex; align-items: center; gap: 8px; }
|
||||||
|
|
||||||
/* ── Links ── */
|
.mw-live { display: flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--mw-t3); }
|
||||||
.mw-tb-left {
|
.mw-live-dot {
|
||||||
display: flex;
|
width: 6px; height: 6px; border-radius: 50%; background: var(--mw-gr);
|
||||||
align-items: center;
|
box-shadow: 0 0 5px rgba(34,197,94,.5); animation: mw-pulse 2s infinite;
|
||||||
gap: 10px;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
@keyframes mw-pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
|
||||||
|
|
||||||
.mw-hamburger {
|
.mw-search {
|
||||||
display: none;
|
display: flex; align-items: center; gap: 6px; background: var(--mw-bg3);
|
||||||
flex-shrink: 0;
|
border: 1px solid var(--mw-b1); border-radius: 6px; padding: 5px 10px;
|
||||||
align-items: center;
|
font-size: 12px; color: var(--mw-t4); cursor: pointer;
|
||||||
justify-content: center;
|
|
||||||
width: 32px; height: 32px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--mw-t3);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background .15s, color .15s;
|
|
||||||
}
|
}
|
||||||
.mw-hamburger:hover { background: var(--mw-bg3); color: var(--mw-t1); }
|
.mw-search kbd { font-size: 9.5px; opacity: .5; margin-left: 2px; }
|
||||||
|
.mw-ip {
|
||||||
.mw-tb-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.mw-tb-parent { color: var(--mw-t4); white-space: nowrap; flex-shrink: 0; }
|
|
||||||
.mw-tb-sep { color: var(--mw-b3); flex-shrink: 0; }
|
|
||||||
.mw-tb-current { color: var(--mw-t2); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
|
|
||||||
/* ── Rechts ── */
|
|
||||||
.mw-tb-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Live-Dot */
|
|
||||||
.mw-tb-live {
|
|
||||||
display: flex; align-items: center; gap: 5px;
|
|
||||||
padding: 4px 9px;
|
|
||||||
border-radius: 99px;
|
|
||||||
background: rgba(34,197,94,.08);
|
|
||||||
border: 1px solid rgba(34,197,94,.18);
|
|
||||||
font-size: 11px; font-weight: 500; color: #4ade80;
|
|
||||||
}
|
|
||||||
.mw-tb-live-dot {
|
|
||||||
width: 6px; height: 6px; border-radius: 50%;
|
|
||||||
background: #22c55e;
|
|
||||||
box-shadow: 0 0 6px rgba(34,197,94,.7);
|
|
||||||
animation: mw-pulse 2s infinite;
|
|
||||||
}
|
|
||||||
@keyframes mw-pulse { 0%,100%{opacity:1} 50%{opacity:.35} }
|
|
||||||
|
|
||||||
/* Suchbutton */
|
|
||||||
.mw-tb-search {
|
|
||||||
display: flex; align-items: center; gap: 7px;
|
|
||||||
height: 32px; padding: 0 12px;
|
|
||||||
background: var(--mw-bg3);
|
|
||||||
border: 1px solid var(--mw-b2);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 12px; color: var(--mw-t4);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color .15s, color .15s;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.mw-tb-search:hover { border-color: var(--mw-b3); color: var(--mw-t2); }
|
|
||||||
.mw-tb-kbd {
|
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
|
||||||
padding: 2px 5px;
|
|
||||||
background: var(--mw-b1); border: 1px solid var(--mw-b2);
|
|
||||||
border-radius: 4px; font-size: 10px; color: var(--mw-t5);
|
|
||||||
font-family: ui-monospace, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hostname-Badge */
|
|
||||||
.mw-tb-host {
|
|
||||||
display: flex; align-items: center; gap: 5px;
|
|
||||||
height: 32px; padding: 0 10px;
|
|
||||||
background: var(--mw-bg3); border: 1px solid var(--mw-b2);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-family: ui-monospace, monospace; font-size: 11px; color: var(--mw-t4);
|
font-family: ui-monospace, monospace; font-size: 11px; color: var(--mw-t4);
|
||||||
white-space: nowrap;
|
background: var(--mw-bg3); border: 1px solid var(--mw-b1); border-radius: 5px; padding: 4px 9px;
|
||||||
}
|
}
|
||||||
|
.mw-btn-primary {
|
||||||
/* + Domain Button */
|
display: inline-flex; align-items: center; gap: 5px;
|
||||||
.mw-tb-add {
|
background: var(--mw-v); border: none; border-radius: 6px; padding: 6px 14px;
|
||||||
display: flex; align-items: center; gap: 6px;
|
font-size: 12.5px; font-weight: 600; color: #fff; cursor: pointer;
|
||||||
height: 32px; padding: 0 14px;
|
box-shadow: 0 0 10px rgba(124,58,237,.35);
|
||||||
background: var(--mw-v);
|
|
||||||
border: none; border-radius: 8px;
|
|
||||||
font-size: 12.5px; font-weight: 600; color: #fff;
|
|
||||||
cursor: pointer; flex-shrink: 0;
|
|
||||||
box-shadow: 0 2px 12px rgba(124,58,237,.35);
|
|
||||||
transition: background .15s, box-shadow .15s;
|
|
||||||
}
|
}
|
||||||
.mw-tb-add:hover { background: #6d28d9; box-shadow: 0 2px 16px rgba(124,58,237,.5); }
|
.mw-btn-primary:hover { background: #6d28d9; }
|
||||||
|
|
||||||
/* ── Content ── */
|
/* ── Content ── */
|
||||||
.mw-content { flex: 1; overflow-y: auto; padding: 20px 22px 30px; }
|
.mw-content { flex: 1; overflow-y: auto; padding: 20px 22px 30px; }
|
||||||
|
|
@ -1445,16 +1346,13 @@ select {
|
||||||
/* ── Responsive ── */
|
/* ── Responsive ── */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.mw-content { padding: 16px 18px 24px; }
|
.mw-content { padding: 16px 18px 24px; }
|
||||||
.mw-topbar { padding: 0 16px; }
|
.mw-topbar { padding: 0 18px; }
|
||||||
}
|
}
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.mw-bottom-grid { grid-template-columns: 1fr; }
|
.mw-bottom-grid { grid-template-columns: 1fr; }
|
||||||
.mw-right-col { flex-direction: row; }
|
.mw-right-col { flex-direction: row; }
|
||||||
/* Hostname auf Tablet kompakter */
|
|
||||||
.mw-tb-host { font-size: 10.5px; padding: 0 8px; }
|
|
||||||
}
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* Sidebar off-canvas */
|
|
||||||
.mw-sidebar {
|
.mw-sidebar {
|
||||||
position: fixed; left: 0; top: 0; z-index: 50;
|
position: fixed; left: 0; top: 0; z-index: 50;
|
||||||
transform: translateX(-100%); transition: transform .25s;
|
transform: translateX(-100%); transition: transform .25s;
|
||||||
|
|
@ -1465,10 +1363,10 @@ select {
|
||||||
background: rgba(0,0,0,.55); z-index: 40;
|
background: rgba(0,0,0,.55); z-index: 40;
|
||||||
}
|
}
|
||||||
.mw-sidebar-overlay.open { display: block; }
|
.mw-sidebar-overlay.open { display: block; }
|
||||||
/* Hamburger einblenden */
|
.mw-hamburger {
|
||||||
.mw-hamburger { display: flex; }
|
display: flex !important; background: none; border: none;
|
||||||
/* Parent bleibt sichtbar (3 Buchstaben) */
|
color: var(--mw-t3); cursor: pointer; padding: 4px; margin-right: 4px;
|
||||||
/* Layout */
|
}
|
||||||
.mw-metric-grid { grid-template-columns: repeat(2, 1fr); }
|
.mw-metric-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
.mw-status-grid { grid-template-columns: repeat(2, 1fr); }
|
.mw-status-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
.mw-shell { display: block; height: 100vh; }
|
.mw-shell { display: block; height: 100vh; }
|
||||||
|
|
@ -1478,176 +1376,7 @@ select {
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.mw-metric-grid { grid-template-columns: 1fr; }
|
.mw-metric-grid { grid-template-columns: 1fr; }
|
||||||
.mw-status-grid { grid-template-columns: 1fr; }
|
.mw-status-grid { grid-template-columns: 1fr; }
|
||||||
.mw-topbar { padding: 0 12px; gap: 5px; }
|
.mw-ip { display: none; }
|
||||||
.mw-tb-parent { display: none; }
|
|
||||||
.mw-tb-sep { display: none; }
|
|
||||||
.mw-tb-search-text { display: none; }
|
|
||||||
.mw-tb-kbd { display: none; }
|
|
||||||
.mw-tb-search { padding: 0 9px; }
|
|
||||||
.mw-tb-add-text { display: none; }
|
|
||||||
.mw-tb-add { padding: 0 9px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── API Key Layout ── */
|
|
||||||
.mw-apikey-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 340px;
|
|
||||||
gap: 14px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
.mw-apikey-layout > * { min-width: 0; }
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.mw-apikey-layout { grid-template-columns: 1fr; }
|
|
||||||
}
|
|
||||||
/* ── Responsive list helpers ── */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.mw-col-hide-sm { display: none !important; }
|
|
||||||
.mw-only-desktop { display: none !important; }
|
|
||||||
}
|
|
||||||
@media (min-width: 641px) {
|
|
||||||
.mw-only-mobile { display: none !important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── API Key div-list ── */
|
|
||||||
.mw-kl-head,
|
|
||||||
.mw-kl-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 104px 80px 90px 40px;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.mw-kl-head {
|
|
||||||
font-size: 9.5px; font-weight: 600; text-transform: uppercase;
|
|
||||||
letter-spacing: .5px; color: var(--mw-t4);
|
|
||||||
border-bottom: 1px solid var(--mw-b1);
|
|
||||||
}
|
|
||||||
.mw-kl-row {
|
|
||||||
border-bottom: 1px solid var(--mw-b1);
|
|
||||||
transition: background .1s;
|
|
||||||
}
|
|
||||||
.mw-kl-row:last-child { border-bottom: none; }
|
|
||||||
.mw-kl-row:hover { background: rgba(255,255,255,.015); }
|
|
||||||
.mw-kl-row > * { min-width: 0; overflow: hidden; }
|
|
||||||
.mw-kl-date-inline { display: none; }
|
|
||||||
.mw-kl-head span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
||||||
|
|
||||||
@media (max-width: 900px) and (min-width: 641px) {
|
|
||||||
.mw-kl-head,
|
|
||||||
.mw-kl-row { grid-template-columns: minmax(0, 1fr) 120px 80px 40px; }
|
|
||||||
.mw-kl-date { display: none; }
|
|
||||||
.mw-kl-date-inline { display: inline; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.mw-kl-head { display: none; }
|
|
||||||
.mw-kl-row {
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
grid-template-rows: auto auto;
|
|
||||||
gap: 5px 8px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
.mw-kl-name { grid-column: 1; grid-row: 1; }
|
|
||||||
.mw-kl-scopes { grid-column: 1; grid-row: 2; flex-wrap: wrap; overflow: visible; }
|
|
||||||
.mw-kl-modus { grid-column: 2; grid-row: 1; }
|
|
||||||
.mw-kl-date { display: none; }
|
|
||||||
.mw-kl-date-inline { display: inline; }
|
|
||||||
.mw-kl-actions { grid-column: 2; grid-row: 2; justify-self: end; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Webhook div-list ── */
|
|
||||||
.mw-whl-head,
|
|
||||||
.mw-whl-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 124px 72px 54px 82px;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.mw-whl-head {
|
|
||||||
font-size: 9.5px; font-weight: 600; text-transform: uppercase;
|
|
||||||
letter-spacing: .5px; color: var(--mw-t4);
|
|
||||||
border-bottom: 1px solid var(--mw-b1);
|
|
||||||
}
|
|
||||||
.mw-whl-row {
|
|
||||||
border-bottom: 1px solid var(--mw-b1);
|
|
||||||
transition: background .1s;
|
|
||||||
}
|
|
||||||
.mw-whl-row:last-child { border-bottom: none; }
|
|
||||||
.mw-whl-row:hover { background: rgba(255,255,255,.015); }
|
|
||||||
.mw-whl-row > * { min-width: 0; }
|
|
||||||
.mw-whl-status-sm, .mw-whl-http-sm { display: none; }
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.mw-whl-head { display: none; }
|
|
||||||
.mw-whl-row {
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
grid-template-rows: auto auto;
|
|
||||||
gap: 5px 8px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
.mw-whl-webhook { grid-column: 1; grid-row: 1; }
|
|
||||||
.mw-whl-events { grid-column: 1; grid-row: 2; }
|
|
||||||
.mw-whl-status { display: none; }
|
|
||||||
.mw-whl-http { display: none; }
|
|
||||||
.mw-whl-date { display: none; }
|
|
||||||
.mw-whl-actions {
|
|
||||||
grid-column: 2; grid-row: 1 / 3;
|
|
||||||
display: flex; flex-direction: column; gap: 4px; align-self: start;
|
|
||||||
}
|
|
||||||
.mw-whl-status-sm, .mw-whl-http-sm { display: inline; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Webhook-Tabelle Layout ── */
|
|
||||||
.mw-wh-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 300px;
|
|
||||||
gap: 14px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
.mw-wh-layout > * { min-width: 0; }
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.mw-wh-layout { grid-template-columns: 1fr; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Sandbox Layout ── */
|
|
||||||
.mw-sandbox-client {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 300px 1fr;
|
|
||||||
gap: 0;
|
|
||||||
border: 1px solid var(--mw-b1);
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--mw-bg2);
|
|
||||||
min-height: 520px;
|
|
||||||
}
|
|
||||||
.mw-sandbox-list {
|
|
||||||
border-right: 1px solid var(--mw-b1);
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 680px;
|
|
||||||
}
|
|
||||||
.mw-sandbox-detail {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 680px;
|
|
||||||
min-height: 400px;
|
|
||||||
}
|
|
||||||
.mw-postfix-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 16px 18px;
|
|
||||||
}
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.mw-sandbox-client { grid-template-columns: 1fr; }
|
|
||||||
.mw-sandbox-list { border-right: none; border-bottom: 1px solid var(--mw-b1); max-height: 260px; }
|
|
||||||
.mw-sandbox-detail { max-height: none; }
|
|
||||||
.mw-postfix-grid { grid-template-columns: 1fr; }
|
|
||||||
}
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.mw-sandbox-client { min-height: 0; }
|
|
||||||
.mw-postfix-grid { grid-template-columns: 1fr; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mw-svc-status-off { color: var(--mw-rd) !important; opacity: 1; }
|
.mw-svc-status-off { color: var(--mw-rd) !important; opacity: 1; }
|
||||||
|
|
@ -2014,20 +1743,6 @@ textarea.mw-modal-input { height: auto; line-height: 1.55; padding: 8px 10px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.mw-btn-primary {
|
|
||||||
display: inline-flex; align-items: center; gap: 6px;
|
|
||||||
padding: 6px 16px; border-radius: 7px; font-size: 13px; font-weight: 500; cursor: pointer;
|
|
||||||
background: var(--mw-v); border: 1px solid var(--mw-v); color: #fff;
|
|
||||||
box-shadow: 0 2px 8px rgba(124,58,237,.3);
|
|
||||||
}
|
|
||||||
.mw-btn-primary:hover { background: #6d28d9; border-color: #6d28d9; }
|
|
||||||
.mw-btn-secondary {
|
|
||||||
display: inline-flex; align-items: center; gap: 6px;
|
|
||||||
padding: 6px 14px; border-radius: 7px; font-size: 13px; font-weight: 500; cursor: pointer;
|
|
||||||
background: var(--mw-bg3); border: 1px solid var(--mw-b2); color: var(--mw-t2);
|
|
||||||
transition: background .15s, border-color .15s;
|
|
||||||
}
|
|
||||||
.mw-btn-secondary:hover { background: var(--mw-bg4); border-color: var(--mw-b3); color: var(--mw-t1); }
|
|
||||||
.mw-btn-cancel {
|
.mw-btn-cancel {
|
||||||
display: inline-flex; align-items: center; gap: 5px;
|
display: inline-flex; align-items: center; gap: 5px;
|
||||||
padding: 6px 14px; border-radius: 7px; font-size: 13px; cursor: pointer;
|
padding: 6px 14px; border-radius: 7px; font-size: 13px; cursor: pointer;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import axios from 'axios';
|
|
||||||
window.axios = axios;
|
|
||||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
|
||||||
|
|
||||||
import '@tailwindplus/elements';
|
|
||||||
import "@phosphor-icons/web/duotone";
|
|
||||||
import "@phosphor-icons/web/light";
|
|
||||||
import "@phosphor-icons/web/regular";
|
|
||||||
import "@phosphor-icons/web/bold";
|
|
||||||
|
|
||||||
import $ from "jquery";
|
|
||||||
window.$ = $;
|
|
||||||
window.jQuery = $;
|
|
||||||
|
|
||||||
import './plugins/GlassToastra/toastra.glass.js';
|
|
||||||
import './plugins/GlassToastra/livewire-adapter';
|
|
||||||
import './utils/events.js';
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import Echo from 'laravel-echo';
|
import Echo from 'laravel-echo';
|
||||||
|
|
||||||
import Pusher from 'pusher-js';
|
import Pusher from 'pusher-js';
|
||||||
|
|
||||||
const reverbKey = import.meta.env.VITE_REVERB_APP_KEY;
|
|
||||||
|
|
||||||
if (reverbKey) {
|
|
||||||
window.Pusher = Pusher;
|
window.Pusher = Pusher;
|
||||||
|
|
||||||
window.Echo = new Echo({
|
window.Echo = new Echo({
|
||||||
broadcaster: 'reverb',
|
broadcaster: 'reverb',
|
||||||
key: reverbKey,
|
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||||
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
|
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
|
||||||
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
||||||
|
|
@ -15,6 +13,5 @@ if (reverbKey) {
|
||||||
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
|
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
|
||||||
enabledTransports: ['ws', 'wss'],
|
enabledTransports: ['ws', 'wss'],
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export default window.Echo ?? null;
|
export default Echo;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,77 @@
|
||||||
<!DOCTYPE html>
|
{{-- resources/views/auth/login.blade.php --}}
|
||||||
<html lang="de">
|
@extends('layouts.blank')
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Anmelden · Mailwolt</title>
|
|
||||||
@vite(['resources/css/app.css'])
|
|
||||||
@livewireStyles
|
|
||||||
</head>
|
|
||||||
<body style="margin:0;background:var(--mw-bg);color:var(--mw-t1);font-family:-apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;font-size:13.5px;-webkit-font-smoothing:antialiased;min-height:100vh;display:flex;align-items:center;justify-content:center;
|
|
||||||
background-image:radial-gradient(900px 600px at 15% 0%,rgba(99,102,241,.07),transparent),radial-gradient(700px 500px at 85% 100%,rgba(59,130,246,.05),transparent)">
|
|
||||||
|
|
||||||
|
@section('title', 'Login')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex items-center justify-center p-6 w-full">
|
||||||
<livewire:auth.login-form />
|
<livewire:auth.login-form />
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
{{-- resources/views/auth/login.blade.php --}}
|
||||||
|
{{--@extends('layouts.app')--}}
|
||||||
|
|
||||||
@livewireScripts
|
{{--@section('title', 'Login')--}}
|
||||||
</body>
|
|
||||||
</html>
|
{{--@section('content')--}}
|
||||||
|
{{-- <div class="min-h-[86vh] grid place-items-center px-4--}}
|
||||||
|
{{-- bg-[radial-gradient(1200px_600px_at_10%_-10%,rgba(59,130,246,.08),transparent),--}}
|
||||||
|
{{-- radial-gradient(900px_500px_at_90%_0%,rgba(99,102,241,.06),transparent)]">--}}
|
||||||
|
|
||||||
|
{{-- <div class="nx-card w-full max-w-[520px]">--}}
|
||||||
|
{{-- --}}{{-- Header-Chip + Icon --}}
|
||||||
|
{{-- <div class="flex items-center justify-between mb-5">--}}
|
||||||
|
{{-- <span class="nx-chip">Erster Login</span>--}}
|
||||||
|
{{-- <i class="ph ph-lock-simple text-white/60"></i>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- <p class="nx-subtle mb-7">--}}
|
||||||
|
{{-- Melde dich mit dem einmaligen Bootstrap-Konto an, um den Setup-Wizard zu starten.--}}
|
||||||
|
{{-- </p>--}}
|
||||||
|
|
||||||
|
{{-- --}}{{-- Fehler (optional) --}}
|
||||||
|
{{-- @if(session('error'))--}}
|
||||||
|
{{-- <div class="nx-alert mb-6">--}}
|
||||||
|
{{-- <i class="ph ph-warning-circle text-rose-300"></i>--}}
|
||||||
|
{{-- <div>--}}
|
||||||
|
{{-- <p class="font-medium">Anmeldung fehlgeschlagen</p>--}}
|
||||||
|
{{-- <p class="text-sm/5 text-rose-200/90">{{ session('error') }}</p>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
|
|
||||||
|
{{-- --}}{{-- Formular --}}
|
||||||
|
{{-- <form method="POST" action="{{ route('login') }}" class="Space-y-5">--}}
|
||||||
|
{{-- @csrf--}}
|
||||||
|
|
||||||
|
{{-- <label class="nx-label" for="email">E-Mail</label>--}}
|
||||||
|
{{-- <input id="email" name="email" type="email" autocomplete="username" autofocus--}}
|
||||||
|
{{-- class="nx-input" value="{{ old('email') }}"/>--}}
|
||||||
|
|
||||||
|
{{-- <label class="nx-label" for="password">Passwort</label>--}}
|
||||||
|
{{-- <div class="relative">--}}
|
||||||
|
{{-- <input id="password" name="password" type="password" autocomplete="current-password" class="nx-input pr-10"/>--}}
|
||||||
|
{{-- <button type="button" class="nx-eye" onclick="this.previousElementSibling.type = this.previousElementSibling.type==='password'?'text':'password'">--}}
|
||||||
|
{{-- <i class="ph ph-eye text-white/60"></i>--}}
|
||||||
|
{{-- </button>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- <div class="flex items-center justify-between pt-1">--}}
|
||||||
|
{{-- <label class="inline-flex items-center gap-2 text-sm text-white/70">--}}
|
||||||
|
{{-- <input type="checkbox" name="remember" class="nx-check"> Session merken--}}
|
||||||
|
{{-- </label>--}}
|
||||||
|
{{-- <a class="nx-link" href="#">Zugang zurücksetzen</a>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
|
||||||
|
{{-- <button type="submit" class="nx-btn w-full">Anmelden</button>--}}
|
||||||
|
{{-- </form>--}}
|
||||||
|
|
||||||
|
{{-- --}}{{-- Optional: Provider-Buttons --}}
|
||||||
|
{{-- --}}{{-- <div class="nx-divider">oder verbinden via</div>--}}
|
||||||
|
{{-- <div class="grid grid-cols-2 gap-3 mt-4">--}}
|
||||||
|
{{-- <button class="nx-btn-ghost"><i class="ph ph-google-logo mr-2"></i> Google</button>--}}
|
||||||
|
{{-- <button class="nx-btn-ghost"><i class="ph ph-github-logo mr-2"></i> GitHub</button>--}}
|
||||||
|
{{-- </div> --}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{-- </div>--}}
|
||||||
|
{{--@endsection--}}
|
||||||
|
|
|
||||||
|
|
@ -1,110 +1,97 @@
|
||||||
{{--resources/views/components/partials/header.blade.php--}}
|
{{--resources/views/components/partials/header.blade.php--}}
|
||||||
<div class="w-full border-b hr">
|
<div
|
||||||
|
class="#sticky top-0 w-full border-b hr #rounded-lg max-w-9xl mx-auto">
|
||||||
<header id="header" class="header w-full rounded-r-2xl rounded-l-none">
|
<header id="header" class="header w-full rounded-r-2xl rounded-l-none">
|
||||||
<nav class="flex h-[71px] items-center justify-between gap-3 px-4">
|
<nav class="flex h-[71px] #h-[74px] items-center justify-between #p-3">
|
||||||
|
<div class="#flex-1 md:w-3/5 w-full">
|
||||||
{{-- Links: Toggle + Seitentitel --}}
|
<div class="relative flex items-center gap-5">
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-3">
|
<button class="sidebar-toggle translate-0 right-5 block s#m:hidden text-white/60 hover:text-white text-2xl">
|
||||||
<button class="sidebar-toggle shrink-0 text-white/60 hover:text-white text-2xl">
|
|
||||||
<i class="ph ph-list"></i>
|
<i class="ph ph-list"></i>
|
||||||
</button>
|
</button>
|
||||||
<h1 class="truncate font-bold text-xl sm:text-2xl">@yield('header_title')</h1>
|
<div class="flex items-center gap-3">
|
||||||
|
<h1 class="font-bold text-2xl">@yield('header_title')</h1>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{-- Rechts: Suche + Aktionen --}}
|
</div>
|
||||||
<div class="flex shrink-0 items-center gap-1.5 sm:gap-2">
|
<div class="flex items-center justify-end gap-2 w-full max-w-96">
|
||||||
|
|
||||||
{{-- Suchbutton --}}
|
|
||||||
<button
|
<button
|
||||||
id="openSearchPaletteBtn"
|
id="openSearchPaletteBtn"
|
||||||
type="button"
|
type="button"
|
||||||
onclick="Livewire.dispatch('openModal',{component:'ui.search.modal.search-palette-modal'})"
|
class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1.5
|
||||||
class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-2.5 py-1.5 text-white/75 hover:text-white hover:border-white/20 sm:px-3"
|
text-white/75 hover:text-white hover:border-white/20"
|
||||||
title="Suche öffnen"
|
title="Suche öffnen"
|
||||||
aria-label="Suche öffnen"
|
aria-label="Suche öffnen"
|
||||||
|
wire:click="$dispatch('openModal',{component:'ui.search.modal.search-palette-modal'})"
|
||||||
>
|
>
|
||||||
<i class="ph ph-magnifying-glass text-base"></i>
|
<i class="ph ph-magnifying-glass text-[16px]"></i>
|
||||||
<span id="searchShortcutKbd"
|
<span id="searchShortcutKbd"
|
||||||
class="hidden sm:inline-block rounded-md bg-white/5 border border-white/10 px-1.5 py-0.5 text-[11px] leading-none">
|
class="rounded-md bg-white/5 border border-white/10 px-1.5 py-0.5 text-[11px] leading-none">
|
||||||
|
<!-- wird per JS gesetzt -->
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button onclick="Livewire.dispatch('openModal',{component:'ui.domain.modal.domain-create-modal'})">
|
||||||
{{-- Domain erstellen --}}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick="Livewire.dispatch('openModal',{component:'ui.domain.modal.domain-create-modal'})"
|
|
||||||
class="btn-ghost text-lg !p-2"
|
|
||||||
title="Domain erstellen"
|
|
||||||
>
|
|
||||||
<i class="ph-duotone ph-globe-hemisphere-east"></i>
|
<i class="ph-duotone ph-globe-hemisphere-east"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@foreach($header as $section)
|
@foreach($header as $section)
|
||||||
|
<div class="#modal-button #relative #mr-2">
|
||||||
|
<div class="#p-1.5">
|
||||||
@if(!$section['image'])
|
@if(!$section['image'])
|
||||||
{{-- Benachrichtigungen --}}
|
|
||||||
<div x-data="{ openMessages: false }" class="relative flex items-center">
|
<div x-data="{ openMessages: false }" class="relative flex items-center">
|
||||||
<button
|
<button @click="openMessages = !openMessages" class="btn-ghost text-base !p-2"><i class="ph-duotone ph-bell #mr-1"></i></button>
|
||||||
@click="openMessages = !openMessages"
|
|
||||||
class="btn-ghost text-base !p-2"
|
|
||||||
title="Benachrichtigungen"
|
|
||||||
>
|
|
||||||
<i class="ph-duotone ph-bell"></i>
|
|
||||||
</button>
|
|
||||||
<div
|
<div
|
||||||
x-show="openMessages"
|
x-show="openMessages"
|
||||||
@click.away="openMessages = false"
|
@click.away="openMessages = false"
|
||||||
x-cloak
|
x-cloak
|
||||||
class="popup absolute top-[calc(100%+0.5rem)] right-0 w-56"
|
class="popup absolute top-16 right-0 w-48"
|
||||||
x-transition:enter="transition ease-out duration-100"
|
x-transition:enter="transition ease-out duration-100"
|
||||||
x-transition:enter-start="opacity-0 scale-95"
|
x-transition:enter-start="opacity-0 transform scale-95"
|
||||||
x-transition:enter-end="opacity-100 scale-100"
|
x-transition:enter-end="opacity-100 transform scale-100"
|
||||||
x-transition:leave="transition ease-in duration-75"
|
x-transition:leave="transition ease-in duration-75"
|
||||||
x-transition:leave-start="opacity-100 scale-100"
|
x-transition:leave-start="opacity-100 transform scale-100"
|
||||||
x-transition:leave-end="opacity-0 scale-95"
|
x-transition:leave-end="opacity-0 transform scale-95"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-center h-40 text-sm text-white/50">
|
<div class="flex items-center justify-center h-40">
|
||||||
Keine Nachrichten
|
Keine Nachrichten
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
{{-- Avatar-Menü --}}
|
|
||||||
<div x-data="{ openMenu: false }" class="relative flex items-center">
|
<div x-data="{ openMenu: false }" class="relative flex items-center">
|
||||||
<button
|
<button @click="openMenu = !openMenu"
|
||||||
@click="openMenu = !openMenu"
|
class="flex items-center focus:outline-none">
|
||||||
class="flex items-center gap-1.5 focus:outline-none"
|
<img class="size-8 rounded-full object-cover shadow-2xl"
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="size-8 rounded-full object-cover shadow-2xl"
|
|
||||||
src="https://i.pravatar.cc/100"
|
src="https://i.pravatar.cc/100"
|
||||||
alt="Avatar"
|
alt="Avatar"/>
|
||||||
/>
|
|
||||||
<svg
|
<svg
|
||||||
class="size-4 text-white/40 transition-transform duration-200"
|
class="size-4 ml-2 text-gray-500 transform transition-transform duration-200"
|
||||||
:class="{ 'rotate-180': openMenu }"
|
:class="{'rotate-180': open}"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor">
|
||||||
>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
d="M19 9l-7 7-7-7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
x-show="openMenu"
|
x-show="openMenu"
|
||||||
@click.away="openMenu = false"
|
@click.away="openMenu = false"
|
||||||
x-cloak
|
x-cloak
|
||||||
class="popup absolute top-[calc(100%+0.5rem)] right-0 w-48"
|
class="popup absolute top-16 right-0 w-48"
|
||||||
x-transition:enter="transition ease-out duration-100"
|
x-transition:enter="transition ease-out duration-100"
|
||||||
x-transition:enter-start="opacity-0 scale-95"
|
x-transition:enter-start="opacity-0 transform scale-95"
|
||||||
x-transition:enter-end="opacity-100 scale-100"
|
x-transition:enter-end="opacity-100 transform scale-100"
|
||||||
x-transition:leave="transition ease-in duration-75"
|
x-transition:leave="transition ease-in duration-75"
|
||||||
x-transition:leave-start="opacity-100 scale-100"
|
x-transition:leave-start="opacity-100 transform scale-100"
|
||||||
x-transition:leave-end="opacity-0 scale-95"
|
x-transition:leave-end="opacity-0 transform scale-95"
|
||||||
>
|
>
|
||||||
@foreach($section['sub'] as $sub)
|
@foreach($section['sub'] as $sub)
|
||||||
<a href="{{ route($sub['route']) }}" class="block px-4 py-2 text-xs hover:bg-white/5">
|
<a href="{{ route($sub['route']) }}"
|
||||||
|
class="block px-4 py-2 text-xs #text-gray-700 #hover:bg-gray-100 #hover:text-black #hover:bg-gradient-to-r #hover:from-[rgba(var(--accent))] #hover:to-transparent">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<i class="ph {{ $sub['icon'] ?? 'ph-circle' }} text-sm text-white/50"></i>
|
<span>Logo</span>
|
||||||
|
{{-- <span><x-dynamic-component :component="$sub['icon']"--}}
|
||||||
|
{{-- class="!size-4"/></span>--}}
|
||||||
<span>{{ $sub['title'] }}</span>
|
<span>{{ $sub['title'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -112,8 +99,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -160,43 +160,23 @@
|
||||||
<div class="mw-main">
|
<div class="mw-main">
|
||||||
|
|
||||||
<header class="mw-topbar">
|
<header class="mw-topbar">
|
||||||
|
<div class="mw-breadcrumb">
|
||||||
{{-- Links: Hamburger + Titel --}}
|
<button class="mw-hamburger" style="display:none" onclick="document.getElementById('mw-sidebar').classList.add('open');document.getElementById('mw-overlay').classList.add('open');">
|
||||||
<div class="mw-tb-left">
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M2 4h14M2 9h14M2 14h14" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||||
<button class="mw-hamburger" onclick="document.getElementById('mw-sidebar').classList.add('open');document.getElementById('mw-overlay').classList.add('open');" aria-label="Menü">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M2 8h12M2 12h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
||||||
</button>
|
</button>
|
||||||
<div class="mw-tb-title">
|
@hasSection('breadcrumb-parent')@yield('breadcrumb-parent')@else{{ $breadcrumbParent ?? 'Dashboard' }}@endif
|
||||||
<span class="mw-tb-parent">@hasSection('breadcrumb-parent')@yield('breadcrumb-parent')@else{{ $breadcrumbParent ?? 'Dashboard' }}@endif</span>
|
<svg width="11" height="11" viewBox="0 0 11 11" fill="none"><path d="M3.5 2l4 3.5-4 3.5" stroke="var(--mw-b3)" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||||
<svg class="mw-tb-sep" width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M3 2l3.5 3L3 8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
<b>@hasSection('breadcrumb')@yield('breadcrumb')@else{{ $breadcrumb ?? 'Übersicht' }}@endif</b>
|
||||||
<span class="mw-tb-current">@hasSection('breadcrumb')@yield('breadcrumb')@else{{ $breadcrumb ?? 'Übersicht' }}@endif</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="mw-topbar-right">
|
||||||
|
<div class="mw-live"><div class="mw-live-dot"></div>Live</div>
|
||||||
{{-- Rechts: Actions --}}
|
<button wire:click="$dispatch('openModal',{component:'ui.search.modal.search-palette-modal'})" class="mw-search">
|
||||||
<div class="mw-tb-right">
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><circle cx="5" cy="5" r="3.8" stroke="currentColor" stroke-width="1.2"/><path d="M8 8l2.5 2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||||
|
Suche <kbd>⌘K</kbd>
|
||||||
{{-- Live-Status --}}
|
|
||||||
<div class="mw-tb-live">
|
|
||||||
<span class="mw-tb-live-dot"></span>
|
|
||||||
<span class="mw-tb-live-label">Live</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Suche --}}
|
|
||||||
<button class="mw-tb-search" onclick="Livewire.dispatch('openModal',{component:'ui.search.modal.search-palette-modal'})">
|
|
||||||
<svg width="13" height="13" viewBox="0 0 12 12" fill="none"><circle cx="5" cy="5" r="3.5" stroke="currentColor" stroke-width="1.3"/><path d="M7.8 7.8l2.2 2.2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
|
||||||
<span class="mw-tb-search-text">Suche</span>
|
|
||||||
<kbd class="mw-tb-kbd">⌘K</kbd>
|
|
||||||
</button>
|
</button>
|
||||||
|
<div class="mw-ip">{{ gethostname() ?: '—' }}</div>
|
||||||
{{-- Domain erstellen --}}
|
<button class="mw-btn-primary" onclick="Livewire.dispatch('openModal',{component:'ui.domain.modal.domain-create-modal'})">+ Domain</button>
|
||||||
<button class="mw-tb-add" onclick="Livewire.dispatch('openModal',{component:'ui.domain.modal.domain-create-modal'})">
|
|
||||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none"><path d="M5.5 1v9M1 5.5h9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
|
|
||||||
<span class="mw-tb-add-text">Domain</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="mw-content">
|
<div class="mw-content">
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@vite(['resources/js/app-webmail.js'])
|
@vite(['resources/js/app.js'])
|
||||||
@livewireScripts
|
@livewireScripts
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ function wmOpenSidebar() { document.getElementById('wm-sidebar').classList.add(
|
||||||
function wmCloseSidebar() { document.getElementById('wm-sidebar').classList.remove('open'); document.getElementById('wm-overlay').style.display='none'; }
|
function wmCloseSidebar() { document.getElementById('wm-sidebar').classList.remove('open'); document.getElementById('wm-overlay').style.display='none'; }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@vite(['resources/js/app-webmail.js'])
|
@vite(['resources/js/app.js'])
|
||||||
@livewireScripts
|
@livewireScripts
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,120 @@
|
||||||
<div style="width:100%;max-width:420px;padding:24px 16px">
|
<div class="w-full #min-h-[86vh] grid place-items-center
|
||||||
|
bg-[radial-gradient(1200px_600px_at_10%_-10%,rgba(59,130,246,.08),transparent),
|
||||||
|
radial-gradient(900px_500px_at_90%_0%,rgba(99,102,241,.06),transparent)]">
|
||||||
|
|
||||||
{{-- Logo --}}
|
{{-- Banner (dismissbar) --}}
|
||||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:32px;justify-content:center">
|
|
||||||
<div style="width:32px;height:32px;background:linear-gradient(135deg,#6366f1,#4f46e5);border-radius:8px;display:flex;align-items:center;justify-content:center;box-shadow:0 0 16px rgba(99,102,241,.4)">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 18 18" fill="none">
|
|
||||||
<path d="M9 2L15.5 5.5v7L9 16 2.5 12.5v-7L9 2Z" stroke="white" stroke-width="1.5" stroke-linejoin="round"/>
|
|
||||||
<path d="M9 6L12 7.5v3L9 12 6 10.5v-3L9 6Z" fill="white" opacity=".9"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style="font-size:14px;font-weight:700;letter-spacing:.3px;color:var(--mw-t1)">MAILWOLT</div>
|
|
||||||
<div style="font-size:11px;color:var(--mw-t4);margin-top:-1px">Control Panel</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Setup-Done Banner --}}
|
|
||||||
@if($showBanner && $banner)
|
@if($showBanner && $banner)
|
||||||
<div style="margin-bottom:14px;padding:12px 14px;border-radius:9px;border:1px solid rgba(34,197,94,.25);background:rgba(34,197,94,.07);display:flex;align-items:flex-start;gap:10px">
|
<div class="max-w-[520px] mb-5 rounded-2xl border border-emerald-400/30 text-emerald-300 bg-emerald-500/10 p-4 #md:p-5 shadow backdrop-blur"
|
||||||
<svg width="15" height="15" viewBox="0 0 14 14" fill="none" style="flex-shrink:0;margin-top:1px;color:#4ade80"><path d="M2 7.5l3 3 7-7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
role="status" aria-live="polite">
|
||||||
<div>
|
<div class="flex items-start gap-3">
|
||||||
<div style="font-size:12.5px;font-weight:600;color:rgba(134,239,172,.95)">{{ $banner['title'] ?? 'Einrichtung abgeschlossen' }}</div>
|
<div class="shrink-0 mt-0.5">
|
||||||
@if(!empty($banner['message']))
|
<i class="ph ph-check-circle text-emerald-100 text-xl"></i>
|
||||||
<div style="font-size:11.5px;color:rgba(134,239,172,.7);margin-top:2px">{{ $banner['message'] }}</div>
|
</div>
|
||||||
@endif
|
<div class="flex-1">
|
||||||
|
<div class="font-semibold text-emerald-100">
|
||||||
|
{{ $banner['title'] ?? 'Erfolgreich registriert' }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-0.5 text-sm text-emerald-200">
|
||||||
|
{{ $banner['message'] ?? 'Dein Konto ist bereit. Melde dich jetzt an.' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- Karte --}}
|
<div class="nx-card w-full max-w-[520px]">
|
||||||
<div style="background:var(--mw-bg2);border:1px solid var(--mw-b2);border-radius:14px;padding:28px 24px">
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<span class="nx-chip">Erster Login</span>
|
||||||
<div style="margin-bottom:20px">
|
<i class="ph ph-lock-simple text-white/60"></i>
|
||||||
<div style="font-size:15px;font-weight:600;color:var(--mw-t1)">Anmelden</div>
|
|
||||||
<div style="font-size:12px;color:var(--mw-t4);margin-top:3px">Melde dich mit deinem Account an</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{{-- globale Fehlermeldung --}}
|
||||||
{{-- Fehler --}}
|
|
||||||
@if($error)
|
@if($error)
|
||||||
<div style="margin-bottom:16px;padding:10px 12px;border-radius:8px;border:1px solid rgba(239,68,68,.25);background:rgba(239,68,68,.07);display:flex;align-items:center;gap:8px">
|
<div class="nx-alert mb-6">
|
||||||
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" style="flex-shrink:0;color:#f87171"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.3"/><path d="M7 4.5v3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><circle cx="7" cy="9.5" r=".7" fill="currentColor"/></svg>
|
<i class="ph ph-warning-circle text-rose-300"></i>
|
||||||
<span style="font-size:12px;color:#f87171">{{ $error }}</span>
|
<div>
|
||||||
|
<p class="font-medium">Anmeldung fehlgeschlagen</p>
|
||||||
|
<p class="text-sm/5 text-rose-200/90">{{ $error }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<form wire:submit.prevent="login" style="display:flex;flex-direction:column;gap:16px">
|
<form wire:submit.prevent="login" class="space-y-5">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mw-modal-label">E-Mail / Benutzername</label>
|
<label class="nx-label" for="email">E-Mail / Benutzername</label>
|
||||||
<input type="text"
|
<input id="name" type="name"
|
||||||
wire:model.defer="name"
|
wire:model.defer="name"
|
||||||
autocomplete="username"
|
autocomplete="username" autofocus
|
||||||
autofocus
|
class="nx-input @error('name') input-error @enderror">
|
||||||
class="mw-modal-input @error('name') border-red-500/50 @enderror"
|
@error('name')
|
||||||
placeholder="admin@example.com">
|
<p class="field-error #mt-1">{{ $message }}</p>
|
||||||
@error('name') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mw-modal-label">Passwort</label>
|
<label class="nx-label" for="password">Passwort</label>
|
||||||
<div style="position:relative">
|
<div class="relative">
|
||||||
<input id="lf-password"
|
<input id="password" type="{{ $show ? 'text' : 'password' }}"
|
||||||
type="password"
|
|
||||||
wire:model.defer="password"
|
wire:model.defer="password"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
class="mw-modal-input @error('password') border-red-500/50 @enderror"
|
class="nx-input pr-10 @error('password') input-error @enderror">
|
||||||
style="padding-right:38px"
|
<button type="button" class="nx-eye"
|
||||||
placeholder="••••••••••">
|
onclick="togglePassword('password', this)">
|
||||||
<button type="button"
|
<i class="ph ph-eye text-white/60"></i>
|
||||||
onclick="(function(btn){var i=document.getElementById('lf-password'),s=btn.querySelector('.ico-show'),h=btn.querySelector('.ico-hide');i.type=i.type==='password'?'text':'password';s.style.display=i.type==='text'?'none':'block';h.style.display=i.type==='text'?'block':'none';})(this)"
|
|
||||||
style="position:absolute;right:0;top:0;height:100%;width:36px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;cursor:pointer;color:var(--mw-t4)">
|
|
||||||
<svg class="ico-show" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
||||||
<svg class="ico-hide" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="display:none"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@error('password') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
@error('password')
|
||||||
|
<p class="field-error mt-1">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
<div>
|
||||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
<div class="flex items-center justify-between pt-1">
|
||||||
<input type="checkbox" wire:model.live="remember" style="display:none" id="lf-remember">
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
<span onclick="document.getElementById('lf-remember').click()"
|
<input type="checkbox" wire:model.live="remember" class="peer hidden">
|
||||||
style="width:16px;height:16px;border-radius:4px;border:1.5px solid var(--mw-b2);background:var(--mw-bg3);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .15s;{{ $remember ? 'background:rgba(99,102,241,.2);border-color:rgba(99,102,241,.6)' : '' }}">
|
<span
|
||||||
@if($remember)
|
class="w-5 h-5 flex items-center justify-center rounded border border-white/30 bg-white/5 peer-checked:bg-emerald-500/20 peer-checked:border-emerald-400 transition">
|
||||||
<svg width="9" height="9" viewBox="0 0 9 9" fill="none"><path d="M1.5 4.5l2 2 4-4" stroke="#a5b4fc" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<i class="ph ph-check text-emerald-400 text-xs hidden peer-checked:inline nx-check"></i>
|
||||||
@endif
|
|
||||||
</span>
|
</span>
|
||||||
<span style="font-size:12px;color:var(--mw-t4)">Merken</span>
|
<span class="text-sm text-white/70">Merken</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit"
|
|
||||||
wire:loading.attr="disabled"
|
<button type="submit" class="nx-btn w-full" wire:loading.attr="disabled">
|
||||||
class="mbx-btn-primary"
|
<span wire:loading.remove>Anmelden</span>
|
||||||
style="width:100%;justify-content:center;font-size:13px;padding:9px 16px">
|
<span wire:loading class="inline-flex items-center gap-2">
|
||||||
<span wire:loading.remove wire:target="login">Anmelden</span>
|
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
|
||||||
<span wire:loading wire:target="login" style="display:none">
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||||
<span style="display:inline-flex;align-items:center;gap:6px">
|
<path class="opacity-75" fill="currentColor"
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="animation:spin .7s linear infinite;flex-shrink:0">
|
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
|
||||||
<path d="M10 6A4 4 0 1 1 6 2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
wird geprüft…
|
wird geprüft…
|
||||||
</span>
|
</span>
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(\App\Models\Setting::signupAllowed())
|
@if(\App\Models\Setting::signupAllowed())
|
||||||
<div style="margin-top:16px;text-align:center;font-size:12px;color:var(--mw-t4)">
|
<div class="mt-6 text-sm text-white/70">
|
||||||
Noch keinen Account?
|
Noch keinen Account? <a wire:navigate href="{{ route('signup') }}" class="nx-link">Zur Registiereung</a>
|
||||||
<a wire:navigate href="{{ route('signup') }}" style="color:#a5b4fc;text-decoration:none">Zur Registrierung</a>
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
function togglePassword(id, btn) {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
const icon = btn.querySelector('i');
|
||||||
|
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text';
|
||||||
|
icon.classList.remove('ph-eye');
|
||||||
|
icon.classList.add('ph-eye-slash');
|
||||||
|
} else {
|
||||||
|
input.type = 'password';
|
||||||
|
icon.classList.remove('ph-eye-slash');
|
||||||
|
icon.classList.add('ph-eye');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
@error('admin_email') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
@error('admin_email') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="mw-modal-label">Passwort <span style="color:var(--mw-t4);font-weight:400">(min. 6 Zeichen)</span></label>
|
<label class="mw-modal-label">Passwort <span style="color:var(--mw-t4);font-weight:400">(min. 10 Zeichen)</span></label>
|
||||||
<input type="password" wire:model="admin_password" class="mw-modal-input">
|
<input type="password" wire:model="admin_password" class="mw-modal-input">
|
||||||
@error('admin_password') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
@error('admin_password') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -151,24 +151,11 @@
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:16px;padding:12px 14px;background:var(--mw-bg3);border-radius:8px;border:1px solid var(--mw-b2)">
|
|
||||||
<label class="mw-modal-check">
|
|
||||||
<input type="checkbox" wire:model="skipSsl">
|
|
||||||
<span class="mw-modal-check-label">SSL jetzt überspringen — später in den Einstellungen einrichten</span>
|
|
||||||
</label>
|
|
||||||
@if($skipSsl)
|
|
||||||
<div style="margin-top:8px;font-size:11.5px;color:#fbbf24;padding-left:23px">Nginx wird ohne SSL konfiguriert. Im Dashboard erscheint ein Hinweis bis SSL eingerichtet ist.</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ── Schritt 5: Domain-Setup ── --}}
|
{{-- ── Schritt 5: Domain-Setup ── --}}
|
||||||
@elseif($step === 5)
|
@elseif($step === 5)
|
||||||
|
@if(!$setupDone)
|
||||||
<div wire:poll.2s="pollSetup"></div>
|
<div wire:poll.2s="pollSetup"></div>
|
||||||
|
@endif
|
||||||
@php
|
|
||||||
$anyFailed = collect($domainStatus)->contains(fn($s) => in_array($s, ['error','nodns','noipv6']));
|
|
||||||
$allDone = $setupDone;
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
<div style="margin-bottom:20px">
|
<div style="margin-bottom:20px">
|
||||||
<div style="font-size:15px;font-weight:600;color:var(--mw-t1)">Domains werden eingerichtet</div>
|
<div style="font-size:15px;font-weight:600;color:var(--mw-t1)">Domains werden eingerichtet</div>
|
||||||
|
|
@ -178,51 +165,22 @@
|
||||||
@php
|
@php
|
||||||
$labels = ['ui' => $ui_domain, 'mail' => $mail_domain, 'webmail' => $webmail_domain];
|
$labels = ['ui' => $ui_domain, 'mail' => $mail_domain, 'webmail' => $webmail_domain];
|
||||||
$statusConfig = [
|
$statusConfig = [
|
||||||
'pending' => [
|
'pending' => ['icon' => '…', 'color' => 'var(--mw-t5)', 'bg' => 'var(--mw-bg3)', 'label' => 'Warte …'],
|
||||||
'icon' => '…', 'color' => 'var(--mw-t5)', 'bg' => 'var(--mw-bg3)',
|
'running' => ['icon' => '↻', 'color' => '#7dd3fc', 'bg' => 'rgba(14,165,233,.08)', 'label' => 'Wird registriert …', 'spin' => true],
|
||||||
'label' => 'Warte …', 'hint' => null,
|
'done' => ['icon' => '✓', 'color' => 'rgba(34,197,94,.9)', 'bg' => 'rgba(34,197,94,.07)', 'label' => 'Abgeschlossen'],
|
||||||
],
|
'nodns' => ['icon' => '!', 'color' => '#fbbf24', 'bg' => 'rgba(251,191,36,.07)', 'label' => 'Kein DNS-Eintrag gefunden'],
|
||||||
'running' => [
|
'error' => ['icon' => '✗', 'color' => '#f87171', 'bg' => 'rgba(239,68,68,.07)', 'label' => 'Fehler bei Registrierung'],
|
||||||
'icon' => '↻', 'color' => '#7dd3fc', 'bg' => 'rgba(14,165,233,.08)',
|
'skip' => ['icon' => '–', 'color' => 'var(--mw-t5)', 'bg' => 'var(--mw-bg3)', 'label' => 'Übersprungen'],
|
||||||
'label' => 'Wird registriert …', 'spin' => true, 'hint' => null,
|
|
||||||
],
|
|
||||||
'done' => [
|
|
||||||
'icon' => '✓', 'color' => 'rgba(34,197,94,.9)', 'bg' => 'rgba(34,197,94,.07)',
|
|
||||||
'label' => 'SSL-Zertifikat ausgestellt', 'hint' => null,
|
|
||||||
],
|
|
||||||
'nodns' => [
|
|
||||||
'icon' => '!', 'color' => '#fbbf24', 'bg' => 'rgba(251,191,36,.07)',
|
|
||||||
'label' => 'Kein DNS-Eintrag gefunden',
|
|
||||||
'hint' => 'Der A-Record dieser Domain zeigt nicht auf diesen Server oder ist noch nicht propagiert. DNS-Einstellungen prüfen und danach Retry klicken.',
|
|
||||||
'hints_extra' => ['DNS A-Record auf Server-IP setzen', 'DNS-Propagierung abwarten (bis zu 24h)', 'Mit dig +short A domain.com prüfen'],
|
|
||||||
],
|
|
||||||
'noipv6' => [
|
|
||||||
'icon' => '!', 'color' => '#fbbf24', 'bg' => 'rgba(251,191,36,.07)',
|
|
||||||
'label' => 'IPv6 nicht konfiguriert',
|
|
||||||
'hint' => 'Die Domain hat einen AAAA-Record, aber dieser Server hat kein aktives IPv6. Let\'s Encrypt prüft alle DNS-Records.',
|
|
||||||
'hints_extra' => ['IPv6 am Server aktivieren', 'ODER: AAAA-Record aus dem DNS entfernen'],
|
|
||||||
],
|
|
||||||
'error' => [
|
|
||||||
'icon' => '✗', 'color' => '#f87171', 'bg' => 'rgba(239,68,68,.07)',
|
|
||||||
'label' => 'SSL-Zertifikat fehlgeschlagen',
|
|
||||||
'hint' => 'Let\'s Encrypt konnte die Domain nicht verifizieren. Self-signed Zertifikat wird verwendet.',
|
|
||||||
'hints_extra' => ['Port 80 muss von außen erreichbar sein', 'Firewall-Regeln prüfen (ufw allow 80)', 'AAAA-Record ohne IPv6 am Server entfernen', 'http://domain/.well-known/acme-challenge/ im Browser testen'],
|
|
||||||
],
|
|
||||||
'skip' => [
|
|
||||||
'icon' => '–', 'color' => 'var(--mw-t4)', 'bg' => 'var(--mw-bg3)',
|
|
||||||
'label' => 'SSL übersprungen', 'hint' => null,
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:8px">
|
<div style="display:flex;flex-direction:column;gap:10px">
|
||||||
@foreach(['ui' => 'UI Domain', 'mail' => 'Mail Domain', 'webmail' => 'Webmail Domain'] as $key => $typeLabel)
|
@foreach(['ui' => 'UI Domain', 'mail' => 'Mail Domain', 'webmail' => 'Webmail Domain'] as $key => $typeLabel)
|
||||||
@php
|
@php
|
||||||
$st = $domainStatus[$key] ?? 'pending';
|
$st = $domainStatus[$key] ?? 'pending';
|
||||||
$cfg = $statusConfig[$st] ?? $statusConfig['pending'];
|
$cfg = $statusConfig[$st] ?? $statusConfig['pending'];
|
||||||
@endphp
|
@endphp
|
||||||
<div style="border-radius:8px;border:1px solid var(--mw-b2);overflow:hidden">
|
<div style="display:flex;align-items:center;gap:12px;padding:12px 14px;background:{{ $cfg['bg'] }};border-radius:8px;border:1px solid var(--mw-b2)">
|
||||||
<div style="display:flex;align-items:center;gap:12px;padding:12px 14px;background:{{ $cfg['bg'] }}">
|
|
||||||
<div style="width:28px;height:28px;border-radius:50%;background:{{ $cfg['bg'] }};border:1px solid {{ $cfg['color'] }};display:flex;align-items:center;justify-content:center;font-size:13px;color:{{ $cfg['color'] }};flex-shrink:0;{{ isset($cfg['spin']) ? 'animation:spin .9s linear infinite' : '' }}">
|
<div style="width:28px;height:28px;border-radius:50%;background:{{ $cfg['bg'] }};border:1px solid {{ $cfg['color'] }};display:flex;align-items:center;justify-content:center;font-size:13px;color:{{ $cfg['color'] }};flex-shrink:0;{{ isset($cfg['spin']) ? 'animation:spin .9s linear infinite' : '' }}">
|
||||||
{{ $cfg['icon'] }}
|
{{ $cfg['icon'] }}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -230,38 +188,10 @@
|
||||||
<div style="font-size:11px;color:var(--mw-t4)">{{ $typeLabel }}</div>
|
<div style="font-size:11px;color:var(--mw-t4)">{{ $typeLabel }}</div>
|
||||||
<div style="font-size:12.5px;color:var(--mw-t1);font-family:monospace;margin-top:1px">{{ $labels[$key] }}</div>
|
<div style="font-size:12.5px;color:var(--mw-t1);font-family:monospace;margin-top:1px">{{ $labels[$key] }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span style="font-size:11.5px;color:{{ $cfg['color'] }};white-space:nowrap">{{ $cfg['label'] }}</span>
|
<span style="font-size:11.5px;color:{{ $cfg['color'] }}">{{ $cfg['label'] }}</span>
|
||||||
</div>
|
|
||||||
@if(!empty($cfg['hint']))
|
|
||||||
<div style="padding:10px 14px;background:rgba(0,0,0,.15);border-top:1px solid var(--mw-b2)">
|
|
||||||
<div style="font-size:11.5px;color:var(--mw-t3);line-height:1.5;margin-bottom:{{ !empty($cfg['hints_extra']) ? '8px' : '0' }}">
|
|
||||||
{{ $cfg['hint'] }}
|
|
||||||
</div>
|
|
||||||
@if(!empty($cfg['hints_extra']))
|
|
||||||
<ul style="margin:0;padding-left:14px;display:flex;flex-direction:column;gap:3px">
|
|
||||||
@foreach($cfg['hints_extra'] as $hint)
|
|
||||||
<li style="font-size:11px;color:var(--mw-t4);line-height:1.4">{{ $hint }}</li>
|
|
||||||
@endforeach
|
|
||||||
</ul>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($allDone && $anyFailed)
|
|
||||||
<div style="margin-top:16px;padding:12px 14px;background:rgba(239,68,68,.05);border:1px solid rgba(239,68,68,.2);border-radius:8px">
|
|
||||||
<div style="font-size:12px;color:var(--mw-t3);margin-bottom:10px">
|
|
||||||
Einige Domains konnten nicht vollständig eingerichtet werden. Du kannst es erneut versuchen oder mit Self-signed Zertifikat fortfahren.
|
|
||||||
</div>
|
|
||||||
<button wire:click="retryDomains" wire:loading.attr="disabled" style="display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:7px;font-size:12px;font-weight:500;cursor:pointer;border:1px solid rgba(99,102,241,.4);background:rgba(99,102,241,.12);color:#a5b4fc">
|
|
||||||
<svg wire:loading.remove wire:target="retryDomains" width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M10 6A4 4 0 1 1 8.5 2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M8.5 1v2h2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
||||||
<svg wire:loading wire:target="retryDomains" width="12" height="12" viewBox="0 0 12 12" fill="none" style="animation:spin .7s linear infinite"><path d="M10 6A4 4 0 1 1 6 2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
|
||||||
Erneut versuchen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -292,12 +222,10 @@
|
||||||
</div>
|
</div>
|
||||||
@elseif($step === 5 && $setupDone)
|
@elseif($step === 5 && $setupDone)
|
||||||
<div style="display:flex;justify-content:flex-end;margin-top:20px">
|
<div style="display:flex;justify-content:flex-end;margin-top:20px">
|
||||||
{{-- Kein wire:click — plain Link damit kein Livewire-POST nötig ist.
|
<button wire:click="goToLogin" class="mbx-btn-primary" style="font-size:12.5px;width:fit-content">
|
||||||
nginx leitet /login nach SSL-Switch automatisch auf HTTPS weiter. --}}
|
|
||||||
<a href="/login" class="mbx-btn-primary" style="font-size:12.5px;width:fit-content;text-decoration:none;display:inline-flex;align-items:center;gap:6px">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6.5l2.5 2.5 5.5-5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6.5l2.5 2.5 5.5-5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
{{ collect($domainStatus)->contains(fn($s) => in_array($s, ['error','nodns','noipv6'])) ? 'Trotzdem zum Login' : 'Zum Login' }}
|
Zum Login
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,6 @@
|
||||||
|
|
||||||
<div wire:poll.30s>
|
<div wire:poll.30s>
|
||||||
|
|
||||||
{{-- SSL-Banner --}}
|
|
||||||
@if(!$sslConfigured)
|
|
||||||
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;margin-bottom:16px;background:rgba(251,191,36,.07);border:1px solid rgba(251,191,36,.25);border-radius:10px">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="flex-shrink:0;color:#fbbf24"><path d="M8 1.5L14.5 13H1.5L8 1.5Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M8 6v4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><circle cx="8" cy="11.5" r=".8" fill="currentColor"/></svg>
|
|
||||||
<div style="flex:1">
|
|
||||||
<span style="font-size:13px;font-weight:600;color:#fbbf24">SSL noch nicht eingerichtet</span>
|
|
||||||
<span style="font-size:12px;color:rgba(251,191,36,.7);margin-left:8px">Domains laufen ohne HTTPS — Zertifikate in den Einstellungen beantragen.</span>
|
|
||||||
</div>
|
|
||||||
<a href="{{ route('ui.system.settings') }}" style="font-size:12px;padding:5px 12px;border-radius:6px;background:rgba(251,191,36,.15);border:1px solid rgba(251,191,36,.3);color:#fbbf24;text-decoration:none;white-space:nowrap">
|
|
||||||
Jetzt einrichten →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
{{-- Hero Banner --}}
|
{{-- Hero Banner --}}
|
||||||
<div class="mw-hero">
|
<div class="mw-hero">
|
||||||
<div class="mw-hero-icon">
|
<div class="mw-hero-icon">
|
||||||
|
|
@ -213,25 +199,8 @@
|
||||||
</div>
|
</div>
|
||||||
<span class="mw-sc-badge mw-badge-mute">{{ $alertCount }} offen</span>
|
<span class="mw-sc-badge mw-badge-mute">{{ $alertCount }} offen</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mw-sc-body">
|
<div class="mw-sc-body" style="font-style:italic;color:var(--mw-t4);">
|
||||||
@if($sandboxAlerts->isEmpty())
|
{{ $alertCount === 0 ? 'Keine Warnungen.' : ($alertCount . ' aktive Warnungen.') }}
|
||||||
<div style="font-style:italic;color:var(--mw-t4);font-size:12px;">Keine Warnungen.</div>
|
|
||||||
@else
|
|
||||||
<div style="display:flex;flex-direction:column;gap:5px;">
|
|
||||||
@foreach($sandboxAlerts as $sr)
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;padding:5px 8px;background:rgba(234,179,8,.07);border:1px solid rgba(234,179,8,.2);border-radius:6px;">
|
|
||||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" style="flex-shrink:0;color:#fbbf24"><path d="M5.5 1L10.5 10H.5L5.5 1Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/><path d="M5.5 4.5v2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
|
||||||
<span style="font-size:11.5px;color:#fbbf24;font-weight:500;">
|
|
||||||
@if($sr->type === 'global') Sandbox global aktiv
|
|
||||||
@elseif($sr->type === 'domain') Sandbox aktiv: {{ $sr->target }}
|
|
||||||
@else Sandbox aktiv: {{ $sr->target }}
|
|
||||||
@endif
|
|
||||||
</span>
|
|
||||||
<a href="{{ route('ui.system.sandbox') }}" style="margin-left:auto;font-size:10.5px;color:rgba(251,191,36,.6);text-decoration:none;white-space:nowrap;">verwalten →</a>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ $_wmUrl = ($_wmSub && $_wmBase)
|
||||||
<td class="mbx-td">
|
<td class="mbx-td">
|
||||||
<div class="mbx-actions">
|
<div class="mbx-actions">
|
||||||
<a href="{{ $_wmUrl }}" target="_blank" class="mbx-act-btn" title="Webmail öffnen">
|
<a href="{{ $_wmUrl }}" target="_blank" class="mbx-act-btn" title="Webmail öffnen">
|
||||||
<svg width="13" height="13" viewBox="0 0 14 14" fill="none"><rect x=".5" y="2.5" width="13" height="9" rx="1.5" stroke="currentColor" stroke-width="1.2"/><path d="M.5 5l6.5 4 6.5-4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
<svg width="13" height="13" viewBox="0 0 14 14" fill="none"><rect x=".5" y="2.5" width="13" height="9" rx="1.5" stroke="currentColor" stroke-width="1.2"/><path d=".5 5l6.5 4 6.5-4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<button wire:click="openDns({{ $systemDomain->id }})" class="mbx-act-btn" title="DNS-Assistent">
|
<button wire:click="openDns({{ $systemDomain->id }})" class="mbx-act-btn" title="DNS-Assistent">
|
||||||
<svg width="13" height="13" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M4.5 9.5C5 8 6 7 7 7s2 1 2.5 2.5" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/><circle cx="7" cy="4.5" r=".7" fill="currentColor"/></svg>
|
<svg width="13" height="13" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M4.5 9.5C5 8 6 7 7 7s2 1 2.5 2.5" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/><circle cx="7" cy="4.5" r=".7" fill="currentColor"/></svg>
|
||||||
|
|
@ -138,15 +138,6 @@ $_wmUrl = ($_wmSub && $_wmBase)
|
||||||
|
|
||||||
<td class="mbx-td">
|
<td class="mbx-td">
|
||||||
<div style="display:flex;align-items:center;gap:8px;min-width:0">
|
<div style="display:flex;align-items:center;gap:8px;min-width:0">
|
||||||
@if(\App\Models\SandboxRoute::isActiveForDomain($d->domain))
|
|
||||||
<div class="dom-icon" style="color:#fbbf24;background:rgba(234,179,8,.1);border-color:rgba(234,179,8,.3);flex-shrink:0">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
|
|
||||||
<path d="M7 2L13 12H1L7 2Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
|
||||||
<path d="M7 6v3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
|
||||||
<circle cx="7" cy="10.5" r=".6" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<div class="dom-icon">
|
<div class="dom-icon">
|
||||||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
|
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
|
||||||
<circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/>
|
<circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/>
|
||||||
|
|
@ -154,7 +145,6 @@ $_wmUrl = ($_wmSub && $_wmBase)
|
||||||
<path d="M1.5 5.5h11M1.5 8.5h11" stroke="currentColor" stroke-width="1.2"/>
|
<path d="M1.5 5.5h11M1.5 8.5h11" stroke="currentColor" stroke-width="1.2"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
<div style="min-width:0">
|
<div style="min-width:0">
|
||||||
<div class="dom-name">{{ $d->domain }}</div>
|
<div class="dom-name">{{ $d->domain }}</div>
|
||||||
@if(!empty($d->description))
|
@if(!empty($d->description))
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
<x-slot:breadcrumb>API Keys</x-slot:breadcrumb>
|
<x-slot:breadcrumb>API Keys</x-slot:breadcrumb>
|
||||||
|
|
||||||
<div x-data="{activeGroup: 'mailboxes'}"
|
<div x-data="{activeGroup: 'mailboxes'}"
|
||||||
x-on:token-created.window="$wire.$refresh()"
|
x-on:token-created.window="$dispatch('openModal', {component: 'ui.system.modal.api-key-show-modal', arguments: {plainText: $event.detail.plainText}})">
|
||||||
x-on:token-deleted.window="$wire.$refresh()">
|
|
||||||
|
|
||||||
<div class="mbx-page-header">
|
<div class="mbx-page-header">
|
||||||
<div class="mbx-page-title">
|
<div class="mbx-page-title">
|
||||||
|
|
@ -24,7 +23,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mw-apikey-layout">
|
<div style="display:grid;grid-template-columns:1fr 340px;gap:14px;align-items:start">
|
||||||
|
|
||||||
{{-- ═══ Left: Keys + Endpoint-Docs ═══ --}}
|
{{-- ═══ Left: Keys + Endpoint-Docs ═══ --}}
|
||||||
<div class="mbx-sections">
|
<div class="mbx-sections">
|
||||||
|
|
@ -47,68 +46,62 @@
|
||||||
<div style="font-size:11.5px;color:var(--mw-t4)">Erstelle deinen ersten Key um externe Anwendungen zu verbinden.</div>
|
<div style="font-size:11.5px;color:var(--mw-t4)">Erstelle deinen ersten Key um externe Anwendungen zu verbinden.</div>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
|
<div class="mbx-table-wrap">
|
||||||
{{-- Header --}}
|
<table class="mbx-table">
|
||||||
<div class="mw-kl-head">
|
<thead>
|
||||||
<span>Name</span>
|
<tr>
|
||||||
<span>Scopes</span>
|
<th class="mbx-th">Name</th>
|
||||||
<span>Modus</span>
|
<th class="mbx-th">Scopes</th>
|
||||||
<span>Erstellt</span>
|
<th class="mbx-th" style="width:86px">Modus</th>
|
||||||
<span></span>
|
<th class="mbx-th" style="width:120px">Zuletzt genutzt</th>
|
||||||
</div>
|
<th class="mbx-th" style="width:110px">Erstellt</th>
|
||||||
|
<th class="mbx-th mbx-th-right" style="width:50px"></th>
|
||||||
{{-- Rows --}}
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
@foreach($tokens as $token)
|
@foreach($tokens as $token)
|
||||||
@php
|
<tr class="mbx-tr">
|
||||||
$abilities = $token->abilities;
|
<td class="mbx-td">
|
||||||
$visible = array_slice($abilities, 0, 2);
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
$rest = count($abilities) - 2;
|
|
||||||
@endphp
|
|
||||||
<div class="mw-kl-row">
|
|
||||||
{{-- Name --}}
|
|
||||||
<div class="mw-kl-name" style="display:flex;align-items:center;gap:8px;overflow:hidden">
|
|
||||||
<div style="width:26px;height:26px;border-radius:7px;background:var(--mw-bg4);border:1px solid var(--mw-b2);display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
<div style="width:26px;height:26px;border-radius:7px;background:var(--mw-bg4);border:1px solid var(--mw-b2);display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
||||||
<path d="M6 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" stroke="var(--mw-t3)" stroke-width="1.3"/>
|
<path d="M6 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" stroke="var(--mw-t3)" stroke-width="1.3"/>
|
||||||
<path d="M9.5 8.5L14 13" stroke="var(--mw-t3)" stroke-width="1.3" stroke-linecap="round"/>
|
<path d="M9.5 8.5L14 13" stroke="var(--mw-t3)" stroke-width="1.3" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span style="font-size:12.5px;font-weight:500;color:var(--mw-t1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1">{{ $token->name }}</span>
|
<span style="font-size:12.5px;font-weight:500;color:var(--mw-t1)">{{ $token->name }}</span>
|
||||||
</div>
|
</div>
|
||||||
{{-- Scopes --}}
|
</td>
|
||||||
<div class="mw-kl-scopes" style="display:flex;flex-wrap:nowrap;gap:3px;align-items:center;overflow:hidden">
|
<td class="mbx-td">
|
||||||
@foreach($visible as $scope)
|
<div style="display:flex;flex-wrap:wrap;gap:3px">
|
||||||
@php $short = collect(explode(':', $scope))->map(fn($p) => $p[0])->join(':'); @endphp
|
@foreach($token->abilities as $scope)
|
||||||
<span title="{{ $scope }}" style="font-family:monospace;font-size:9.5px;padding:1px 5px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4)">{{ $short }}</span>
|
<span style="font-family:monospace;font-size:9.5px;padding:1px 5px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4)">{{ $scope }}</span>
|
||||||
@endforeach
|
@endforeach
|
||||||
@if($rest > 0)
|
|
||||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.api-key-scopes-modal',arguments:{tokenId:{{ $token->id }}}})"
|
|
||||||
style="font-family:monospace;font-size:9.5px;padding:1px 6px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-v2);cursor:pointer;white-space:nowrap;flex-shrink:0">
|
|
||||||
+{{ $rest }}
|
|
||||||
</button>
|
|
||||||
@endif
|
|
||||||
<span class="mw-kl-date-inline" style="font-size:10px;color:var(--mw-t5);margin-left:2px">· {{ $token->created_at->format('d.m.Y') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
{{-- Modus --}}
|
</td>
|
||||||
<div class="mw-kl-modus">
|
<td class="mbx-td">
|
||||||
@if($token->sandbox)
|
@if($token->sandbox)
|
||||||
<span style="font-size:10px;padding:2px 7px;border-radius:5px;background:rgba(251,191,36,.1);border:1px solid rgba(251,191,36,.25);color:#f59e0b;font-weight:500;white-space:nowrap">Sandbox</span>
|
<span style="font-size:10px;padding:2px 7px;border-radius:5px;background:rgba(251,191,36,.1);border:1px solid rgba(251,191,36,.25);color:#f59e0b;font-weight:500">Sandbox</span>
|
||||||
@else
|
@else
|
||||||
<span style="font-size:10px;padding:2px 7px;border-radius:5px;background:rgba(16,185,129,.08);border:1px solid rgba(16,185,129,.2);color:#34d399;font-weight:500;white-space:nowrap">Live</span>
|
<span style="font-size:10px;padding:2px 7px;border-radius:5px;background:rgba(16,185,129,.08);border:1px solid rgba(16,185,129,.2);color:#34d399;font-weight:500">Live</span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</td>
|
||||||
{{-- Date (desktop only) --}}
|
<td class="mbx-td mbx-td-muted" style="font-size:11px">{{ $token->last_used_at?->diffForHumans() ?? '—' }}</td>
|
||||||
<div class="mw-kl-date" style="font-size:11px;color:var(--mw-t4)">{{ $token->created_at->format('d.m.Y') }}</div>
|
<td class="mbx-td mbx-td-muted" style="font-size:11px">{{ $token->created_at->format('d.m.Y') }}</td>
|
||||||
{{-- Actions --}}
|
<td class="mbx-td">
|
||||||
<div class="mw-kl-actions">
|
<div class="mbx-actions">
|
||||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.api-key-delete-modal',arguments:{tokenId:{{ $token->id }}}})"
|
<button wire:click="deleteToken({{ $token->id }})"
|
||||||
|
wire:confirm="API Key '{{ $token->name }}' wirklich löschen?"
|
||||||
class="mbx-act-btn mbx-act-danger" title="Löschen">
|
class="mbx-act-btn mbx-act-danger" title="Löschen">
|
||||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -300,5 +293,4 @@ Content-Type: application/json</pre>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
@php
|
@php
|
||||||
$componentIcons = [
|
$componentIcons = [
|
||||||
'nginx' => '<path d="M7 2L2 5v4l5 3 5-3V5L7 2Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/><path d="M7 2v11M2 5l10 6M12 5L2 11" stroke="currentColor" stroke-width="1.2"/>',
|
'nginx' => '<path d="M7 2L2 5v4l5 3 5-3V5L7 2Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/><path d="M7 2v11M2 5l10 6M12 5L2 11" stroke="currentColor" stroke-width="1.2"/>',
|
||||||
'postfix' => '<rect x=".5" y="2.5" width="13" height="9" rx="1.5" stroke="currentColor" stroke-width="1.2"/><path d="M.5 5l6.5 4.5L13.5 5" stroke="currentColor" stroke-width="1.2"/>',
|
'postfix' => '<rect x=".5" y="2.5" width="13" height="9" rx="1.5" stroke="currentColor" stroke-width="1.2"/><path d=".5 5l6.5 4.5L13.5 5" stroke="currentColor" stroke-width="1.2"/>',
|
||||||
'dovecot' => '<path d="M7 1.5C7 1.5 2 4 2 8.5a5 5 0 0 0 10 0C12 4 7 1.5 7 1.5Z" stroke="currentColor" stroke-width="1.2"/>',
|
'dovecot' => '<path d="M7 1.5C7 1.5 2 4 2 8.5a5 5 0 0 0 10 0C12 4 7 1.5 7 1.5Z" stroke="currentColor" stroke-width="1.2"/>',
|
||||||
'rspamd' => '<path d="M7 1.5L12.5 4v4.8c0 2.8-2.3 4.8-5.5 5.2C3.8 13.6 1.5 11.6 1.5 8.8V4L7 1.5Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>',
|
'rspamd' => '<path d="M7 1.5L12.5 4v4.8c0 2.8-2.3 4.8-5.5 5.2C3.8 13.6 1.5 11.6 1.5 8.8V4L7 1.5Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>',
|
||||||
'fail2ban' => '<rect x="1.5" y="6" width="11" height="7.5" rx="1.5" stroke="currentColor" stroke-width="1.2"/><path d="M4.5 6V4.5a2.5 2.5 0 0 1 5 0V6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>',
|
'fail2ban' => '<rect x="1.5" y="6" width="11" height="7.5" rx="1.5" stroke="currentColor" stroke-width="1.2"/><path d="M4.5 6V4.5a2.5 2.5 0 0 1 5 0V6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>',
|
||||||
|
|
|
||||||
|
|
@ -18,37 +18,33 @@
|
||||||
@error('name') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
@error('name') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:14px">
|
<div style="margin-top:16px">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
|
||||||
<label class="mw-modal-label" style="margin:0">Berechtigungen</label>
|
<label class="mw-modal-label" style="margin:0">Berechtigungen (Scopes)</label>
|
||||||
<button type="button" wire:click="toggleAll"
|
<button type="button" wire:click="toggleAll"
|
||||||
style="background:none;border:none;font-size:11px;color:var(--mw-v);cursor:pointer;padding:0">
|
style="background:none;border:none;font-size:11px;color:var(--mw-v);cursor:pointer;padding:0">
|
||||||
{{ count($selected) === count($scopes) ? 'Alle abwählen' : 'Alle wählen' }}
|
{{ count($selected) === count($scopes) ? 'Alle abwählen' : 'Alle wählen' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:12px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:8px">
|
<div style="display:flex;flex-direction:column;gap:6px;padding:12px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:8px">
|
||||||
@foreach($scopes as $key => $label)
|
@foreach($scopes as $key => $label)
|
||||||
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;padding:5px 8px;border-radius:5px;transition:background .1s"
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||||
onmouseover="this.style.background='var(--mw-bg3)'"
|
|
||||||
onmouseout="this.style.background='transparent'">
|
|
||||||
<input type="checkbox" wire:model="selected" value="{{ $key }}"
|
<input type="checkbox" wire:model="selected" value="{{ $key }}"
|
||||||
style="width:13px;height:13px;flex-shrink:0;accent-color:var(--mw-v)">
|
style="width:13px;height:13px;accent-color:var(--mw-v)">
|
||||||
<div>
|
<span style="font-size:12px;color:var(--mw-t2);font-family:monospace">{{ $key }}</span>
|
||||||
<div style="font-size:11px;color:var(--mw-t2);font-family:monospace;line-height:1.3">{{ $key }}</div>
|
<span style="font-size:11px;color:var(--mw-t4)">— {{ $label }}</span>
|
||||||
<div style="font-size:10px;color:var(--mw-t4);line-height:1.3">{{ $label }}</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@error('selected') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
@error('selected') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:12px">
|
<div style="margin-top:14px;padding:12px;background:rgba(251,191,36,.06);border:1px solid rgba(251,191,36,.2);border-radius:8px">
|
||||||
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;padding:10px 12px;background:rgba(251,191,36,.05);border:1px solid rgba(251,191,36,.18);border-radius:8px">
|
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
|
||||||
<input type="checkbox" wire:model="sandbox" style="width:13px;height:13px;flex-shrink:0;accent-color:#f59e0b">
|
<input type="checkbox" wire:model="sandbox" style="width:13px;height:13px;margin-top:1px;accent-color:#f59e0b">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:12px;color:var(--mw-t1);font-weight:500">Sandbox-Modus</div>
|
<div style="font-size:12.5px;color:var(--mw-t1);font-weight:500">Sandbox-Modus</div>
|
||||||
<div style="font-size:10.5px;color:var(--mw-t4);margin-top:1px">Schreiboperationen werden simuliert — keine Änderungen in der DB.</div>
|
<div style="font-size:11px;color:var(--mw-t4);margin-top:2px">Schreiboperationen werden simuliert — keine Änderungen in der Datenbank. Ideal für Tests und Entwicklung.</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<div class="mw-modal-frame">
|
|
||||||
|
|
||||||
<div class="mw-modal-head">
|
|
||||||
<div style="display:flex;align-items:center;gap:7px">
|
|
||||||
<span class="mbx-badge-mute">API Key</span>
|
|
||||||
<span style="font-size:12px;color:var(--mw-t3)">— Löschen</span>
|
|
||||||
</div>
|
|
||||||
<button wire:click="$dispatch('closeModal')" class="mw-modal-close">
|
|
||||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M1 1l11 11M12 1L1 12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mw-modal-body">
|
|
||||||
<div style="padding:12px 14px;border-radius:8px;border:1px solid rgba(239,68,68,.25);background:rgba(239,68,68,.06)">
|
|
||||||
<div style="font-size:12.5px;font-weight:500;color:var(--mw-t1);font-family:monospace">{{ $tokenName }}</div>
|
|
||||||
<div style="font-size:11px;color:rgba(252,165,165,.8);margin-top:4px">
|
|
||||||
Dieser API Key wird dauerhaft gelöscht. Anwendungen die ihn nutzen verlieren sofort den Zugriff.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mw-modal-foot">
|
|
||||||
<button wire:click="$dispatch('closeModal')" class="mw-btn-cancel">Abbrechen</button>
|
|
||||||
<button wire:click="delete" class="mw-btn-del">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
||||||
Löschen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<div class="mw-modal-frame">
|
|
||||||
|
|
||||||
<div class="mw-modal-head">
|
|
||||||
<div style="display:flex;align-items:center;gap:7px">
|
|
||||||
<span class="mbx-badge-mute">{{ $tokenName }}</span>
|
|
||||||
<span style="font-size:12px;color:var(--mw-t3)">— Scopes</span>
|
|
||||||
</div>
|
|
||||||
<button wire:click="$dispatch('closeModal')" class="mw-modal-close">
|
|
||||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M1 1l11 11M12 1L1 12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mw-modal-body">
|
|
||||||
<div style="display:flex;flex-direction:column;gap:5px">
|
|
||||||
@foreach($scopes as $scope)
|
|
||||||
@php $short = collect(explode(':', $scope))->map(fn($p) => $p[0])->join(':'); @endphp
|
|
||||||
<div style="display:flex;align-items:center;gap:10px;padding:8px 10px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:7px">
|
|
||||||
<code style="font-family:monospace;font-size:11px;padding:1px 7px;background:var(--mw-bg3);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4);flex-shrink:0">{{ $short }}</code>
|
|
||||||
<span style="font-size:11.5px;color:var(--mw-t3)">{{ $scope }}</span>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mw-modal-foot">
|
|
||||||
<button wire:click="$dispatch('closeModal')" class="mw-btn-cancel">Schließen</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<span class="mbx-badge-ok">Key erstellt</span>
|
<span class="mbx-badge-ok">Key erstellt</span>
|
||||||
<span style="font-size:12px;color:var(--mw-t3)">— einmalig anzeigen</span>
|
<span style="font-size:12px;color:var(--mw-t3)">— einmalig anzeigen</span>
|
||||||
</div>
|
</div>
|
||||||
<button wire:click="dismiss" class="mw-modal-close">
|
<button wire:click="$dispatch('closeModal')" class="mw-modal-close">
|
||||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M1 1l11 11M12 1L1 12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M1 1l11 11M12 1L1 12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mw-modal-foot">
|
<div class="mw-modal-foot">
|
||||||
<button wire:click="dismiss" class="mw-btn-primary">Fertig</button>
|
<button wire:click="$dispatch('closeModal')" class="mbx-btn-primary">Fertig</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,16 +30,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Sandbox-Routing Regeln --}}
|
|
||||||
<livewire:ui.system.sandbox-routes />
|
|
||||||
|
|
||||||
<div style="height:1px;background:var(--mw-b1);margin-bottom:14px"></div>
|
|
||||||
|
|
||||||
{{-- Mail-Client Layout --}}
|
{{-- Mail-Client Layout --}}
|
||||||
<div class="mw-sandbox-client">
|
<div style="display:grid;grid-template-columns:320px 1fr;gap:0;border:1px solid var(--mw-b1);border-radius:10px;overflow:hidden;background:var(--mw-bg2);min-height:520px">
|
||||||
|
|
||||||
{{-- ═══ Left: Mail-Liste ═══ --}}
|
{{-- ═══ Left: Mail-Liste ═══ --}}
|
||||||
<div class="mw-sandbox-list">
|
<div style="border-right:1px solid var(--mw-b1);overflow-y:auto;max-height:680px">
|
||||||
|
|
||||||
@if($mails->isEmpty())
|
@if($mails->isEmpty())
|
||||||
<div style="padding:48px 16px;text-align:center">
|
<div style="padding:48px 16px;text-align:center">
|
||||||
|
|
@ -88,7 +83,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- ═══ Right: Mail-Detail ═══ --}}
|
{{-- ═══ Right: Mail-Detail ═══ --}}
|
||||||
<div class="mw-sandbox-detail">
|
<div style="overflow-y:auto;max-height:680px">
|
||||||
|
|
||||||
@if(!$selected)
|
@if(!$selected)
|
||||||
<div style="display:flex;align-items:center;justify-content:center;height:100%;min-height:400px;flex-direction:column;gap:12px">
|
<div style="display:flex;align-items:center;justify-content:center;height:100%;min-height:400px;flex-direction:column;gap:12px">
|
||||||
|
|
@ -194,7 +189,7 @@
|
||||||
<span style="font-size:11px;color:var(--mw-t4);margin-left:6px">— Sandbox-Transport aktivieren</span>
|
<span style="font-size:11px;color:var(--mw-t4);margin-left:6px">— Sandbox-Transport aktivieren</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mw-postfix-grid">
|
<div style="padding:16px 18px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:11px;color:var(--mw-t4);margin-bottom:6px;font-weight:500">1. master.cf — Transport anlegen</div>
|
<div style="font-size:11px;color:var(--mw-t4);margin-bottom:6px;font-weight:500">1. master.cf — Transport anlegen</div>
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
<div style="margin-bottom:14px">
|
|
||||||
|
|
||||||
{{-- ── Header ── --}}
|
|
||||||
<div class="mbx-domain-head" style="margin-bottom:10px">
|
|
||||||
<div class="mbx-domain-info">
|
|
||||||
<span style="font-size:10.5px;padding:2px 8px;border-radius:4px;background:rgba(234,179,8,.12);border:1px solid rgba(234,179,8,.3);color:#fbbf24;font-weight:600;">
|
|
||||||
Sandbox-Routing
|
|
||||||
</span>
|
|
||||||
<span style="font-size:11px;color:var(--mw-t4);margin-left:6px">— Transport-Regeln für Postfix</span>
|
|
||||||
</div>
|
|
||||||
<button wire:click="syncPostfix" class="mw-btn-secondary"
|
|
||||||
style="display:flex;align-items:center;gap:6px;font-size:12px;padding:5px 12px;white-space:nowrap">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
|
|
||||||
<path d="M12.5 7A5.5 5.5 0 1 1 7 1.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
|
||||||
<path d="M7 .5l2.5 1.5L7 3.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
Postfix-Sync
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ── Sync-Message ── --}}
|
|
||||||
@if($syncMessage)
|
|
||||||
<div style="margin-bottom:10px;padding:7px 12px;border-radius:6px;font-size:11.5px;display:flex;align-items:center;gap:7px;
|
|
||||||
{{ $syncOk
|
|
||||||
? 'background:rgba(34,197,94,.07);border:1px solid rgba(34,197,94,.25);color:var(--mw-gr)'
|
|
||||||
: 'background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.25);color:#f87171' }}">
|
|
||||||
@if($syncOk)
|
|
||||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none"><path d="M1.5 5.5l3 3 5-5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
||||||
@else
|
|
||||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none"><path d="M1.5 1.5l8 8M9.5 1.5l-8 8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
|
||||||
@endif
|
|
||||||
{{ $syncMessage }}
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
{{-- ── Add-Form ── --}}
|
|
||||||
<div style="display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
|
|
||||||
<select wire:model.live="newType"
|
|
||||||
style="font-size:12px;padding:5px 8px;border-radius:6px;background:var(--mw-bg3);border:1px solid var(--mw-b2);color:var(--mw-t2);cursor:pointer;outline:none;height:30px;flex-shrink:0">
|
|
||||||
<option value="global">global</option>
|
|
||||||
<option value="domain">domain</option>
|
|
||||||
<option value="address">address</option>
|
|
||||||
</select>
|
|
||||||
@if($newType !== 'global')
|
|
||||||
<input wire:model="newTarget" type="text"
|
|
||||||
placeholder="{{ $newType === 'domain' ? 'example.com' : 'user@example.com' }}"
|
|
||||||
style="font-size:12px;padding:5px 10px;border-radius:6px;background:var(--mw-bg3);border:1px solid var(--mw-b2);color:var(--mw-t1);outline:none;width:220px;height:30px;flex-shrink:0"
|
|
||||||
wire:keydown.enter="addRoute">
|
|
||||||
@endif
|
|
||||||
<button wire:click="addRoute" class="mw-btn-primary" style="font-size:12px;padding:5px 14px;height:30px;white-space:nowrap;flex-shrink:0">
|
|
||||||
+ Hinzufügen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ── Route List ── --}}
|
|
||||||
@if($this->routes->isEmpty())
|
|
||||||
<div style="padding:20px 16px;text-align:center;color:var(--mw-t4);font-size:12px;background:var(--mw-bg3);border:1px solid var(--mw-b1);border-radius:8px;">
|
|
||||||
Keine Sandbox-Routen konfiguriert.
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<div style="border:1px solid var(--mw-b1);border-radius:8px;overflow:hidden">
|
|
||||||
@foreach($this->routes as $route)
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;
|
|
||||||
{{ !$loop->last ? 'border-bottom:1px solid var(--mw-b1);' : '' }}
|
|
||||||
background:{{ $route->is_active ? 'var(--mw-bg3)' : 'var(--mw-bg4)' }}">
|
|
||||||
|
|
||||||
{{-- Type badge --}}
|
|
||||||
@if($route->type === 'global')
|
|
||||||
<span style="font-size:9.5px;padding:1px 6px;border-radius:4px;background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.25);color:#f87171;font-weight:600;flex-shrink:0">global</span>
|
|
||||||
@elseif($route->type === 'domain')
|
|
||||||
<span style="font-size:9.5px;padding:1px 6px;border-radius:4px;background:rgba(124,58,237,.12);border:1px solid rgba(124,58,237,.25);color:var(--mw-v2);font-weight:600;flex-shrink:0">domain</span>
|
|
||||||
@else
|
|
||||||
<span style="font-size:9.5px;padding:1px 6px;border-radius:4px;background:rgba(59,130,246,.12);border:1px solid rgba(59,130,246,.25);color:#60a5fa;font-weight:600;flex-shrink:0">address</span>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
{{-- Target --}}
|
|
||||||
<span style="font-size:12px;color:{{ $route->is_active ? 'var(--mw-t1)' : 'var(--mw-t4)' }};font-family:monospace;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
|
||||||
{{ $route->target ?? '* (alle)' }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{{-- Active indicator --}}
|
|
||||||
@if($route->is_active)
|
|
||||||
<span style="font-size:9.5px;padding:1px 6px;border-radius:4px;background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.2);color:var(--mw-gr);font-weight:600;flex-shrink:0">aktiv</span>
|
|
||||||
@else
|
|
||||||
<span style="font-size:9.5px;padding:1px 6px;border-radius:4px;background:var(--mw-bg4);border:1px solid var(--mw-b2);color:var(--mw-t4);font-weight:600;flex-shrink:0">inaktiv</span>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
{{-- Toggle button --}}
|
|
||||||
<button wire:click="toggleRoute({{ $route->id }})" class="mbx-act-btn"
|
|
||||||
title="{{ $route->is_active ? 'Deaktivieren' : 'Aktivieren' }}"
|
|
||||||
style="flex-shrink:0">
|
|
||||||
@if($route->is_active)
|
|
||||||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none"><rect x="1" y="4" width="12" height="6" rx="3" stroke="currentColor" stroke-width="1.2"/><circle cx="10" cy="7" r="2" fill="currentColor"/></svg>
|
|
||||||
@else
|
|
||||||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none"><rect x="1" y="4" width="12" height="6" rx="3" stroke="currentColor" stroke-width="1.2"/><circle cx="4" cy="7" r="2" fill="currentColor" opacity=".4"/></svg>
|
|
||||||
@endif
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{{-- Delete button --}}
|
|
||||||
<button wire:click="removeRoute({{ $route->id }})"
|
|
||||||
wire:confirm="Route '{{ $route->target ?? 'global' }}' wirklich löschen?"
|
|
||||||
class="mbx-act-btn mbx-act-danger" title="Löschen" style="flex-shrink:0">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none">
|
|
||||||
<path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mw-wh-layout">
|
<div style="display:grid;grid-template-columns:1fr 300px;gap:14px;align-items:start">
|
||||||
|
|
||||||
{{-- ═══ Left ═══ --}}
|
{{-- ═══ Left ═══ --}}
|
||||||
<div class="mbx-sections">
|
<div class="mbx-sections">
|
||||||
|
|
@ -45,68 +45,45 @@
|
||||||
<div style="font-size:11.5px;color:var(--mw-t4)">Verbinde externe Systeme — sie werden bei Events automatisch benachrichtigt.</div>
|
<div style="font-size:11.5px;color:var(--mw-t4)">Verbinde externe Systeme — sie werden bei Events automatisch benachrichtigt.</div>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
|
<div class="mbx-table-wrap">
|
||||||
{{-- Header --}}
|
<table class="mbx-table">
|
||||||
<div class="mw-whl-head">
|
<thead>
|
||||||
<span>Webhook</span>
|
<tr>
|
||||||
<span>Events</span>
|
<th class="mbx-th">Webhook</th>
|
||||||
<span>Status</span>
|
<th class="mbx-th">Events</th>
|
||||||
<span>HTTP</span>
|
<th class="mbx-th" style="width:80px">Status</th>
|
||||||
<span>Aktionen</span>
|
<th class="mbx-th" style="width:90px">HTTP</th>
|
||||||
</div>
|
<th class="mbx-th" style="width:130px">Zuletzt ausgelöst</th>
|
||||||
|
<th class="mbx-th mbx-th-right" style="width:80px">Aktionen</th>
|
||||||
{{-- Rows --}}
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
@foreach($webhooks as $wh)
|
@foreach($webhooks as $wh)
|
||||||
@php
|
<tr class="mbx-tr">
|
||||||
$evVisible = array_slice($wh->events, 0, 2);
|
<td class="mbx-td">
|
||||||
$evRest = count($wh->events) - 2;
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
@endphp
|
|
||||||
<div class="mw-whl-row">
|
|
||||||
{{-- Webhook name + url --}}
|
|
||||||
<div class="mw-whl-webhook" style="display:flex;align-items:center;gap:8px;min-width:0">
|
|
||||||
<div style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:{{ $wh->is_active ? '#34d399' : 'var(--mw-t4)' }};box-shadow:{{ $wh->is_active ? '0 0 6px rgba(52,211,153,.5)' : 'none' }}"></div>
|
<div style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:{{ $wh->is_active ? '#34d399' : 'var(--mw-t4)' }};box-shadow:{{ $wh->is_active ? '0 0 6px rgba(52,211,153,.5)' : 'none' }}"></div>
|
||||||
<div style="min-width:0">
|
<div>
|
||||||
<div style="font-size:12.5px;font-weight:500;color:var(--mw-t1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
<div style="font-size:12.5px;font-weight:500;color:var(--mw-t1)">{{ $wh->name }}</div>
|
||||||
{{ $wh->name }}
|
<div style="font-family:monospace;font-size:10.5px;color:var(--mw-t4);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:260px">{{ $wh->url }}</div>
|
||||||
<span class="mw-whl-status-sm">
|
|
||||||
@if($wh->is_active)
|
|
||||||
<span class="mbx-badge-ok" style="font-size:9px;margin-left:4px">Aktiv</span>
|
|
||||||
@else
|
|
||||||
<span class="mbx-badge-warn" style="font-size:9px;margin-left:4px">Pausiert</span>
|
|
||||||
@endif
|
|
||||||
</span>
|
|
||||||
<span class="mw-whl-http-sm" style="font-family:monospace;font-size:9.5px;margin-left:4px">
|
|
||||||
@if($wh->last_status !== null)
|
|
||||||
@if($wh->last_status >= 200 && $wh->last_status < 300)
|
|
||||||
<span style="color:#34d399;font-weight:600">{{ $wh->last_status }}</span>
|
|
||||||
@else
|
|
||||||
<span style="color:#f87171;font-weight:600">{{ $wh->last_status === 0 ? 'T/O' : $wh->last_status }}</span>
|
|
||||||
@endif
|
|
||||||
@endif
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div style="font-family:monospace;font-size:10px;color:var(--mw-t5);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ $wh->url }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{-- Events --}}
|
</td>
|
||||||
<div class="mw-whl-events" style="display:flex;flex-wrap:wrap;gap:3px;align-items:center">
|
<td class="mbx-td">
|
||||||
@foreach($evVisible as $ev)
|
<div style="display:flex;flex-wrap:wrap;gap:3px">
|
||||||
<span style="font-size:9.5px;padding:1px 5px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4);font-family:monospace;white-space:nowrap">{{ $ev }}</span>
|
@foreach($wh->events as $ev)
|
||||||
|
<span style="font-size:9.5px;padding:1px 5px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4);font-family:monospace">{{ $ev }}</span>
|
||||||
@endforeach
|
@endforeach
|
||||||
@if($evRest > 0)
|
|
||||||
<span style="font-size:9.5px;padding:1px 6px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4);white-space:nowrap">+{{ $evRest }}</span>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
{{-- Status --}}
|
</td>
|
||||||
<div class="mw-whl-status">
|
<td class="mbx-td">
|
||||||
@if($wh->is_active)
|
@if($wh->is_active)
|
||||||
<span class="mbx-badge-ok">Aktiv</span>
|
<span class="mbx-badge-ok">Aktiv</span>
|
||||||
@else
|
@else
|
||||||
<span class="mbx-badge-warn">Pausiert</span>
|
<span class="mbx-badge-warn">Pausiert</span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</td>
|
||||||
{{-- HTTP --}}
|
<td class="mbx-td">
|
||||||
<div class="mw-whl-http">
|
|
||||||
@if($wh->last_status === null)
|
@if($wh->last_status === null)
|
||||||
<span style="font-size:11px;color:var(--mw-t4)">—</span>
|
<span style="font-size:11px;color:var(--mw-t4)">—</span>
|
||||||
@elseif($wh->last_status >= 200 && $wh->last_status < 300)
|
@elseif($wh->last_status >= 200 && $wh->last_status < 300)
|
||||||
|
|
@ -116,9 +93,11 @@
|
||||||
@else
|
@else
|
||||||
<span style="font-family:monospace;font-size:11px;color:#f87171;font-weight:600">{{ $wh->last_status }}</span>
|
<span style="font-family:monospace;font-size:11px;color:#f87171;font-weight:600">{{ $wh->last_status }}</span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</td>
|
||||||
{{-- Actions --}}
|
<td class="mbx-td mbx-td-muted" style="font-size:11px">
|
||||||
<div class="mw-whl-actions">
|
{{ $wh->last_triggered_at?->diffForHumans() ?? '—' }}
|
||||||
|
</td>
|
||||||
|
<td class="mbx-td">
|
||||||
<div class="mbx-actions">
|
<div class="mbx-actions">
|
||||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.webhook-edit-modal',arguments:{webhookId:{{ $wh->id }}}})"
|
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.webhook-edit-modal',arguments:{webhookId:{{ $wh->id }}}})"
|
||||||
class="mbx-act-btn" title="Bearbeiten">
|
class="mbx-act-btn" title="Bearbeiten">
|
||||||
|
|
@ -137,10 +116,12 @@
|
||||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,29 +28,12 @@
|
||||||
|
|
||||||
<span class="hidden"></span>
|
<span class="hidden"></span>
|
||||||
|
|
||||||
{{-- <div x-show="show && showActiveComponent"--}}
|
<div x-show="show && showActiveComponent"
|
||||||
{{-- x-bind:class="modalWidth"--}}
|
|
||||||
{{-- id="modal-container"--}}
|
|
||||||
{{-- x-trap.noscroll.inert="show && showActiveComponent"--}}
|
|
||||||
{{-- aria-modal="true"--}}
|
|
||||||
{{-- class="mw-modal-box inline-block w-full sm:max-w-md relative z-10">--}}
|
|
||||||
|
|
||||||
<span class="hidden"></span>
|
|
||||||
|
|
||||||
<div
|
|
||||||
x-show="show && showActiveComponent"
|
|
||||||
x-transition:enter="ease-out duration-300"
|
|
||||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
x-transition:leave="ease-in duration-200"
|
|
||||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
x-bind:class="modalWidth"
|
x-bind:class="modalWidth"
|
||||||
class="mw-modal-box relative inline-block w-full align-bottom rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:w-full"
|
|
||||||
id="modal-container"
|
id="modal-container"
|
||||||
x-trap.inert="show && showActiveComponent"
|
x-trap.noscroll.inert="show && showActiveComponent"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
>
|
class="mw-modal-box inline-block w-full sm:max-w-md relative z-10">
|
||||||
|
|
||||||
<div class="mw-modal-inner">
|
<div class="mw-modal-inner">
|
||||||
@forelse($components as $id => $component)
|
@forelse($components as $id => $component)
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,6 @@ use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
try {
|
|
||||||
$setupDone = \App\Models\Setting::get('setup_completed', '0') === '1';
|
|
||||||
} catch (\Throwable) {
|
|
||||||
$setupDone = false;
|
|
||||||
}
|
|
||||||
if (!$setupDone) {
|
|
||||||
return redirect()->route('setup');
|
|
||||||
}
|
|
||||||
return Auth::check()
|
return Auth::check()
|
||||||
? redirect()->route('ui.dashboard')
|
? redirect()->route('ui.dashboard')
|
||||||
: redirect()->route('login');
|
: redirect()->route('login');
|
||||||
|
|
@ -116,5 +108,7 @@ Route::middleware('guest.only')->group(function () {
|
||||||
Route::get('/signup', [SignUpController::class, 'show'])->middleware('signup.open')->name('signup');
|
Route::get('/signup', [SignUpController::class, 'show'])->middleware('signup.open')->name('signup');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::middleware('auth')->group(function () {
|
||||||
Route::get('/setup', \App\Livewire\Setup\Wizard::class)->name('setup');
|
Route::get('/setup', \App\Livewire\Setup\Wizard::class)->name('setup');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default ({mode}) => {
|
||||||
return defineConfig({
|
return defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
laravel({
|
laravel({
|
||||||
input: ['resources/css/app.css', 'resources/js/app.js', 'resources/js/app-webmail.js'],
|
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||||
refresh: true,
|
refresh: true,
|
||||||
}),
|
}),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
|
|
@ -21,7 +21,6 @@ export default ({mode}) => {
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
strictPort: true,
|
|
||||||
|
|
||||||
hmr: {
|
hmr: {
|
||||||
host: 'ui.dev.mail.nexlab.at',
|
host: 'ui.dev.mail.nexlab.at',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue