Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill
- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er) - Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link - Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest - Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen) - Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
86
resources/views/installer/steps/app.blade.php
Normal file
86
resources/views/installer/steps/app.blade.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<x-layouts.installer :currentStep="3">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">App-Einstellungen</h2>
|
||||
<p class="text-sm text-gray-600 mb-4">Gib den Namen deines Vereins und die Administrator-Zugangsdaten ein.</p>
|
||||
|
||||
<form method="POST" action="{{ route('install.app.store') }}">
|
||||
@csrf
|
||||
|
||||
{{-- App settings --}}
|
||||
<div class="space-y-4 mb-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wider">Verein</h3>
|
||||
|
||||
<div>
|
||||
<label for="app_name" class="block text-sm font-medium text-gray-700 mb-1">Vereinsname / App-Name *</label>
|
||||
<input type="text" name="app_name" id="app_name" value="{{ old('app_name', session('installer.app_name', '')) }}"
|
||||
required maxlength="100" placeholder="z.B. SG Musterstadt Handball"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('app_name') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="app_slogan" class="block text-sm font-medium text-gray-700 mb-1">Slogan</label>
|
||||
<input type="text" name="app_slogan" id="app_slogan" value="{{ old('app_slogan', session('installer.app_slogan', '')) }}"
|
||||
maxlength="255" placeholder="z.B. Gemeinsam stark — auf und neben dem Spielfeld"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="mt-1 text-xs text-gray-400">Optional. Wird auf der Login-Seite und im Footer angezeigt.</p>
|
||||
@error('app_slogan') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="app_url" class="block text-sm font-medium text-gray-700 mb-1">App-URL *</label>
|
||||
<input type="url" name="app_url" id="app_url" value="{{ old('app_url', session('installer.app_url', request()->getSchemeAndHttpHost())) }}"
|
||||
required placeholder="https://handball.example.com"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="mt-1 text-xs text-gray-400">Die URL, unter der die App erreichbar ist.</p>
|
||||
@error('app_url') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Admin credentials --}}
|
||||
<div class="space-y-4 border-t border-gray-200 pt-5">
|
||||
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wider">Administrator-Konto</h3>
|
||||
|
||||
<div>
|
||||
<label for="admin_name" class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
|
||||
<input type="text" name="admin_name" id="admin_name" value="{{ old('admin_name', session('installer.admin_name', '')) }}"
|
||||
required maxlength="255" placeholder="Vor- und Nachname"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('admin_name') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="admin_email" class="block text-sm font-medium text-gray-700 mb-1">E-Mail *</label>
|
||||
<input type="email" name="admin_email" id="admin_email" value="{{ old('admin_email', session('installer.admin_email', '')) }}"
|
||||
required maxlength="255" placeholder="admin@example.com"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('admin_email') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="admin_password" class="block text-sm font-medium text-gray-700 mb-1">Passwort *</label>
|
||||
<input type="password" name="admin_password" id="admin_password"
|
||||
required minlength="8" placeholder="Mindestens 8 Zeichen"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('admin_password') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="admin_password_confirmation" class="block text-sm font-medium text-gray-700 mb-1">Passwort bestätigen *</label>
|
||||
<input type="password" name="admin_password_confirmation" id="admin_password_confirmation"
|
||||
required minlength="8" placeholder="Passwort wiederholen"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-between items-center">
|
||||
<a href="{{ route('install.database') }}"
|
||||
class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition">
|
||||
Zurück
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="px-5 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition">
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</x-layouts.installer>
|
||||
85
resources/views/installer/steps/database.blade.php
Normal file
85
resources/views/installer/steps/database.blade.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<x-layouts.installer :currentStep="2">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Datenbank einrichten</h2>
|
||||
<p class="text-sm text-gray-600 mb-4">Wähle den Datenbanktyp und gib die Verbindungsdaten ein.</p>
|
||||
|
||||
<form method="POST" action="{{ route('install.database.store') }}" x-data="{ driver: @js(old('db_driver', $dbDriver)), submitting: false }" @submit="submitting = true">
|
||||
@csrf
|
||||
|
||||
{{-- Driver selection --}}
|
||||
<div class="mb-5">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Datenbanktyp</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="db_driver" value="sqlite" x-model="driver"
|
||||
class="text-blue-600 focus:ring-blue-500">
|
||||
<span class="text-sm text-gray-800">SQLite</span>
|
||||
<span class="text-xs text-gray-400">(Empfohlen für Einzelbetrieb)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="db_driver" value="mysql" x-model="driver"
|
||||
class="text-blue-600 focus:ring-blue-500">
|
||||
<span class="text-sm text-gray-800">MySQL</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- SQLite info --}}
|
||||
<div x-show="driver === 'sqlite'" x-cloak class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md text-sm text-blue-700">
|
||||
SQLite speichert alle Daten in einer einzelnen Datei. Ideal für kleine bis mittlere Installationen.
|
||||
Es werden keine weiteren Angaben benötigt.
|
||||
</div>
|
||||
|
||||
{{-- MySQL fields --}}
|
||||
<div x-show="driver === 'mysql'" x-cloak class="space-y-4 mb-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="db_host" class="block text-sm font-medium text-gray-700 mb-1">Host</label>
|
||||
<input type="text" name="db_host" id="db_host" value="{{ old('db_host', '127.0.0.1') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('db_host') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="db_port" class="block text-sm font-medium text-gray-700 mb-1">Port</label>
|
||||
<input type="number" name="db_port" id="db_port" value="{{ old('db_port', '3306') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('db_port') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="db_database" class="block text-sm font-medium text-gray-700 mb-1">Datenbankname</label>
|
||||
<input type="text" name="db_database" id="db_database" value="{{ old('db_database') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('db_database') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="db_username" class="block text-sm font-medium text-gray-700 mb-1">Benutzername</label>
|
||||
<input type="text" name="db_username" id="db_username" value="{{ old('db_username') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('db_username') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="db_password" class="block text-sm font-medium text-gray-700 mb-1">Passwort</label>
|
||||
<input type="password" name="db_password" id="db_password" value="{{ old('db_password') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('db_password') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-between items-center">
|
||||
<a href="{{ route('install.requirements') }}"
|
||||
class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition">
|
||||
Zurück
|
||||
</a>
|
||||
<button type="submit" :disabled="submitting"
|
||||
class="px-5 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition disabled:opacity-50 disabled:cursor-wait flex items-center gap-2">
|
||||
<template x-if="submitting">
|
||||
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<span x-text="submitting ? 'Migrationen werden ausgeführt...' : 'Datenbank einrichten'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</x-layouts.installer>
|
||||
154
resources/views/installer/steps/finalize.blade.php
Normal file
154
resources/views/installer/steps/finalize.blade.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<x-layouts.installer :currentStep="5">
|
||||
@if ($installed ?? false)
|
||||
{{-- ══════ SUCCESS PAGE ══════ --}}
|
||||
<div class="text-center mb-4">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 mb-4">
|
||||
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Installation erfolgreich!</h2>
|
||||
<p class="text-sm text-gray-600 mt-1">Die Handball WebApp ist einsatzbereit.</p>
|
||||
</div>
|
||||
|
||||
{{-- Credentials table --}}
|
||||
<div class="bg-gray-50 rounded-md border border-gray-200 p-4 mb-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3">Zugangsdaten</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200">
|
||||
<th class="text-left py-1.5 px-2 font-medium text-gray-600">Rolle</th>
|
||||
<th class="text-left py-1.5 px-2 font-medium text-gray-600">E-Mail</th>
|
||||
<th class="text-left py-1.5 px-2 font-medium text-gray-600">Passwort</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-100 bg-blue-50">
|
||||
<td class="py-1.5 px-2 font-medium text-gray-800">Administrator</td>
|
||||
<td class="py-1.5 px-2 text-gray-700">{{ $adminEmail }}</td>
|
||||
<td class="py-1.5 px-2 text-gray-500 italic">Dein gewähltes Passwort</td>
|
||||
</tr>
|
||||
@if ($installDemo ?? false)
|
||||
<tr class="border-b border-gray-100">
|
||||
<td class="py-1.5 px-2 font-medium text-gray-800">Trainer</td>
|
||||
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">trainer@handball.local</td>
|
||||
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">trainer1234</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-100">
|
||||
<td class="py-1.5 px-2 font-medium text-gray-800">Elternvertretung</td>
|
||||
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">elternvertretung@handball.local</td>
|
||||
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">eltern1234</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-100">
|
||||
<td class="py-1.5 px-2 text-gray-800">Eltern (Beispiel)</td>
|
||||
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">mary.parker@handball.local</td>
|
||||
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">eltern1234</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-1.5 px-2 text-gray-800">Eltern (Beispiel)</td>
|
||||
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">tony.stark@handball.local</td>
|
||||
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">eltern1234</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($installDemo ?? false)
|
||||
<p class="text-xs text-gray-500 mb-4">
|
||||
Die Beispieldaten umfassen ein Demo-Team mit 27 Spielern, 35 Eltern, 8 Events und weitere
|
||||
Testdaten. Alle Demo-Eltern nutzen das Passwort <code class="bg-gray-100 px-1 rounded">eltern1234</code>.
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<div class="text-center">
|
||||
<a href="/login"
|
||||
class="inline-block px-6 py-2.5 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700 transition">
|
||||
Zum Login
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
{{-- ══════ FINALIZE FORM ══════ --}}
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Installation abschließen</h2>
|
||||
|
||||
{{-- Summary --}}
|
||||
<div class="bg-gray-50 rounded-md border border-gray-200 p-4 mb-5">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">Zusammenfassung</h3>
|
||||
<dl class="grid grid-cols-2 gap-y-2 gap-x-4 text-sm">
|
||||
<dt class="text-gray-500">App-Name:</dt>
|
||||
<dd class="text-gray-800 font-medium">{{ $appName }}</dd>
|
||||
<dt class="text-gray-500">Administrator:</dt>
|
||||
<dd class="text-gray-800">{{ $adminName }} ({{ $adminEmail }})</dd>
|
||||
<dt class="text-gray-500">Datenbank:</dt>
|
||||
<dd class="text-gray-800">{{ $dbDriver === 'mysql' ? 'MySQL' : 'SQLite' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('install.finalize.store') }}" x-data="{ submitting: false }" @submit="submitting = true">
|
||||
@csrf
|
||||
|
||||
{{-- Demo data checkbox --}}
|
||||
<div class="mb-5 p-4 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<label class="flex items-start gap-3 cursor-pointer">
|
||||
<input type="checkbox" name="install_demo" value="1" checked
|
||||
class="mt-0.5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-800">Beispieldaten installieren</span>
|
||||
<p class="text-xs text-gray-600 mt-1">
|
||||
Erstellt ein Demo-Team mit 27 Spielern, 35 Eltern-Accounts, 8 Events
|
||||
(Training, Heimspiel, Auswärtsspiel, Turnier, Besprechung, etc.),
|
||||
Catering-Einträge, Zeitnehmer, Kommentare und weitere Testdaten.
|
||||
Ideal, um alle Funktionen der App kennenzulernen.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{-- Registration opt-in --}}
|
||||
<div class="mb-5 p-4 bg-green-50 border border-green-200 rounded-md">
|
||||
<label class="flex items-start gap-3 cursor-pointer">
|
||||
<input type="checkbox" name="register_installation" value="1" checked
|
||||
class="mt-0.5 rounded border-gray-300 text-green-600 focus:ring-green-500">
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-800">Installation registrieren</span>
|
||||
<p class="text-xs text-gray-600 mt-1">
|
||||
Registriert diese Installation beim Entwickler. Es werden nur technische Daten
|
||||
übermittelt (App-Name, URL, PHP-Version, App-Version). Keine persönlichen Daten.
|
||||
Ermöglicht Update-Benachrichtigungen und Support-Anfragen.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{-- License key (optional) --}}
|
||||
<div class="mb-5">
|
||||
<label for="license_key" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Lizenzschlüssel <span class="text-gray-400 font-normal">(optional)</span>
|
||||
</label>
|
||||
<input type="text" name="license_key" id="license_key"
|
||||
placeholder="XXXX-XXXX-XXXX-XXXX"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm font-mono focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="text-xs text-gray-400 mt-1">Falls vorhanden. Kann auch später in den Einstellungen eingetragen werden.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<a href="{{ route('install.mail') }}"
|
||||
class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition">
|
||||
Zurück
|
||||
</a>
|
||||
<button type="submit" :disabled="submitting"
|
||||
class="px-5 py-2 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700 transition disabled:opacity-50 disabled:cursor-wait flex items-center gap-2">
|
||||
<template x-if="submitting">
|
||||
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<span x-text="submitting ? 'Installation läuft...' : 'Installation abschließen'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</x-layouts.installer>
|
||||
237
resources/views/installer/steps/mail.blade.php
Normal file
237
resources/views/installer/steps/mail.blade.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<x-layouts.installer :currentStep="4">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-2">E-Mail-Konfiguration</h2>
|
||||
<p class="text-sm text-gray-600 mb-5">
|
||||
Damit Funktionen wie "Passwort vergessen" funktionieren, muss ein E-Mail-Versand konfiguriert werden.
|
||||
Du kannst dies auch spaeter in den Admin-Einstellungen aendern.
|
||||
</p>
|
||||
|
||||
<form method="POST" action="{{ route('install.mail.store') }}"
|
||||
x-data="{
|
||||
mailMode: '{{ old('mail_mode', 'smtp') }}',
|
||||
editor: null,
|
||||
testing: false,
|
||||
testResult: false,
|
||||
testSuccess: false,
|
||||
testMessage: '',
|
||||
syncEditor() {
|
||||
if (this.editor) {
|
||||
document.getElementById('input-pw-reset-de').value = this.editor.root.innerHTML;
|
||||
}
|
||||
},
|
||||
async testSmtp() {
|
||||
this.testing = true;
|
||||
this.testResult = false;
|
||||
try {
|
||||
const res = await fetch('{{ route("install.test-mail") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mail_host: document.getElementById('mail_host').value,
|
||||
mail_port: document.getElementById('mail_port').value,
|
||||
mail_username: document.getElementById('mail_username').value,
|
||||
mail_password: document.getElementById('mail_password').value,
|
||||
mail_encryption: document.getElementById('mail_encryption').value,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
this.testSuccess = data.success;
|
||||
this.testMessage = data.message;
|
||||
} catch (e) {
|
||||
this.testSuccess = false;
|
||||
this.testMessage = 'Netzwerkfehler: ' + e.message;
|
||||
}
|
||||
this.testing = false;
|
||||
this.testResult = true;
|
||||
}
|
||||
}"
|
||||
@submit="syncEditor()">
|
||||
@csrf
|
||||
|
||||
{{-- Mail-Modus --}}
|
||||
<div class="mb-5">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">Versandmethode</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-start gap-3 p-3 border rounded-md cursor-pointer transition"
|
||||
:class="mailMode === 'smtp' ? 'border-blue-400 bg-blue-50' : 'border-gray-200 hover:bg-gray-50'">
|
||||
<input type="radio" name="mail_mode" value="smtp" x-model="mailMode"
|
||||
class="mt-0.5 text-blue-600 focus:ring-blue-500">
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-800">SMTP-Server</span>
|
||||
<p class="text-xs text-gray-500 mt-0.5">E-Mails werden ueber einen SMTP-Server versendet (empfohlen).</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-start gap-3 p-3 border rounded-md cursor-pointer transition"
|
||||
:class="mailMode === 'log' ? 'border-blue-400 bg-blue-50' : 'border-gray-200 hover:bg-gray-50'">
|
||||
<input type="radio" name="mail_mode" value="log" x-model="mailMode"
|
||||
class="mt-0.5 text-blue-600 focus:ring-blue-500">
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-800">Kein Versand (Log)</span>
|
||||
<p class="text-xs text-gray-500 mt-0.5">E-Mails werden nur ins Log geschrieben. Passwort-Reset funktioniert dann nicht.</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- SMTP-Felder --}}
|
||||
<div x-show="mailMode === 'smtp'" x-cloak class="space-y-4 mb-5 p-4 bg-gray-50 border border-gray-200 rounded-md">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="mail_host" class="block text-sm font-medium text-gray-700 mb-1">SMTP-Host</label>
|
||||
<input type="text" name="mail_host" id="mail_host" value="{{ old('mail_host') }}"
|
||||
placeholder="z.B. smtp.strato.de"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('mail_host') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="mail_port" class="block text-sm font-medium text-gray-700 mb-1">Port</label>
|
||||
<input type="number" name="mail_port" id="mail_port" value="{{ old('mail_port', '587') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('mail_port') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="mail_username" class="block text-sm font-medium text-gray-700 mb-1">Benutzername</label>
|
||||
<input type="text" name="mail_username" id="mail_username" value="{{ old('mail_username') }}"
|
||||
placeholder="z.B. noreply@deinverein.de"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('mail_username') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="mail_password" class="block text-sm font-medium text-gray-700 mb-1">Passwort</label>
|
||||
<input type="password" name="mail_password" id="mail_password" value="{{ old('mail_password') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('mail_password') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="mail_from_address" class="block text-sm font-medium text-gray-700 mb-1">Absender-Adresse</label>
|
||||
<input type="email" name="mail_from_address" id="mail_from_address" value="{{ old('mail_from_address') }}"
|
||||
placeholder="z.B. noreply@deinverein.de"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
@error('mail_from_address') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="mail_from_name" class="block text-sm font-medium text-gray-700 mb-1">Absender-Name <span class="text-gray-400 font-normal">(optional)</span></label>
|
||||
<input type="text" name="mail_from_name" id="mail_from_name" value="{{ old('mail_from_name') }}"
|
||||
placeholder="z.B. SG Woelfe Handball"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="mail_encryption" class="block text-sm font-medium text-gray-700 mb-1">Verschluesselung</label>
|
||||
<select name="mail_encryption" id="mail_encryption"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="tls" {{ old('mail_encryption', 'tls') === 'tls' ? 'selected' : '' }}>TLS (Port 587, empfohlen)</option>
|
||||
<option value="ssl" {{ old('mail_encryption') === 'ssl' ? 'selected' : '' }}>SSL (Port 465)</option>
|
||||
<option value="none" {{ old('mail_encryption') === 'none' ? 'selected' : '' }}>Keine</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- SMTP-Test --}}
|
||||
<div class="pt-2 border-t border-gray-200">
|
||||
<button type="button" @click="testSmtp()"
|
||||
:disabled="testing"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-emerald-600 rounded-md hover:bg-emerald-700 transition disabled:opacity-50 disabled:cursor-wait inline-flex items-center gap-2">
|
||||
<template x-if="testing">
|
||||
<svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<span x-text="testing ? 'Teste Verbindung...' : 'Verbindung testen'"></span>
|
||||
</button>
|
||||
<p x-show="testResult" x-cloak x-text="testMessage"
|
||||
:class="testSuccess ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-sm mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Passwort-Reset E-Mail Template --}}
|
||||
<div class="mb-5">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-1">Passwort-Reset E-Mail (Deutsch)</h3>
|
||||
<p class="text-xs text-gray-500 mb-3">
|
||||
Dieser Text wird versendet, wenn ein Benutzer sein Passwort zuruecksetzen moechte.
|
||||
Platzhalter: <code class="bg-gray-100 px-1 rounded">{name}</code>,
|
||||
<code class="bg-gray-100 px-1 rounded">{app_name}</code>.
|
||||
Der Reset-Link wird automatisch als Button angefuegt.
|
||||
</p>
|
||||
<div id="editor-pw-reset" class="bg-white border border-gray-300 rounded-md" style="min-height: 150px;">{!! $defaultPwResetDe !!}</div>
|
||||
<input type="hidden" name="password_reset_email_de" id="input-pw-reset-de" value="{{ old('password_reset_email_de', $defaultPwResetDe) }}">
|
||||
<p class="text-xs text-gray-400 mt-2">Texte fuer weitere Sprachen werden automatisch erstellt und koennen spaeter in den Einstellungen angepasst werden.</p>
|
||||
</div>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<ul class="text-sm text-red-700 list-disc list-inside">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<a href="{{ route('install.app') }}"
|
||||
class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition">
|
||||
Zurueck
|
||||
</a>
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="submit"
|
||||
class="px-5 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition">
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{-- Ueberspringen-Link (separates Formular) --}}
|
||||
<div class="text-center mt-3">
|
||||
<form method="POST" action="{{ route('install.mail.store') }}" class="inline">
|
||||
@csrf
|
||||
<input type="hidden" name="mail_mode" value="log">
|
||||
<button type="submit" class="text-xs text-gray-400 hover:text-gray-600 underline transition">
|
||||
Schritt ueberspringen (kein E-Mail-Versand)
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Quill Editor --}}
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.snow.css" rel="stylesheet"
|
||||
integrity="sha384-cPa8kzsYWhqpAfWOLWYIw3V0BhPi/m3lrd8tBTPxr2NrYCHRVZ7xy1cEoRGOM/03" crossorigin="anonymous">
|
||||
<style>
|
||||
#editor-pw-reset .ql-editor { min-height: 120px; }
|
||||
.ql-toolbar.ql-snow { border-radius: 0.375rem 0.375rem 0 0; border-color: #d1d5db; }
|
||||
.ql-container.ql-snow { border-radius: 0 0 0.375rem 0.375rem; border-color: #d1d5db; }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.min.js"
|
||||
integrity="sha384-QUJ+ckWz1M+a7w0UfG1sEn4pPrbQwSxGm/1TIPyioqXBrwuT9l4f9gdHWLDLbVWI" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const editorEl = document.getElementById('editor-pw-reset');
|
||||
if (!editorEl) return;
|
||||
|
||||
const quill = new Quill('#editor-pw-reset', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'header': [2, 3, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link'],
|
||||
['clean']
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Speichere Referenz fuer Alpine.js syncEditor()
|
||||
const component = document.querySelector('[x-data]').__x.$data;
|
||||
component.editor = quill;
|
||||
});
|
||||
</script>
|
||||
</x-layouts.installer>
|
||||
79
resources/views/installer/steps/requirements.blade.php
Normal file
79
resources/views/installer/steps/requirements.blade.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<x-layouts.installer :currentStep="1">
|
||||
@if (! session('setup_token_hash'))
|
||||
{{-- Token-Eingabe: User muss zuerst das Setup-Token eingeben --}}
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Setup-Token</h2>
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||
<p class="text-sm text-amber-700 mb-3">
|
||||
Zum Schutz der Installation wird ein Setup-Token benoetigt.
|
||||
Du findest es in der Datei <code class="bg-amber-100 px-1 rounded font-mono text-xs">storage/setup-token</code>
|
||||
auf dem Server (per FTP oder Dateimanager).
|
||||
</p>
|
||||
<form method="GET" action="{{ route('install.requirements') }}" class="flex gap-2">
|
||||
<input type="text" name="token"
|
||||
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Token eingeben..." required autofocus>
|
||||
<button type="submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition">
|
||||
Bestaetigen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@else
|
||||
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Systemvoraussetzungen</h2>
|
||||
<p class="text-sm text-gray-600 mb-4">Bitte stelle sicher, dass alle Voraussetzungen erfüllt sind, bevor du fortfährst.</p>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200">
|
||||
<th class="text-left py-2 px-2 font-medium text-gray-700">Prüfung</th>
|
||||
<th class="text-left py-2 px-2 font-medium text-gray-700">Status</th>
|
||||
<th class="text-center py-2 px-1 font-medium text-gray-700 w-10">OK</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($checks as $check)
|
||||
<tr class="border-b border-gray-100 {{ $check['passed'] ? '' : ($check['required'] ? 'bg-red-50' : 'bg-yellow-50') }}">
|
||||
<td class="py-2 px-2 text-gray-800">
|
||||
{{ $check['name'] }}
|
||||
@if (! $check['required'])
|
||||
<span class="text-xs text-gray-400">(optional)</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-2 px-2 text-gray-500">{{ $check['current'] }}</td>
|
||||
<td class="py-2 px-1 text-center">
|
||||
@if ($check['passed'])
|
||||
<svg class="w-5 h-5 text-green-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
@elseif ($check['required'])
|
||||
<svg class="w-5 h-5 text-red-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
@else
|
||||
<svg class="w-5 h-5 text-yellow-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-between items-center">
|
||||
<a href="{{ route('install.requirements') }}"
|
||||
class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition">
|
||||
Erneut prüfen
|
||||
</a>
|
||||
|
||||
@if ($allPassed)
|
||||
<a href="{{ route('install.database') }}"
|
||||
class="px-5 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition">
|
||||
Weiter
|
||||
</a>
|
||||
@else
|
||||
<span class="px-5 py-2 text-sm font-medium text-white bg-gray-300 rounded-md cursor-not-allowed">
|
||||
Weiter
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endif
|
||||
</x-layouts.installer>
|
||||
Reference in New Issue
Block a user