- 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>
238 lines
14 KiB
PHP
238 lines
14 KiB
PHP
<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>
|