Compare commits
No commits in common. "main" and "v1.1.159" have entirely different histories.
|
|
@ -33,7 +33,7 @@ SESSION_ENCRYPT=false
|
|||
SESSION_PATH=/
|
||||
# For cross-subdomain session sharing (e.g. webmail on mail.example.com):
|
||||
# SESSION_DOMAIN=.example.com
|
||||
SESSION_DOMAIN=
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
#BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/backups
|
||||
/storage/pail
|
||||
/vendor
|
||||
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.
|
||||
|
|
@ -26,6 +26,7 @@ class WizardDomains extends Command
|
|||
|
||||
@mkdir(self::STATE_DIR, 0755, true);
|
||||
|
||||
// Start: alle auf pending
|
||||
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||
file_put_contents(self::STATE_DIR . "/{$key}", 'pending');
|
||||
}
|
||||
|
|
@ -33,7 +34,6 @@ class WizardDomains extends Command
|
|||
$domains = ['ui' => $ui, 'mail' => $mail, 'webmail' => $webmail];
|
||||
$allOk = true;
|
||||
|
||||
// DNS prüfen
|
||||
foreach ($domains as $key => $domain) {
|
||||
if (!$domain) {
|
||||
file_put_contents(self::STATE_DIR . "/{$key}", 'skip');
|
||||
|
|
@ -42,75 +42,47 @@ class WizardDomains extends Command
|
|||
|
||||
file_put_contents(self::STATE_DIR . "/{$key}", 'running');
|
||||
|
||||
// DNS prüfen
|
||||
$hasDns = checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA');
|
||||
if (!$hasDns) {
|
||||
file_put_contents(self::STATE_DIR . "/{$key}", 'nodns');
|
||||
$allOk = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$allOk) {
|
||||
file_put_contents(self::STATE_DIR . '/done', '0');
|
||||
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(
|
||||
'sudo -n %s --ui-host %s --webmail-host %s --mail-host %s --ssl-auto %d',
|
||||
escapeshellarg($helper),
|
||||
escapeshellarg($ui),
|
||||
escapeshellarg($webmail),
|
||||
escapeshellarg($mail),
|
||||
$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');
|
||||
// SSL-Zertifikat anfordern
|
||||
if ($ssl) {
|
||||
$out = shell_exec(sprintf(
|
||||
'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');
|
||||
}
|
||||
|
||||
// done-Datei: Shell-Script schreibt "1"/"0"; Fallback wenn Script abstürzte.
|
||||
$doneVal = trim((string) @file_get_contents(self::STATE_DIR . '/done'));
|
||||
if ($doneVal === '') {
|
||||
file_put_contents(self::STATE_DIR . '/done', '0');
|
||||
$doneVal = '0';
|
||||
// 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($ui),
|
||||
escapeshellarg($webmail),
|
||||
escapeshellarg($mail),
|
||||
$ssl ? 1 : 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
|
||||
|
||||
file_put_contents(self::STATE_DIR . '/done', $allOk ? '1' : '0');
|
||||
Setting::set('ssl_configured', $allOk ? '1' : '0');
|
||||
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 $password = '';
|
||||
public bool $remember = false;
|
||||
public ?string $error = null;
|
||||
public bool $show = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -45,18 +45,14 @@ class Wizard extends Component
|
|||
|
||||
private const STATE_DIR = '/var/lib/mailwolt/wizard';
|
||||
|
||||
public function mount()
|
||||
public function mount(): void
|
||||
{
|
||||
$this->instance_name = config('app.name', 'Mailwolt');
|
||||
try {
|
||||
$this->timezone = Setting::get('timezone', 'Europe/Berlin');
|
||||
$this->locale = Setting::get('locale', 'de');
|
||||
$this->ui_domain = Setting::get('ui_domain', '');
|
||||
$this->mail_domain = Setting::get('mail_domain', '');
|
||||
$this->webmail_domain = Setting::get('webmail_domain', '');
|
||||
} catch (\Throwable) {
|
||||
// DB noch nicht migriert — Standardwerte bleiben
|
||||
}
|
||||
$this->instance_name = config('app.name', 'Mailwolt');
|
||||
$this->timezone = Setting::get('timezone', 'Europe/Berlin');
|
||||
$this->locale = Setting::get('locale', 'de');
|
||||
$this->ui_domain = Setting::get('ui_domain', '');
|
||||
$this->mail_domain = Setting::get('mail_domain', '');
|
||||
$this->webmail_domain = Setting::get('webmail_domain', '');
|
||||
}
|
||||
|
||||
public function updatedUiDomain(): void { $this->fillEmptyDomains($this->ui_domain); }
|
||||
|
|
@ -94,10 +90,10 @@ class Wizard extends Component
|
|||
3 => $this->validate([
|
||||
'admin_name' => 'required|string|min:2|max:64',
|
||||
'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.min' => 'Mindestens 6 Zeichen.',
|
||||
'admin_password.min' => 'Mindestens 10 Zeichen.',
|
||||
'admin_password.same' => 'Passwörter stimmen nicht überein.',
|
||||
]),
|
||||
default => null,
|
||||
|
|
@ -116,7 +112,7 @@ class Wizard extends Component
|
|||
$this->validate([
|
||||
'admin_name' => 'required|string|min:2|max:64',
|
||||
'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
|
||||
|
|
@ -152,7 +148,7 @@ class Wizard extends Component
|
|||
file_put_contents(self::STATE_DIR . "/{$k}", 'pending');
|
||||
}
|
||||
|
||||
$ssl = $this->skipSsl ? 0 : 1;
|
||||
$ssl = (!$this->skipSsl && app()->isProduction()) ? 1 : 0;
|
||||
$artisan = base_path('artisan');
|
||||
$cmd = sprintf(
|
||||
'nohup php %s mailwolt:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=%d > /dev/null 2>&1 &',
|
||||
|
|
@ -169,8 +165,6 @@ class Wizard extends Component
|
|||
|
||||
public function pollSetup(): void
|
||||
{
|
||||
if ($this->setupDone) return;
|
||||
|
||||
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||
$file = self::STATE_DIR . "/{$key}";
|
||||
$this->domainStatus[$key] = is_readable($file)
|
||||
|
|
@ -184,36 +178,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
|
||||
{
|
||||
$sslOk = Setting::get('ssl_configured', '0') === '1' && $this->ui_domain;
|
||||
$url = $sslOk
|
||||
? 'https://' . $this->ui_domain . '/login'
|
||||
: '/login';
|
||||
return redirect()->to($url)->with('setup_done', true);
|
||||
return redirect()->route('login')->with('setup_done', true);
|
||||
}
|
||||
|
||||
private function writeEnv(array $values): void
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use App\Models\BackupJob;
|
|||
use App\Models\BackupPolicy;
|
||||
use App\Models\Domain;
|
||||
use App\Models\MailUser;
|
||||
use App\Models\SandboxRoute;
|
||||
use App\Models\Setting as SettingModel;
|
||||
use App\Support\CacheVer;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
|
@ -44,8 +43,7 @@ class Dashboard extends Component
|
|||
'mailboxCount' => MailUser::where('is_system', false)->where('is_active', true)->count(),
|
||||
'servicesActive' => $servicesActive,
|
||||
'servicesTotal' => count($services),
|
||||
'alertCount' => SandboxRoute::where('is_active', true)->count(),
|
||||
'sandboxAlerts' => SandboxRoute::activeRoutes(),
|
||||
'alertCount' => 0,
|
||||
'backup' => $this->backupData(),
|
||||
'mailHostname' => gethostname() ?: 'mailserver',
|
||||
'services' => $services,
|
||||
|
|
|
|||
|
|
@ -126,20 +126,21 @@ class SearchPaletteModal extends ModalComponent
|
|||
|
||||
public function go(string $type, int $id): void
|
||||
{
|
||||
if ($type === 'domain') {
|
||||
$component = 'ui.domain.modal.domain-edit-modal';
|
||||
$arguments = ['domainId' => $id];
|
||||
} elseif ($type === 'mailbox') {
|
||||
$component = 'ui.mail.modal.mailbox-edit-modal';
|
||||
$arguments = ['mailboxId' => $id];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
// Schließe die Palette …
|
||||
$this->dispatch('closeModal');
|
||||
|
||||
// Search palette schließen, dann nach dem State-Reset (300 ms) das Ziel-Modal öffnen
|
||||
$this->forceClose()->closeModal();
|
||||
$payload = json_encode(['component' => $component, 'arguments' => $arguments]);
|
||||
$this->js("setTimeout(()=>Livewire.dispatch('openModal',{$payload}),350)");
|
||||
// … und navigiere / öffne Kontext:
|
||||
// - Domain → scrolle/markiere Domainkarte
|
||||
// - Mailbox → öffne Bearbeiten-Modal
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -20,22 +20,21 @@ class ApiKeyCreateModal extends ModalComponent
|
|||
'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
|
||||
{
|
||||
$this->validate([
|
||||
'name' => 'required|string|max:80',
|
||||
'selected' => 'required|array|min:1',
|
||||
'name' => 'required|string|max:80',
|
||||
'selected' => 'required|array|min:1',
|
||||
'selected.*' => 'in:' . implode(',', array_keys(self::$availableScopes)),
|
||||
], [
|
||||
'selected.required' => '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;
|
||||
if ($this->sandbox) {
|
||||
|
|
@ -44,10 +43,7 @@ class ApiKeyCreateModal extends ModalComponent
|
|||
}
|
||||
|
||||
$this->dispatch('token-created', plainText: $token->plainTextToken);
|
||||
$this->dispatch('openModal',
|
||||
component: 'ui.system.modal.api-key-show-modal',
|
||||
arguments: ['plainText' => $token->plainTextToken],
|
||||
);
|
||||
$this->closeModal();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
return '2xl';
|
||||
return 'md';
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ class UpdatePage extends Component
|
|||
$rcRaw = @trim(@file_get_contents(self::STATE_DIR . '/rc') ?: '');
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
|
|
@ -255,14 +255,16 @@ class UpdatePage extends Component
|
|||
|
||||
protected function readCurrentVersion(): ?string
|
||||
{
|
||||
// Lokal: direkt aus git describe lesen damit Entwicklungsumgebung immer aktuell ist
|
||||
if (app()->isLocal()) {
|
||||
$tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null'));
|
||||
$v = $this->normalizeVersion($tag);
|
||||
if ($v) return $v;
|
||||
}
|
||||
|
||||
$v = @trim(@file_get_contents(self::VERSION_FILE) ?: '');
|
||||
if ($v !== '') return $v;
|
||||
|
||||
// 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'));
|
||||
$v = $this->normalizeVersion($tag);
|
||||
if ($v) return $v;
|
||||
|
||||
$raw = @trim(@file_get_contents(self::VERSION_FILE_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).'];
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
746
installer.sh
746
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"
|
||||
BAR="──────────────────────────────────────────────────────────────────────────────"
|
||||
|
||||
# ===== Install-Log =====
|
||||
LOG_FILE="/var/log/mailwolt-install.log"
|
||||
> "$LOG_FILE"
|
||||
|
||||
header() {
|
||||
echo -e "${CYAN}${BAR}${NC}"
|
||||
echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}"
|
||||
|
|
@ -67,72 +63,27 @@ footer_ok() {
|
|||
local ip="$1"
|
||||
local app_name="${2:-$APP_NAME}"
|
||||
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 -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 ""
|
||||
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 " Mail-TLS Cert: ${GREY}${cert_dir}/{cert.pem,key.pem}${NC} (Postfix/Dovecot)"
|
||||
echo -e " Postfix/Dovecot: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}"
|
||||
echo -e " Aufruf: ${CYAN}http://${ip}${NC} ${GREY}| https://${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
|
||||
}
|
||||
|
||||
_STEP_T=0
|
||||
_SPIN_PID=
|
||||
|
||||
_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() { :; }
|
||||
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; }; }
|
||||
|
||||
# ===== 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; }
|
||||
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 =====
|
||||
require_root
|
||||
header
|
||||
|
||||
SERVER_IP="$(detect_ip)"
|
||||
APP_PW="${APP_PW:-$(pw)}"
|
||||
MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}" # Wizard setzt später FQDN
|
||||
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; }
|
||||
|
||||
step "Paketquellen aktualisieren" "10 Sek"
|
||||
log "Paketquellen aktualisieren…"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
quietly apt-get update -y
|
||||
ok
|
||||
apt-get update -y
|
||||
|
||||
# ---- MariaDB-Workaround (fix für mariadb-common prompt) ----
|
||||
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
|
||||
|
||||
step "Pakete installieren" "2–5 Min"
|
||||
quietly apt-get -y -o Dpkg::Options::="--force-confdef" \
|
||||
# ---- Basis-Pakete installieren ----
|
||||
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 \
|
||||
postfix postfix-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 \
|
||||
opendkim opendkim-tools \
|
||||
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 \
|
||||
certbot python3-certbot-nginx \
|
||||
fail2ban \
|
||||
ca-certificates rsyslog sudo openssl netcat-openbsd monit git
|
||||
ok
|
||||
ca-certificates rsyslog sudo openssl netcat-openbsd monit
|
||||
|
||||
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
|
||||
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
|
||||
quietly bash -c "id '$APP_USER' >/dev/null 2>&1 || adduser --disabled-password --gecos '' '$APP_USER'"
|
||||
quietly usermod -a -G www-data "$APP_USER"
|
||||
ok
|
||||
|
||||
id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
|
||||
usermod -a -G www-data "$APP_USER"
|
||||
|
||||
# ===== Self-signed TLS (SAN = IP) =====
|
||||
CERT="${CERT_DIR}/cert.pem"
|
||||
|
|
@ -209,7 +153,7 @@ KEY="${CERT_DIR}/key.pem"
|
|||
OSSL_CFG="${CERT_DIR}/openssl.cnf"
|
||||
|
||||
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
|
||||
[req]
|
||||
default_bits = 2048
|
||||
|
|
@ -229,26 +173,26 @@ subjectAltName = @alt_names
|
|||
[alt_names]
|
||||
IP.1 = ${SERVER_IP}
|
||||
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"
|
||||
chmod 600 "$KEY"; chmod 644 "$CERT"
|
||||
ok
|
||||
fi
|
||||
|
||||
# ===== MariaDB vorbereiten =====
|
||||
step "Datenbank einrichten" "10 Sek"
|
||||
quietly systemctl enable --now mariadb
|
||||
log "MariaDB vorbereiten…"
|
||||
systemctl enable --now mariadb
|
||||
DB_NAME="${DB_USER}"
|
||||
DB_USER="${DB_USER}"
|
||||
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 USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
||||
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
SQL
|
||||
ok
|
||||
|
||||
# ===== Postfix konfigurieren (25/465/587) =====
|
||||
step "Mailserver konfigurieren (Postfix / Dovecot / Rspamd)" "15 Sek"
|
||||
log "Postfix konfigurieren…"
|
||||
postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
||||
postconf -e "myorigin = \$myhostname"
|
||||
postconf -e "mydestination = "
|
||||
|
|
@ -327,9 +271,10 @@ CONF
|
|||
chown root:postfix /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) =====
|
||||
log "Dovecot konfigurieren…"
|
||||
cat > /etc/dovecot/dovecot.conf <<'CONF'
|
||||
!include_try /etc/dovecot/conf.d/*.conf
|
||||
CONF
|
||||
|
|
@ -405,14 +350,15 @@ ssl_cert = <${CERT}
|
|||
ssl_key = <${KEY}
|
||||
CONF
|
||||
|
||||
try_quiet systemctl enable --now dovecot
|
||||
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
|
||||
try_quiet systemctl enable --now rspamd
|
||||
systemctl enable --now rspamd || true
|
||||
|
||||
cat > /etc/opendkim.conf <<'CONF'
|
||||
Syslog yes
|
||||
|
|
@ -428,17 +374,17 @@ LogWhy yes
|
|||
OversignHeaders From
|
||||
# KeyTable / SigningTable später im Wizard
|
||||
CONF
|
||||
try_quiet systemctl enable --now opendkim
|
||||
try_quiet systemctl enable --now redis-server
|
||||
ok
|
||||
systemctl enable --now opendkim || true
|
||||
|
||||
# ===== Redis =====
|
||||
systemctl enable --now redis-server
|
||||
|
||||
# ===== 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
|
||||
|
||||
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"
|
||||
PHP_FPM_SOCK="/run/php/php-fpm.sock"
|
||||
[ -S "/run/php/php8.2-fpm.sock" ] && PHP_FPM_SOCK="/run/php/php8.2-fpm.sock"
|
||||
|
||||
cat > ${NGINX_SITE} <<CONF
|
||||
server {
|
||||
|
|
@ -452,10 +398,6 @@ server {
|
|||
access_log /var/log/nginx/${APP_USER}_access.log;
|
||||
error_log /var/log/nginx/${APP_USER}_error.log;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/letsencrypt;
|
||||
try_files \$uri =404;
|
||||
}
|
||||
location / {
|
||||
try_files \$uri \$uri/ /index.php?\$query_string;
|
||||
}
|
||||
|
|
@ -464,7 +406,38 @@ server {
|
|||
fastcgi_pass unix:${PHP_FPM_SOCK};
|
||||
}
|
||||
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)\$ {
|
||||
expires 30d;
|
||||
|
|
@ -473,396 +446,176 @@ server {
|
|||
}
|
||||
CONF
|
||||
ln -sf ${NGINX_SITE} ${NGINX_SITE_LINK}
|
||||
try_quiet nginx -t
|
||||
try_quiet systemctl enable --now nginx
|
||||
ok
|
||||
nginx -t && systemctl enable --now nginx
|
||||
|
||||
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")"
|
||||
chown "$APP_USER":$APP_GROUP "$(dirname "$APP_DIR")"
|
||||
chown -R "$APP_USER":$APP_GROUP "$(dirname "$APP_DIR")"
|
||||
|
||||
if [ ! -d "${APP_DIR}/.git" ]; then
|
||||
rm -rf "${APP_DIR}"
|
||||
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"
|
||||
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
|
||||
ok
|
||||
|
||||
# ===== .env erstellen und befüllen =====
|
||||
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} && 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_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_DEBUG=.*|APP_DEBUG=${APP_DEBUG}|g" "${APP_DIR}/.env"
|
||||
|
||||
sed -i "s|^DB_CONNECTION=.*|DB_CONNECTION=mysql|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*DB_HOST=.*|DB_HOST=127.0.0.1|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*DB_PORT=.*|DB_PORT=3306|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*DB_DATABASE=.*|DB_DATABASE=${DB_NAME}|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*DB_USERNAME=.*|DB_USERNAME=${DB_USER}|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*DB_PASSWORD=.*|DB_PASSWORD=${DB_PASS}|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*DB_HOST=.*|DB_HOST=127.0.0.1|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*DB_PORT=.*|DB_PORT=3306|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*DB_DATABASE=.*|DB_DATABASE=${DB_NAME}|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*DB_USERNAME=.*|DB_USERNAME=${DB_USER}|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*DB_PASSWORD=.*|DB_PASSWORD=${DB_PASS}|g" "${APP_DIR}/.env"
|
||||
|
||||
sed -i "s|^CACHE_DRIVER=.*|CACHE_DRIVER=redis|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*CACHE_PREFIX=.*|CACHE_PREFIX=${APP_USER}|g" "${APP_DIR}/.env"
|
||||
sed -i -E "s|^[#[:space:]]*CACHE_PREFIX=.*|CACHE_PREFIX=${APP_USER}|g" "${APP_DIR}/.env"
|
||||
|
||||
sed -i "s|^SESSION_DRIVER=.*|SESSION_DRIVER=redis|g" "${APP_DIR}/.env"
|
||||
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_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_EMAIL="${APP_USER}@localhost"
|
||||
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")"
|
||||
|
||||
grep -q '^SETUP_PHASE=' "${APP_DIR}/.env" \
|
||||
|| echo "SETUP_PHASE=bootstrap" >> "${APP_DIR}/.env"
|
||||
grep -q '^SETUP_PHASE=' "${APP_DIR}/.env" || echo "SETUP_PHASE=bootstrap" >> "${APP_DIR}/.env"
|
||||
sed -i "s|^SETUP_PHASE=.*|SETUP_PHASE=bootstrap|g" "${APP_DIR}/.env"
|
||||
|
||||
grep -q '^BOOTSTRAP_ADMIN_USER=' "${APP_DIR}/.env" \
|
||||
|| echo "BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}" >> "${APP_DIR}/.env"
|
||||
grep -q '^BOOTSTRAP_ADMIN_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"
|
||||
|
||||
grep -q '^BOOTSTRAP_ADMIN_EMAIL=' "${APP_DIR}/.env" \
|
||||
|| echo "BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}" >> "${APP_DIR}/.env"
|
||||
grep -q '^BOOTSTRAP_ADMIN_EMAIL=' "${APP_DIR}/.env" || echo "BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}" >> "$>
|
||||
sed -i "s|^BOOTSTRAP_ADMIN_EMAIL=.*|BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}|g" "${APP_DIR}/.env"
|
||||
|
||||
grep -q '^BOOTSTRAP_ADMIN_PASSWORD_HASH=' "${APP_DIR}/.env" \
|
||||
|| echo "BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}" >> "${APP_DIR}/.env"
|
||||
grep -q '^BOOTSTRAP_ADMIN_PASSWORD_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"
|
||||
|
||||
step "PHP-Abhängigkeiten installieren (Composer)" "1–2 Min"
|
||||
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-dev --optimize-autoloader --no-interaction"
|
||||
ok
|
||||
# ===== Node/NPM installieren (für Vite/Tailwind Build) =====
|
||||
if [ "$NODE_SETUP" = "nodesource" ]; then
|
||||
# LTS via NodeSource (empfohlen für aktuelle LTS)
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt-get install -y nodejs
|
||||
else
|
||||
# Debian-Repo (ok für Basics, aber u.U. älter)
|
||||
apt-get install -y nodejs npm
|
||||
fi
|
||||
|
||||
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
|
||||
# ===== Projekt aus Git holen (PLATZHALTER) =====
|
||||
# Falls dein Repo später bereitsteht, überschreibt dieser Block das leere/Standard-Laravel.
|
||||
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
|
||||
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
|
||||
|
||||
# ===== Frontend Build (nur wenn package.json existiert) =====
|
||||
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
|
||||
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"
|
||||
quietly apt-get install -y nodejs
|
||||
else
|
||||
quietly apt-get install -y nodejs npm
|
||||
fi
|
||||
fi
|
||||
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
|
||||
if ! sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build" >> "$LOG_FILE" 2>&1; then
|
||||
warn "npm run build fehlgeschlagen — manuell nachholen: cd ${APP_DIR} && npm run build"
|
||||
else
|
||||
ok
|
||||
fi
|
||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
|
||||
# Prod-Build (Vite/Tailwind)
|
||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build || npm run build:prod || true"
|
||||
fi
|
||||
|
||||
mkdir -p /var/lib/mailwolt/wizard
|
||||
chown www-data:www-data /var/lib/mailwolt/wizard
|
||||
chmod 775 /var/lib/mailwolt/wizard
|
||||
|
||||
step "Hilfsskripte & Konfiguration installieren" "5 Sek"
|
||||
cat > /usr/local/sbin/mailwolt-apply-domains <<'HELPER'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# ===== App-User/Gruppen & Rechte (am ENDE ausführen) =====
|
||||
APP_USER="${APP_USER:-${APP_NAME}app}"
|
||||
APP_GROUP="${APP_GROUP}"
|
||||
APP_PW="${APP_PW:-changeme123}"
|
||||
APP_DIR="${APP_DIR}"
|
||||
|
||||
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
|
||||
|
||||
# --- 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
|
||||
# Mindestens ein Cert vorhanden → HTTP-Redirect Block
|
||||
cat <<CONF
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name ${UI_HOST} ${WEBMAIL_HOST};
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
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"
|
||||
# User anlegen (nur falls noch nicht vorhanden) + Passwort setzen + Gruppe
|
||||
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
|
||||
echo "${APP_USER}:${APP_PW}" | quietly chpasswd
|
||||
quietly usermod -a -G "$APP_GROUP" "$APP_USER"
|
||||
usermod -a -G "$APP_GROUP" "$APP_USER"
|
||||
|
||||
# Besitz & Rechte
|
||||
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
|
||||
find "$APP_DIR" -type d -exec chmod 775 {} \; 2>>"$LOG_FILE"
|
||||
find "$APP_DIR" -type f -exec chmod 664 {} \; 2>>"$LOG_FILE"
|
||||
find "$APP_DIR" -type d -exec chmod 775 {} \;
|
||||
find "$APP_DIR" -type f -exec chmod 664 {} \;
|
||||
chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache
|
||||
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"
|
||||
try_quiet setfacl -dR -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 \
|
||||
"${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
|
||||
|
||||
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
|
||||
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"
|
||||
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"
|
||||
try_quiet systemctl restart php${PHPV}-fpm
|
||||
systemctl restart php${PHPV}-fpm || true
|
||||
fi
|
||||
|
||||
# 9) Optional: deinem Shell-/IDE-User ebenfalls Schreibrechte geben
|
||||
IDE_USER="${SUDO_USER:-}"
|
||||
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"
|
||||
try_quiet setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR"
|
||||
try_quiet setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR"
|
||||
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 "${YELLOW}[i]${NC} Benutzer '${IDE_USER}' wurde für Schreibzugriff freigeschaltet (ACL + Gruppe ${APP_GROUP})."
|
||||
fi
|
||||
|
||||
try_quiet systemctl reload nginx
|
||||
try_quiet systemctl restart php*-fpm
|
||||
ok
|
||||
# Webstack neu laden
|
||||
systemctl reload nginx || true
|
||||
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'
|
||||
set daemon 60
|
||||
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
|
||||
start program = "/bin/systemctl start postfix"
|
||||
stop program = "/bin/systemctl stop postfix"
|
||||
if failed host 127.0.0.1 port 25 protocol smtp for 3 cycles then restart
|
||||
if 5 restarts within 10 cycles then alert
|
||||
if failed port 25 protocol smtp then restart
|
||||
|
||||
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"
|
||||
stop program = "/bin/systemctl stop dovecot"
|
||||
if failed host 127.0.0.1 port 143 type tcp for 3 cycles then restart
|
||||
if failed host 127.0.0.1 port 993 type tcpssl for 3 cycles then restart
|
||||
if 5 restarts within 10 cycles then alert
|
||||
if failed port 143 type tcp then restart
|
||||
if failed port 993 type tcp ssl then restart
|
||||
|
||||
check process mariadb matching "mysqld"
|
||||
check process mariadb with pidfile /var/run/mysqld/mysqld.pid
|
||||
start program = "/bin/systemctl start mariadb"
|
||||
stop program = "/bin/systemctl stop mariadb"
|
||||
if failed host 127.0.0.1 port 3306 type tcp for 2 cycles then restart
|
||||
if 5 restarts within 10 cycles then alert
|
||||
if failed port 3306 type tcp then restart
|
||||
|
||||
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 type tcp for 2 cycles then restart
|
||||
if 5 restarts within 10 cycles then alert
|
||||
if failed port 6379 type tcp then restart
|
||||
|
||||
check process rspamd matching "rspamd: main process"
|
||||
start program = "/bin/systemctl start rspamd" with timeout 60 seconds
|
||||
check process rspamd with pidfile /run/rspamd/rspamd.pid
|
||||
start program = "/bin/systemctl start 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 host 127.0.0.1 port 11334 type tcp for 3 cycles then restart
|
||||
if 5 restarts within 10 cycles then alert
|
||||
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 host 127.0.0.1 port 8891 type tcp for 2 cycles 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
|
||||
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 host 127.0.0.1 port 80 type tcp for 2 cycles then restart
|
||||
if 5 restarts within 10 cycles then alert
|
||||
|
||||
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
|
||||
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 || { warn "Monit-Config ungültig — prüfe /etc/monit/monitrc"; }
|
||||
try_quiet systemctl enable --now monit
|
||||
systemctl disable --now monit || true
|
||||
apt-mark hold monit >/dev/null 2>&1 || true
|
||||
|
||||
# ===== Smoke-Test =====
|
||||
step "Dienste prüfen (Port-Check)"
|
||||
_sok=0; _sfail=0
|
||||
|
||||
smoke_smtp() {
|
||||
local port="$1" label="$2"
|
||||
local out
|
||||
out=$(printf "EHLO localhost\r\nQUIT\r\n" | timeout 5s nc -w3 127.0.0.1 "$port" 2>/dev/null || true)
|
||||
if echo "$out" | grep -q '^220'; 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_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 ))"
|
||||
# ===== Smoke-Test (alle Ports, mit Timeouts) =====
|
||||
log "Smoke-Test (Ports & Banner):"
|
||||
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
|
||||
printf "[465] " && timeout 6s openssl s_client -connect 127.0.0.1:465 -brief -quiet </dev/null || echo "[465] Verbindung fehlgeschlagen"
|
||||
printf "[587] " && timeout 6s openssl s_client -starttls smtp -connect 127.0.0.1:587 -brief -quiet </dev/null || echo "[587] Verbindung fehlgeschlagen"
|
||||
printf "[110] " && timeout 6s bash -lc 'printf "QUIT\r\n" | nc -v -w 4 127.0.0.1 110 2>&1' || true
|
||||
printf "[995] " && timeout 6s openssl s_client -connect 127.0.0.1:995 -brief -quiet </dev/null || echo "[995] Verbindung fehlgeschlagen"
|
||||
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 "[993] " && timeout 6s openssl s_client -connect 127.0.0.1:993 -brief -quiet </dev/null || echo "[993] Verbindung fehlgeschlagen"
|
||||
set -e
|
||||
|
||||
echo
|
||||
echo -e " ${GREY}Bootstrap-Login (nur für ERSTEN Login & Wizard):${NC}"
|
||||
echo -e " ${CYAN}User: ${NC}${BOOTSTRAP_USER}"
|
||||
echo -e " ${CYAN}Passwort: ${NC}${BOOTSTRAP_PASS}"
|
||||
echo -e " ${GREY}Log: ${LOG_FILE}${NC}"
|
||||
echo "=============================================================="
|
||||
echo " Bootstrap-Login (nur für ERSTEN Login & Wizard):"
|
||||
echo " User: ${BOOTSTRAP_USER}"
|
||||
echo " Passwort: ${BOOTSTRAP_PASS}"
|
||||
echo "=============================================================="
|
||||
echo
|
||||
|
||||
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,528 @@
|
|||
#!/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 (npm run dev)
|
||||
location = /vite-hmr {
|
||||
proxy_pass http://127.0.0.1:5173/vite-hmr;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_http_version 1.1; 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_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
|
||||
location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_http_version 1.1; 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
|
||||
}
|
||||
|
||||
# ── Builder Webmail: nur /webmail/* erlaubt, root → redirect ───────────────
|
||||
build_webmail_http_only(){
|
||||
local host="$1" outfile="$2" def_flag="${3:-nodefault}"
|
||||
local def=""
|
||||
[[ "${DEV_MODE}" = "1" && "${def_flag}" = "default" ]] && def=" default_server"
|
||||
[[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
||||
cat > "$outfile" <<CONF
|
||||
# --- ${host} : Webmail (domain-routing, volle Laravel-App) ---
|
||||
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;
|
||||
access_log /var/log/nginx/${host/_/__}_webmail_access.log;
|
||||
error_log /var/log/nginx/${host/_/__}_webmail_error.log;
|
||||
client_max_body_size 25m;
|
||||
location = / { return 301 http://\$host/login; }
|
||||
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 ~* \.(css|js|ico|svg|woff2?|ttf|jpg|jpeg|png|gif)\$ { expires 30d; access_log off; }
|
||||
}
|
||||
CONF
|
||||
}
|
||||
|
||||
build_webmail_tls(){
|
||||
local host="$1" cert_dir="$2" outfile="$3"
|
||||
cat > "$outfile" <<CONF
|
||||
# --- ${host} : Webmail TLS (domain-routing, volle Laravel-App) ---
|
||||
server {
|
||||
listen 80; listen [::]:80; server_name ${host};
|
||||
location ^~ /.well-known/acme-challenge/ { root ${ACME_ROOT}; allow all; }
|
||||
return 301 https://\$host\$request_uri;
|
||||
}
|
||||
server {
|
||||
listen 443 ssl${NGINX_HTTP2_SUFFIX}; listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
||||
server_name ${host};
|
||||
ssl_certificate ${cert_dir}/fullchain.pem;
|
||||
ssl_certificate_key ${cert_dir}/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
location ^~ /.well-known/acme-challenge/ { root ${ACME_ROOT}; allow all; }
|
||||
root ${APP_DIR}/public;
|
||||
index index.php;
|
||||
access_log /var/log/nginx/${host}_webmail_ssl_access.log;
|
||||
error_log /var/log/nginx/${host}_webmail_ssl_error.log;
|
||||
client_max_body_size 25m;
|
||||
location = / { return 301 https://\$host/login; }
|
||||
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 ~* \.(css|js|ico|svg|woff2?|ttf|jpg|jpeg|png|gif)\$ { expires 30d; access_log off; }
|
||||
}
|
||||
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
|
||||
build_site_http_only "_" "$UI_SITE" "default"
|
||||
build_webmail_http_only "_" "$WEBMAIL_SITE" "nodefault"
|
||||
else
|
||||
if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
||||
build_site_http_only "$UI_HOST" "$UI_SITE"
|
||||
build_webmail_http_only "$WEBMAIL_HOST" "$WEBMAIL_SITE"
|
||||
else
|
||||
build_site_tls "$UI_HOST" "/etc/ssl/ui" "$UI_SITE"
|
||||
build_webmail_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,46 @@
|
|||
#!/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 /var/lib/mailwolt/wizard
|
||||
chown www-data:www-data /var/lib/mailwolt/wizard
|
||||
: > "$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 -------------------------------------------------------
|
||||
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}"
|
||||
BRANCH="${BRANCH:-main}"
|
||||
MODE="${UPDATE_MODE:-tags}"
|
||||
ALLOW_DIRTY="${ALLOW_DIRTY:-0}"
|
||||
BRANCH="${BRANCH:-main}" # nur relevant bei UPDATE_MODE=branch
|
||||
MODE="${UPDATE_MODE:-tags}" # tags | branch
|
||||
ALLOW_DIRTY="${ALLOW_DIRTY:-0}" # 1 = Dirty-Working-Tree zulassen
|
||||
|
||||
STATE_DIR="/var/lib/mailwolt/update"
|
||||
LOCK_FILE="/var/run/mailwolt-update.lock"
|
||||
LOG_FILE="/var/log/mailwolt-update.log"
|
||||
|
||||
# npm / CI Defaults
|
||||
# npm / CI Defaults für weniger Lärm
|
||||
export CI=1
|
||||
export NPM_CONFIG_FUND=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)"
|
||||
}
|
||||
|
||||
write_version_files(){
|
||||
write_build_info(){
|
||||
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 '%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() {
|
||||
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; }
|
||||
[[ -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_dirty_check
|
||||
|
||||
|
|
@ -173,7 +136,7 @@ NEW_REV="$OLD_REV"
|
|||
if [[ "$MODE" = "tags" ]]; then
|
||||
# → Neueste Tags holen
|
||||
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
|
||||
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}"
|
||||
|
|
@ -246,6 +209,15 @@ if [[ $NEED_FRONTEND -eq 1 ]]; then
|
|||
fi
|
||||
|
||||
# -------- 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
|
||||
artisan_down
|
||||
MAINTENANCE_ACTIVE=1
|
||||
|
|
@ -282,8 +254,8 @@ if [[ "$MAINTENANCE_ACTIVE" -eq 1 ]]; then
|
|||
MAINTENANCE_ACTIVE=0
|
||||
fi
|
||||
|
||||
# -------- Version + Build-Info ablegen ----------------------------------------
|
||||
# -------- Build-Info ablegen --------------------------------------------------
|
||||
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",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "bash -c 'trap \"rm -f public/hot\" EXIT INT TERM; rm -f public/hot && vite'"
|
||||
"dev": "node -e \"try{require('fs').rmSync('./public/hot')}catch(e){}\" && vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
@import "../fonts/Space/font.css";
|
||||
|
||||
@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 '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
|
@ -100,19 +99,6 @@
|
|||
.mw-divider {
|
||||
@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 ============ */
|
||||
|
|
@ -1107,125 +1093,40 @@ select {
|
|||
/* ── Main ── */
|
||||
.mw-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
|
||||
|
||||
/* ════════════════════════════════════════
|
||||
Topbar — Redesign
|
||||
════════════════════════════════════════ */
|
||||
/* ── Topbar ── */
|
||||
.mw-topbar {
|
||||
height: 54px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid var(--mw-b1);
|
||||
background: var(--mw-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
height: 50px; padding: 0 22px; border-bottom: 1px solid var(--mw-b1);
|
||||
background: var(--mw-bg); display: flex; align-items: center;
|
||||
justify-content: space-between; flex-shrink: 0;
|
||||
}
|
||||
.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-tb-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
.mw-live { display: flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--mw-t3); }
|
||||
.mw-live-dot {
|
||||
width: 6px; height: 6px; border-radius: 50%; background: var(--mw-gr);
|
||||
box-shadow: 0 0 5px rgba(34,197,94,.5); animation: mw-pulse 2s infinite;
|
||||
}
|
||||
@keyframes mw-pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
|
||||
|
||||
.mw-hamburger {
|
||||
display: none;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
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-search {
|
||||
display: flex; align-items: center; gap: 6px; background: var(--mw-bg3);
|
||||
border: 1px solid var(--mw-b1); border-radius: 6px; padding: 5px 10px;
|
||||
font-size: 12px; color: var(--mw-t4); cursor: pointer;
|
||||
}
|
||||
.mw-hamburger:hover { background: var(--mw-bg3); color: var(--mw-t1); }
|
||||
|
||||
.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;
|
||||
.mw-search kbd { font-size: 9.5px; opacity: .5; margin-left: 2px; }
|
||||
.mw-ip {
|
||||
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;
|
||||
}
|
||||
|
||||
/* + Domain Button */
|
||||
.mw-tb-add {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
height: 32px; padding: 0 14px;
|
||||
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-btn-primary {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
background: var(--mw-v); border: none; border-radius: 6px; padding: 6px 14px;
|
||||
font-size: 12.5px; font-weight: 600; color: #fff; cursor: pointer;
|
||||
box-shadow: 0 0 10px rgba(124,58,237,.35);
|
||||
}
|
||||
.mw-tb-add:hover { background: #6d28d9; box-shadow: 0 2px 16px rgba(124,58,237,.5); }
|
||||
.mw-btn-primary:hover { background: #6d28d9; }
|
||||
|
||||
/* ── Content ── */
|
||||
.mw-content { flex: 1; overflow-y: auto; padding: 20px 22px 30px; }
|
||||
|
|
@ -1445,16 +1346,13 @@ select {
|
|||
/* ── Responsive ── */
|
||||
@media (max-width: 1024px) {
|
||||
.mw-content { padding: 16px 18px 24px; }
|
||||
.mw-topbar { padding: 0 16px; }
|
||||
.mw-topbar { padding: 0 18px; }
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.mw-bottom-grid { grid-template-columns: 1fr; }
|
||||
.mw-right-col { flex-direction: row; }
|
||||
/* Hostname auf Tablet kompakter */
|
||||
.mw-tb-host { font-size: 10.5px; padding: 0 8px; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
/* Sidebar off-canvas */
|
||||
.mw-sidebar {
|
||||
position: fixed; left: 0; top: 0; z-index: 50;
|
||||
transform: translateX(-100%); transition: transform .25s;
|
||||
|
|
@ -1465,10 +1363,10 @@ select {
|
|||
background: rgba(0,0,0,.55); z-index: 40;
|
||||
}
|
||||
.mw-sidebar-overlay.open { display: block; }
|
||||
/* Hamburger einblenden */
|
||||
.mw-hamburger { display: flex; }
|
||||
/* Parent bleibt sichtbar (3 Buchstaben) */
|
||||
/* Layout */
|
||||
.mw-hamburger {
|
||||
display: flex !important; background: none; border: none;
|
||||
color: var(--mw-t3); cursor: pointer; padding: 4px; margin-right: 4px;
|
||||
}
|
||||
.mw-metric-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.mw-status-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.mw-shell { display: block; height: 100vh; }
|
||||
|
|
@ -1478,176 +1376,7 @@ select {
|
|||
@media (max-width: 480px) {
|
||||
.mw-metric-grid { grid-template-columns: 1fr; }
|
||||
.mw-status-grid { grid-template-columns: 1fr; }
|
||||
.mw-topbar { padding: 0 12px; gap: 5px; }
|
||||
.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-ip { display: none; }
|
||||
}
|
||||
|
||||
.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 */
|
||||
.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 {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
padding: 6px 14px; border-radius: 7px; font-size: 13px; cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
import Echo from 'laravel-echo';
|
||||
|
||||
import Pusher from 'pusher-js';
|
||||
window.Pusher = Pusher;
|
||||
|
||||
const reverbKey = import.meta.env.VITE_REVERB_APP_KEY;
|
||||
window.Echo = new Echo({
|
||||
broadcaster: 'reverb',
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
|
||||
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
||||
wsPath: '/ws',
|
||||
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
|
||||
enabledTransports: ['ws', 'wss'],
|
||||
});
|
||||
|
||||
if (reverbKey) {
|
||||
window.Pusher = Pusher;
|
||||
window.Echo = new Echo({
|
||||
broadcaster: 'reverb',
|
||||
key: reverbKey,
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
|
||||
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
||||
wsPath: '/ws',
|
||||
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
|
||||
enabledTransports: ['ws', 'wss'],
|
||||
});
|
||||
}
|
||||
|
||||
export default window.Echo ?? null;
|
||||
export default Echo;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,77 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<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)">
|
||||
{{-- resources/views/auth/login.blade.php --}}
|
||||
@extends('layouts.blank')
|
||||
|
||||
<livewire:auth.login-form />
|
||||
@section('title', 'Login')
|
||||
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
@section('content')
|
||||
<div class="flex items-center justify-center p-6 w-full">
|
||||
<livewire:auth.login-form />
|
||||
</div>
|
||||
@endsection
|
||||
{{-- resources/views/auth/login.blade.php --}}
|
||||
{{--@extends('layouts.app')--}}
|
||||
|
||||
{{--@section('title', 'Login')--}}
|
||||
|
||||
{{--@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,119 +1,107 @@
|
|||
{{--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">
|
||||
<nav class="flex h-[71px] items-center justify-between gap-3 px-4">
|
||||
|
||||
{{-- Links: Toggle + Seitentitel --}}
|
||||
<div class="flex min-w-0 flex-1 items-center gap-3">
|
||||
<button class="sidebar-toggle shrink-0 text-white/60 hover:text-white text-2xl">
|
||||
<i class="ph ph-list"></i>
|
||||
</button>
|
||||
<h1 class="truncate font-bold text-xl sm:text-2xl">@yield('header_title')</h1>
|
||||
<nav class="flex h-[71px] #h-[74px] items-center justify-between #p-3">
|
||||
<div class="#flex-1 md:w-3/5 w-full">
|
||||
<div class="relative flex items-center gap-5">
|
||||
<button class="sidebar-toggle translate-0 right-5 block s#m:hidden text-white/60 hover:text-white text-2xl">
|
||||
<i class="ph ph-list"></i>
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="font-bold text-2xl">@yield('header_title')</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Rechts: Suche + Aktionen --}}
|
||||
<div class="flex shrink-0 items-center gap-1.5 sm:gap-2">
|
||||
|
||||
{{-- Suchbutton --}}
|
||||
<div class="flex items-center justify-end gap-2 w-full max-w-96">
|
||||
<button
|
||||
id="openSearchPaletteBtn"
|
||||
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-2.5 py-1.5 text-white/75 hover:text-white hover:border-white/20 sm:px-3"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1.5
|
||||
text-white/75 hover:text-white hover:border-white/20"
|
||||
title="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"
|
||||
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>
|
||||
</button>
|
||||
|
||||
{{-- 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"
|
||||
>
|
||||
<button onclick="Livewire.dispatch('openModal',{component:'ui.domain.modal.domain-create-modal'})">
|
||||
<i class="ph-duotone ph-globe-hemisphere-east"></i>
|
||||
</button>
|
||||
|
||||
@foreach($header as $section)
|
||||
@if(!$section['image'])
|
||||
{{-- Benachrichtigungen --}}
|
||||
<div x-data="{ openMessages: false }" class="relative flex items-center">
|
||||
<button
|
||||
@click="openMessages = !openMessages"
|
||||
class="btn-ghost text-base !p-2"
|
||||
title="Benachrichtigungen"
|
||||
>
|
||||
<i class="ph-duotone ph-bell"></i>
|
||||
</button>
|
||||
<div
|
||||
x-show="openMessages"
|
||||
@click.away="openMessages = false"
|
||||
x-cloak
|
||||
class="popup absolute top-[calc(100%+0.5rem)] right-0 w-56"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
>
|
||||
<div class="flex items-center justify-center h-40 text-sm text-white/50">
|
||||
Keine Nachrichten
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
{{-- Avatar-Menü --}}
|
||||
<div x-data="{ openMenu: false }" class="relative flex items-center">
|
||||
<button
|
||||
@click="openMenu = !openMenu"
|
||||
class="flex items-center gap-1.5 focus:outline-none"
|
||||
>
|
||||
<img
|
||||
class="size-8 rounded-full object-cover shadow-2xl"
|
||||
src="https://i.pravatar.cc/100"
|
||||
alt="Avatar"
|
||||
/>
|
||||
<svg
|
||||
class="size-4 text-white/40 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': openMenu }"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
x-show="openMenu"
|
||||
@click.away="openMenu = false"
|
||||
x-cloak
|
||||
class="popup absolute top-[calc(100%+0.5rem)] right-0 w-48"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
>
|
||||
@foreach($section['sub'] as $sub)
|
||||
<a href="{{ route($sub['route']) }}" class="block px-4 py-2 text-xs hover:bg-white/5">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="ph {{ $sub['icon'] ?? 'ph-circle' }} text-sm text-white/50"></i>
|
||||
<span>{{ $sub['title'] }}</span>
|
||||
<div class="#modal-button #relative #mr-2">
|
||||
<div class="#p-1.5">
|
||||
@if(!$section['image'])
|
||||
<div x-data="{ openMessages: false }" class="relative flex items-center">
|
||||
<button @click="openMessages = !openMessages" class="btn-ghost text-base !p-2"><i class="ph-duotone ph-bell #mr-1"></i></button>
|
||||
<div
|
||||
x-show="openMessages"
|
||||
@click.away="openMessages = false"
|
||||
x-cloak
|
||||
class="popup absolute top-16 right-0 w-48"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<div class="flex items-center justify-center h-40">
|
||||
Keine Nachrichten
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div x-data="{ openMenu: false }" class="relative flex items-center">
|
||||
<button @click="openMenu = !openMenu"
|
||||
class="flex items-center focus:outline-none">
|
||||
<img class="size-8 rounded-full object-cover shadow-2xl"
|
||||
src="https://i.pravatar.cc/100"
|
||||
alt="Avatar"/>
|
||||
<svg
|
||||
class="size-4 ml-2 text-gray-500 transform transition-transform duration-200"
|
||||
:class="{'rotate-180': open}"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
x-show="openMenu"
|
||||
@click.away="openMenu = false"
|
||||
x-cloak
|
||||
class="popup absolute top-16 right-0 w-48"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
@foreach($section['sub'] as $sub)
|
||||
<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">
|
||||
<span>Logo</span>
|
||||
{{-- <span><x-dynamic-component :component="$sub['icon']"--}}
|
||||
{{-- class="!size-4"/></span>--}}
|
||||
<span>{{ $sub['title'] }}</span>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -160,43 +160,23 @@
|
|||
<div class="mw-main">
|
||||
|
||||
<header class="mw-topbar">
|
||||
|
||||
{{-- Links: Hamburger + Titel --}}
|
||||
<div class="mw-tb-left">
|
||||
<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>
|
||||
<div class="mw-breadcrumb">
|
||||
<button class="mw-hamburger" style="display:none" onclick="document.getElementById('mw-sidebar').classList.add('open');document.getElementById('mw-overlay').classList.add('open');">
|
||||
<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>
|
||||
<div class="mw-tb-title">
|
||||
<span class="mw-tb-parent">@hasSection('breadcrumb-parent')@yield('breadcrumb-parent')@else{{ $breadcrumbParent ?? 'Dashboard' }}@endif</span>
|
||||
<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>
|
||||
<span class="mw-tb-current">@hasSection('breadcrumb')@yield('breadcrumb')@else{{ $breadcrumb ?? 'Übersicht' }}@endif</span>
|
||||
</div>
|
||||
@hasSection('breadcrumb-parent')@yield('breadcrumb-parent')@else{{ $breadcrumbParent ?? 'Dashboard' }}@endif
|
||||
<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>
|
||||
<b>@hasSection('breadcrumb')@yield('breadcrumb')@else{{ $breadcrumb ?? 'Übersicht' }}@endif</b>
|
||||
</div>
|
||||
|
||||
{{-- Rechts: Actions --}}
|
||||
<div class="mw-tb-right">
|
||||
|
||||
{{-- 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>
|
||||
<div class="mw-topbar-right">
|
||||
<div class="mw-live"><div class="mw-live-dot"></div>Live</div>
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.search.modal.search-palette-modal'})" class="mw-search">
|
||||
<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>
|
||||
</button>
|
||||
|
||||
{{-- Domain erstellen --}}
|
||||
<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 class="mw-ip">{{ gethostname() ?: '—' }}</div>
|
||||
<button class="mw-btn-primary" onclick="Livewire.dispatch('openModal',{component:'ui.domain.modal.domain-create-modal'})">+ Domain</button>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="mw-content">
|
||||
|
|
|
|||
|
|
@ -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 --}}
|
||||
<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 --}}
|
||||
{{-- Banner (dismissbar) --}}
|
||||
@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">
|
||||
<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>
|
||||
<div>
|
||||
<div style="font-size:12.5px;font-weight:600;color:rgba(134,239,172,.95)">{{ $banner['title'] ?? 'Einrichtung abgeschlossen' }}</div>
|
||||
@if(!empty($banner['message']))
|
||||
<div style="font-size:11.5px;color:rgba(134,239,172,.7);margin-top:2px">{{ $banner['message'] }}</div>
|
||||
@endif
|
||||
<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"
|
||||
role="status" aria-live="polite">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="shrink-0 mt-0.5">
|
||||
<i class="ph ph-check-circle text-emerald-100 text-xl"></i>
|
||||
</div>
|
||||
<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>
|
||||
@endif
|
||||
|
||||
{{-- Karte --}}
|
||||
<div style="background:var(--mw-bg2);border:1px solid var(--mw-b2);border-radius:14px;padding:28px 24px">
|
||||
|
||||
<div style="margin-bottom:20px">
|
||||
<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 class="nx-card w-full max-w-[520px]">
|
||||
<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>
|
||||
|
||||
{{-- Fehler --}}
|
||||
{{-- globale Fehlermeldung --}}
|
||||
@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">
|
||||
<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>
|
||||
<span style="font-size:12px;color:#f87171">{{ $error }}</span>
|
||||
<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">{{ $error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form wire:submit.prevent="login" style="display:flex;flex-direction:column;gap:16px">
|
||||
<form wire:submit.prevent="login" class="space-y-5">
|
||||
|
||||
<div>
|
||||
<label class="mw-modal-label">E-Mail / Benutzername</label>
|
||||
<input type="text"
|
||||
<label class="nx-label" for="email">E-Mail / Benutzername</label>
|
||||
<input id="name" type="name"
|
||||
wire:model.defer="name"
|
||||
autocomplete="username"
|
||||
autofocus
|
||||
class="mw-modal-input @error('name') border-red-500/50 @enderror"
|
||||
placeholder="admin@example.com">
|
||||
@error('name') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||
autocomplete="username" autofocus
|
||||
class="nx-input @error('name') input-error @enderror">
|
||||
@error('name')
|
||||
<p class="field-error #mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="mw-modal-label">Passwort</label>
|
||||
<div style="position:relative">
|
||||
<input id="lf-password"
|
||||
type="password"
|
||||
<label class="nx-label" for="password">Passwort</label>
|
||||
<div class="relative">
|
||||
<input id="password" type="{{ $show ? 'text' : 'password' }}"
|
||||
wire:model.defer="password"
|
||||
autocomplete="current-password"
|
||||
class="mw-modal-input @error('password') border-red-500/50 @enderror"
|
||||
style="padding-right:38px"
|
||||
placeholder="••••••••••">
|
||||
<button type="button"
|
||||
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>
|
||||
class="nx-input pr-10 @error('password') input-error @enderror">
|
||||
<button type="button" class="nx-eye"
|
||||
onclick="togglePassword('password', this)">
|
||||
<i class="ph ph-eye text-white/60"></i>
|
||||
</button>
|
||||
</div>
|
||||
@error('password') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||
@error('password')
|
||||
<p class="field-error mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" wire:model.live="remember" style="display:none" id="lf-remember">
|
||||
<span onclick="document.getElementById('lf-remember').click()"
|
||||
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)' : '' }}">
|
||||
@if($remember)
|
||||
<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>
|
||||
@endif
|
||||
</span>
|
||||
<span style="font-size:12px;color:var(--mw-t4)">Merken</span>
|
||||
</label>
|
||||
<div>
|
||||
<div class="flex items-center justify-between pt-1">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" wire:model.live="remember" class="peer hidden">
|
||||
<span
|
||||
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">
|
||||
<i class="ph ph-check text-emerald-400 text-xs hidden peer-checked:inline nx-check"></i>
|
||||
</span>
|
||||
<span class="text-sm text-white/70">Merken</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
wire:loading.attr="disabled"
|
||||
class="mbx-btn-primary"
|
||||
style="width:100%;justify-content:center;font-size:13px;padding:9px 16px">
|
||||
<span wire:loading.remove wire:target="login">Anmelden</span>
|
||||
<span wire:loading wire:target="login" style="display:none">
|
||||
<span style="display:inline-flex;align-items:center;gap:6px">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style="animation:spin .7s linear infinite;flex-shrink:0">
|
||||
<path d="M10 6A4 4 0 1 1 6 2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
wird geprüft…
|
||||
</span>
|
||||
|
||||
<button type="submit" class="nx-btn w-full" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove>Anmelden</span>
|
||||
<span wire:loading class="inline-flex items-center gap-2">
|
||||
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
|
||||
</svg>
|
||||
wird geprüft…
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if(\App\Models\Setting::signupAllowed())
|
||||
<div style="margin-top:16px;text-align:center;font-size:12px;color:var(--mw-t4)">
|
||||
Noch keinen Account?
|
||||
<a wire:navigate href="{{ route('signup') }}" style="color:#a5b4fc;text-decoration:none">Zur Registrierung</a>
|
||||
<div class="mt-6 text-sm text-white/70">
|
||||
Noch keinen Account? <a wire:navigate href="{{ route('signup') }}" class="nx-link">Zur Registiereung</a>
|
||||
</div>
|
||||
@endif
|
||||
</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
|
||||
</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">
|
||||
@error('admin_password') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
|
|
@ -163,12 +163,9 @@
|
|||
|
||||
{{-- ── Schritt 5: Domain-Setup ── --}}
|
||||
@elseif($step === 5)
|
||||
<div wire:poll.2s="pollSetup"></div>
|
||||
|
||||
@php
|
||||
$anyFailed = collect($domainStatus)->contains(fn($s) => in_array($s, ['error','nodns','noipv6']));
|
||||
$allDone = $setupDone;
|
||||
@endphp
|
||||
@if(!$setupDone)
|
||||
<div wire:poll.2s="pollSetup"></div>
|
||||
@endif
|
||||
|
||||
<div style="margin-bottom:20px">
|
||||
<div style="font-size:15px;font-weight:600;color:var(--mw-t1)">Domains werden eingerichtet</div>
|
||||
|
|
@ -178,90 +175,33 @@
|
|||
@php
|
||||
$labels = ['ui' => $ui_domain, 'mail' => $mail_domain, 'webmail' => $webmail_domain];
|
||||
$statusConfig = [
|
||||
'pending' => [
|
||||
'icon' => '…', 'color' => 'var(--mw-t5)', 'bg' => 'var(--mw-bg3)',
|
||||
'label' => 'Warte …', 'hint' => null,
|
||||
],
|
||||
'running' => [
|
||||
'icon' => '↻', 'color' => '#7dd3fc', 'bg' => 'rgba(14,165,233,.08)',
|
||||
'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,
|
||||
],
|
||||
'pending' => ['icon' => '…', 'color' => 'var(--mw-t5)', 'bg' => 'var(--mw-bg3)', 'label' => 'Warte …'],
|
||||
'running' => ['icon' => '↻', 'color' => '#7dd3fc', 'bg' => 'rgba(14,165,233,.08)', 'label' => 'Wird registriert …', 'spin' => true],
|
||||
'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'],
|
||||
'error' => ['icon' => '✗', 'color' => '#f87171', 'bg' => 'rgba(239,68,68,.07)', 'label' => 'Fehler bei Registrierung'],
|
||||
'skip' => ['icon' => '–', 'color' => 'var(--mw-t4)', 'bg' => 'var(--mw-bg3)', 'label' => 'SSL übersprungen'],
|
||||
];
|
||||
@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)
|
||||
@php
|
||||
$st = $domainStatus[$key] ?? 'pending';
|
||||
$cfg = $statusConfig[$st] ?? $statusConfig['pending'];
|
||||
@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'] }}">
|
||||
<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'] }}
|
||||
</div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<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>
|
||||
<span style="font-size:11.5px;color:{{ $cfg['color'] }};white-space:nowrap">{{ $cfg['label'] }}</span>
|
||||
<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="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'] }}
|
||||
</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 style="flex:1;min-width:0">
|
||||
<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>
|
||||
@endif
|
||||
<span style="font-size:11.5px;color:{{ $cfg['color'] }}">{{ $cfg['label'] }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</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
|
||||
|
||||
</div>
|
||||
|
|
@ -292,12 +232,10 @@
|
|||
</div>
|
||||
@elseif($step === 5 && $setupDone)
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:20px">
|
||||
{{-- Kein wire:click — plain Link damit kein Livewire-POST nötig ist.
|
||||
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">
|
||||
<button wire:click="goToLogin" class="mbx-btn-primary" style="font-size:12.5px;width:fit-content">
|
||||
<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' }}
|
||||
</a>
|
||||
Zum Login
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
|
|
|||
|
|
@ -213,25 +213,8 @@
|
|||
</div>
|
||||
<span class="mw-sc-badge mw-badge-mute">{{ $alertCount }} offen</span>
|
||||
</div>
|
||||
<div class="mw-sc-body">
|
||||
@if($sandboxAlerts->isEmpty())
|
||||
<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 class="mw-sc-body" style="font-style:italic;color:var(--mw-t4);">
|
||||
{{ $alertCount === 0 ? 'Keine Warnungen.' : ($alertCount . ' aktive Warnungen.') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -138,15 +138,6 @@ $_wmUrl = ($_wmSub && $_wmBase)
|
|||
|
||||
<td class="mbx-td">
|
||||
<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">
|
||||
<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"/>
|
||||
|
|
@ -154,7 +145,6 @@ $_wmUrl = ($_wmSub && $_wmBase)
|
|||
<path d="M1.5 5.5h11M1.5 8.5h11" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
<div style="min-width:0">
|
||||
<div class="dom-name">{{ $d->domain }}</div>
|
||||
@if(!empty($d->description))
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
<x-slot:breadcrumb>API Keys</x-slot:breadcrumb>
|
||||
|
||||
<div x-data="{activeGroup: 'mailboxes'}"
|
||||
x-on:token-created.window="$wire.$refresh()"
|
||||
x-on:token-deleted.window="$wire.$refresh()">
|
||||
x-on:token-created.window="$dispatch('openModal', {component: 'ui.system.modal.api-key-show-modal', arguments: {plainText: $event.detail.plainText}})">
|
||||
|
||||
<div class="mbx-page-header">
|
||||
<div class="mbx-page-title">
|
||||
|
|
@ -24,7 +23,7 @@
|
|||
</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 ═══ --}}
|
||||
<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>
|
||||
@else
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="mw-kl-head">
|
||||
<span>Name</span>
|
||||
<span>Scopes</span>
|
||||
<span>Modus</span>
|
||||
<span>Erstellt</span>
|
||||
<span></span>
|
||||
<div class="mbx-table-wrap">
|
||||
<table class="mbx-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="mbx-th">Name</th>
|
||||
<th class="mbx-th">Scopes</th>
|
||||
<th class="mbx-th" style="width:86px">Modus</th>
|
||||
<th class="mbx-th" style="width:120px">Zuletzt genutzt</th>
|
||||
<th class="mbx-th" style="width:110px">Erstellt</th>
|
||||
<th class="mbx-th mbx-th-right" style="width:50px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($tokens as $token)
|
||||
<tr class="mbx-tr">
|
||||
<td class="mbx-td">
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<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">
|
||||
<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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span style="font-size:12.5px;font-weight:500;color:var(--mw-t1)">{{ $token->name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:3px">
|
||||
@foreach($token->abilities as $scope)
|
||||
<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
|
||||
</div>
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
@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">Sandbox</span>
|
||||
@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">Live</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="mbx-td mbx-td-muted" style="font-size:11px">{{ $token->last_used_at?->diffForHumans() ?? '—' }}</td>
|
||||
<td class="mbx-td mbx-td-muted" style="font-size:11px">{{ $token->created_at->format('d.m.Y') }}</td>
|
||||
<td class="mbx-td">
|
||||
<div class="mbx-actions">
|
||||
<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">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Rows --}}
|
||||
@foreach($tokens as $token)
|
||||
@php
|
||||
$abilities = $token->abilities;
|
||||
$visible = array_slice($abilities, 0, 2);
|
||||
$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">
|
||||
<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="M9.5 8.5L14 13" stroke="var(--mw-t3)" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</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>
|
||||
</div>
|
||||
{{-- Scopes --}}
|
||||
<div class="mw-kl-scopes" style="display:flex;flex-wrap:nowrap;gap:3px;align-items:center;overflow:hidden">
|
||||
@foreach($visible as $scope)
|
||||
@php $short = collect(explode(':', $scope))->map(fn($p) => $p[0])->join(':'); @endphp
|
||||
<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>
|
||||
@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>
|
||||
{{-- Modus --}}
|
||||
<div class="mw-kl-modus">
|
||||
@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>
|
||||
@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>
|
||||
@endif
|
||||
</div>
|
||||
{{-- Date (desktop only) --}}
|
||||
<div class="mw-kl-date" style="font-size:11px;color:var(--mw-t4)">{{ $token->created_at->format('d.m.Y') }}</div>
|
||||
{{-- Actions --}}
|
||||
<div class="mw-kl-actions">
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.api-key-delete-modal',arguments:{tokenId:{{ $token->id }}}})"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
|
@ -300,5 +293,4 @@ Content-Type: application/json</pre>
|
|||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,37 +18,33 @@
|
|||
@error('name') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||
</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">
|
||||
<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"
|
||||
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' }}
|
||||
</button>
|
||||
</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)
|
||||
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;padding:5px 8px;border-radius:5px;transition:background .1s"
|
||||
onmouseover="this.style.background='var(--mw-bg3)'"
|
||||
onmouseout="this.style.background='transparent'">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" wire:model="selected" value="{{ $key }}"
|
||||
style="width:13px;height:13px;flex-shrink:0;accent-color:var(--mw-v)">
|
||||
<div>
|
||||
<div style="font-size:11px;color:var(--mw-t2);font-family:monospace;line-height:1.3">{{ $key }}</div>
|
||||
<div style="font-size:10px;color:var(--mw-t4);line-height:1.3">{{ $label }}</div>
|
||||
</div>
|
||||
style="width:13px;height:13px;accent-color:var(--mw-v)">
|
||||
<span style="font-size:12px;color:var(--mw-t2);font-family:monospace">{{ $key }}</span>
|
||||
<span style="font-size:11px;color:var(--mw-t4)">— {{ $label }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
@error('selected') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
|
||||
<div style="margin-top:12px">
|
||||
<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">
|
||||
<input type="checkbox" wire:model="sandbox" style="width:13px;height:13px;flex-shrink:0;accent-color:#f59e0b">
|
||||
<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:flex-start;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" wire:model="sandbox" style="width:13px;height:13px;margin-top:1px;accent-color:#f59e0b">
|
||||
<div>
|
||||
<div style="font-size:12px;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:12.5px;color:var(--mw-t1);font-weight:500">Sandbox-Modus</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>
|
||||
</label>
|
||||
</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 style="font-size:12px;color:var(--mw-t3)">— einmalig anzeigen</span>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -30,16 +30,11 @@
|
|||
</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 --}}
|
||||
<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 ═══ --}}
|
||||
<div class="mw-sandbox-list">
|
||||
<div style="border-right:1px solid var(--mw-b1);overflow-y:auto;max-height:680px">
|
||||
|
||||
@if($mails->isEmpty())
|
||||
<div style="padding:48px 16px;text-align:center">
|
||||
|
|
@ -88,7 +83,7 @@
|
|||
</div>
|
||||
|
||||
{{-- ═══ Right: Mail-Detail ═══ --}}
|
||||
<div class="mw-sandbox-detail">
|
||||
<div style="overflow-y:auto;max-height:680px">
|
||||
|
||||
@if(!$selected)
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mw-postfix-grid">
|
||||
<div style="padding:16px 18px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px">
|
||||
|
||||
<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 class="mw-wh-layout">
|
||||
<div style="display:grid;grid-template-columns:1fr 300px;gap:14px;align-items:start">
|
||||
|
||||
{{-- ═══ Left ═══ --}}
|
||||
<div class="mbx-sections">
|
||||
|
|
@ -45,102 +45,83 @@
|
|||
<div style="font-size:11.5px;color:var(--mw-t4)">Verbinde externe Systeme — sie werden bei Events automatisch benachrichtigt.</div>
|
||||
</div>
|
||||
@else
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="mw-whl-head">
|
||||
<span>Webhook</span>
|
||||
<span>Events</span>
|
||||
<span>Status</span>
|
||||
<span>HTTP</span>
|
||||
<span>Aktionen</span>
|
||||
</div>
|
||||
|
||||
{{-- Rows --}}
|
||||
@foreach($webhooks as $wh)
|
||||
@php
|
||||
$evVisible = array_slice($wh->events, 0, 2);
|
||||
$evRest = count($wh->events) - 2;
|
||||
@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="min-width:0">
|
||||
<div style="font-size:12.5px;font-weight:500;color:var(--mw-t1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
{{ $wh->name }}
|
||||
<span class="mw-whl-status-sm">
|
||||
<div class="mbx-table-wrap">
|
||||
<table class="mbx-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="mbx-th">Webhook</th>
|
||||
<th class="mbx-th">Events</th>
|
||||
<th class="mbx-th" style="width:80px">Status</th>
|
||||
<th class="mbx-th" style="width:90px">HTTP</th>
|
||||
<th class="mbx-th" style="width:130px">Zuletzt ausgelöst</th>
|
||||
<th class="mbx-th mbx-th-right" style="width:80px">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($webhooks as $wh)
|
||||
<tr class="mbx-tr">
|
||||
<td class="mbx-td">
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<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>
|
||||
<div style="font-size:12.5px;font-weight:500;color:var(--mw-t1)">{{ $wh->name }}</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:3px">
|
||||
@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
|
||||
</div>
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
@if($wh->is_active)
|
||||
<span class="mbx-badge-ok" style="font-size:9px;margin-left:4px">Aktiv</span>
|
||||
<span class="mbx-badge-ok">Aktiv</span>
|
||||
@else
|
||||
<span class="mbx-badge-warn" style="font-size:9px;margin-left:4px">Pausiert</span>
|
||||
<span class="mbx-badge-warn">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
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
@if($wh->last_status === null)
|
||||
<span style="font-size:11px;color:var(--mw-t4)">—</span>
|
||||
@elseif($wh->last_status >= 200 && $wh->last_status < 300)
|
||||
<span style="font-family:monospace;font-size:11px;color:#34d399;font-weight:600">{{ $wh->last_status }}</span>
|
||||
@elseif($wh->last_status === 0)
|
||||
<span style="font-family:monospace;font-size:11px;color:#f87171;font-weight:600">Timeout</span>
|
||||
@else
|
||||
<span style="font-family:monospace;font-size:11px;color:#f87171;font-weight:600">{{ $wh->last_status }}</span>
|
||||
@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>
|
||||
{{-- Events --}}
|
||||
<div class="mw-whl-events" style="display:flex;flex-wrap:wrap;gap:3px;align-items:center">
|
||||
@foreach($evVisible 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;white-space:nowrap">{{ $ev }}</span>
|
||||
@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>
|
||||
{{-- Status --}}
|
||||
<div class="mw-whl-status">
|
||||
@if($wh->is_active)
|
||||
<span class="mbx-badge-ok">Aktiv</span>
|
||||
@else
|
||||
<span class="mbx-badge-warn">Pausiert</span>
|
||||
@endif
|
||||
</div>
|
||||
{{-- HTTP --}}
|
||||
<div class="mw-whl-http">
|
||||
@if($wh->last_status === null)
|
||||
<span style="font-size:11px;color:var(--mw-t4)">—</span>
|
||||
@elseif($wh->last_status >= 200 && $wh->last_status < 300)
|
||||
<span style="font-family:monospace;font-size:11px;color:#34d399;font-weight:600">{{ $wh->last_status }}</span>
|
||||
@elseif($wh->last_status === 0)
|
||||
<span style="font-family:monospace;font-size:11px;color:#f87171;font-weight:600">Timeout</span>
|
||||
@else
|
||||
<span style="font-family:monospace;font-size:11px;color:#f87171;font-weight:600">{{ $wh->last_status }}</span>
|
||||
@endif
|
||||
</div>
|
||||
{{-- Actions --}}
|
||||
<div class="mw-whl-actions">
|
||||
<div class="mbx-actions">
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.webhook-edit-modal',arguments:{webhookId:{{ $wh->id }}}})"
|
||||
class="mbx-act-btn" title="Bearbeiten">
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M9 2l2 2-7 7H2V9l7-7Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
<button wire:click="toggleActive({{ $wh->id }})"
|
||||
class="mbx-act-btn" title="{{ $wh->is_active ? 'Pausieren' : 'Aktivieren' }}">
|
||||
@if($wh->is_active)
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><rect x="3" y="2" width="2.5" height="9" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="7.5" y="2" width="2.5" height="9" rx="1" stroke="currentColor" stroke-width="1.2"/></svg>
|
||||
@else
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M4 2.5l7 4-7 4V2.5Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
@endif
|
||||
</button>
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.webhook-delete-modal',arguments:{webhookId:{{ $wh->id }}}})"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="mbx-td mbx-td-muted" style="font-size:11px">
|
||||
{{ $wh->last_triggered_at?->diffForHumans() ?? '—' }}
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
<div class="mbx-actions">
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.webhook-edit-modal',arguments:{webhookId:{{ $wh->id }}}})"
|
||||
class="mbx-act-btn" title="Bearbeiten">
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M9 2l2 2-7 7H2V9l7-7Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
<button wire:click="toggleActive({{ $wh->id }})"
|
||||
class="mbx-act-btn" title="{{ $wh->is_active ? 'Pausieren' : 'Aktivieren' }}">
|
||||
@if($wh->is_active)
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><rect x="3" y="2" width="2.5" height="9" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="7.5" y="2" width="2.5" height="9" rx="1" stroke="currentColor" stroke-width="1.2"/></svg>
|
||||
@else
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M4 2.5l7 4-7 4V2.5Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
@endif
|
||||
</button>
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.webhook-delete-modal',arguments:{webhookId:{{ $wh->id }}}})"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -28,31 +28,14 @@
|
|||
|
||||
<span class="hidden"></span>
|
||||
|
||||
{{-- <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">--}}
|
||||
<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"
|
||||
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"
|
||||
x-trap.inert="show && showActiveComponent"
|
||||
aria-modal="true"
|
||||
>
|
||||
|
||||
<div class="mw-modal-inner">
|
||||
<div class="mw-modal-inner">
|
||||
@forelse($components as $id => $component)
|
||||
<div x-show.immediate="activeComponent == '{{ $id }}'"
|
||||
x-ref="{{ $id }}"
|
||||
|
|
|
|||
|
|
@ -13,14 +13,6 @@ use Illuminate\Support\Facades\Auth;
|
|||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
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()
|
||||
? redirect()->route('ui.dashboard')
|
||||
: 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('/setup', \App\Livewire\Setup\Wizard::class)->name('setup');
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::get('/setup', \App\Livewire\Setup\Wizard::class)->name('setup');
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue