fix: Admin Edit-Button + Email-Kontenverwaltung in Einstellungen

- Fix: Admin Edit/Delete-Buttons reagieren jetzt (@click.stop)
- Neu: Email-Kontenverwaltung in den Benutzer-Einstellungen
  - IMAP/SMTP-Server, Port, SSL, Anmeldedaten konfigurieren
  - Konten hinzufuegen, bearbeiten, loeschen
  - Verbindungstest-Button
  - Standard-Konto festlegen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-11 15:30:29 +02:00
parent 35099de2c5
commit 042a067e81
2 changed files with 252 additions and 7 deletions
+2 -2
View File
@@ -42,8 +42,8 @@
<Column field="storage_quota_mb" header="Quota (MB)" />
<Column header="Aktionen" style="width: 120px">
<template #body="{ data }">
<Button icon="pi pi-pencil" text rounded size="small" @click="openEditUser(data)" />
<Button icon="pi pi-trash" text rounded size="small" severity="danger" @click="confirmDeleteUser(data)" />
<Button icon="pi pi-pencil" text rounded size="small" @click.stop="openEditUser(data)" />
<Button icon="pi pi-trash" text rounded size="small" severity="danger" @click.stop="confirmDeleteUser(data)" />
</template>
</Column>
</DataTable>
+250 -5
View File
@@ -4,6 +4,7 @@
<h2>Einstellungen</h2>
</div>
<!-- Profile -->
<div class="settings-section">
<h3>Profil</h3>
<div class="settings-info">
@@ -22,6 +23,7 @@
</div>
</div>
<!-- Change Password -->
<div class="settings-section">
<h3>Passwort aendern</h3>
<form @submit.prevent="handleChangePassword" class="password-form">
@@ -42,18 +44,130 @@
<Button type="submit" label="Passwort aendern" :loading="pwLoading" />
</form>
</div>
<!-- Email Accounts -->
<div class="settings-section">
<div class="section-header">
<h3>E-Mail-Konten</h3>
<Button icon="pi pi-plus" label="Konto hinzufuegen" size="small" @click="openNewAccount" />
</div>
<div v-if="!emailAccounts.length" class="empty-hint">
<i class="pi pi-envelope"></i>
<p>Keine E-Mail-Konten konfiguriert. Fuege ein Konto hinzu, um den E-Mail-Client zu nutzen.</p>
</div>
<div v-for="account in emailAccounts" :key="account.id" class="email-account-card">
<div class="account-info">
<div class="account-name">
<i class="pi pi-envelope"></i>
<strong>{{ account.display_name }}</strong>
<Tag v-if="account.is_default" value="Standard" size="small" severity="info" />
</div>
<div class="account-detail">{{ account.email_address }}</div>
<div class="account-detail">IMAP: {{ account.imap_host }}:{{ account.imap_port }} | SMTP: {{ account.smtp_host }}:{{ account.smtp_port }}</div>
</div>
<div class="account-actions">
<Button icon="pi pi-pencil" text size="small" @click="openEditAccount(account)" />
<Button icon="pi pi-check-circle" text size="small" title="Verbindung testen" @click="testAccount(account)" />
<Button icon="pi pi-trash" text size="small" severity="danger" @click="confirmDeleteAccount(account)" />
</div>
</div>
</div>
<!-- Email Account Dialog -->
<Dialog v-model:visible="showAccountDialog" :header="editingAccount ? 'E-Mail-Konto bearbeiten' : 'Neues E-Mail-Konto'" modal :style="{ width: '550px' }">
<div class="field">
<label>Anzeigename</label>
<InputText v-model="accountForm.display_name" placeholder="z.B. Arbeit, Privat" fluid autofocus />
</div>
<div class="field">
<label>E-Mail-Adresse</label>
<InputText v-model="accountForm.email_address" type="email" placeholder="name@beispiel.de" fluid />
</div>
<h4 class="section-title">Posteingang (IMAP)</h4>
<div class="field-row">
<div class="field flex-grow">
<label>IMAP-Server</label>
<InputText v-model="accountForm.imap_host" placeholder="imap.beispiel.de" fluid />
</div>
<div class="field" style="width: 100px">
<label>Port</label>
<InputText v-model.number="accountForm.imap_port" type="number" fluid />
</div>
<div class="field" style="width: 80px">
<label>SSL</label>
<InputSwitch v-model="accountForm.imap_ssl" />
</div>
</div>
<h4 class="section-title">Postausgang (SMTP)</h4>
<div class="field-row">
<div class="field flex-grow">
<label>SMTP-Server</label>
<InputText v-model="accountForm.smtp_host" placeholder="smtp.beispiel.de" fluid />
</div>
<div class="field" style="width: 100px">
<label>Port</label>
<InputText v-model.number="accountForm.smtp_port" type="number" fluid />
</div>
<div class="field" style="width: 80px">
<label>SSL</label>
<InputSwitch v-model="accountForm.smtp_ssl" />
</div>
</div>
<h4 class="section-title">Anmeldedaten</h4>
<div class="field">
<label>Benutzername</label>
<InputText v-model="accountForm.username" placeholder="Oft die E-Mail-Adresse" fluid />
</div>
<div class="field">
<label>{{ editingAccount ? 'Passwort (leer = nicht aendern)' : 'Passwort' }}</label>
<Password v-model="accountForm.password" :feedback="false" toggle-mask fluid />
</div>
<div class="field">
<label><input type="checkbox" v-model="accountForm.is_default" /> Als Standard-Konto verwenden</label>
</div>
<Message v-if="accountError" severity="error" :closable="false">{{ accountError }}</Message>
<template #footer>
<Button label="Abbrechen" text @click="showAccountDialog = false" />
<Button :label="editingAccount ? 'Speichern' : 'Hinzufuegen'" @click="saveAccount" :loading="accountSaving" />
</template>
</Dialog>
<!-- Delete Account Confirm -->
<Dialog v-model:visible="showDeleteAccount" header="E-Mail-Konto loeschen" modal :style="{ width: '400px' }">
<p>Moechtest du das Konto <strong>{{ deleteAccountTarget?.display_name }}</strong> ({{ deleteAccountTarget?.email_address }}) wirklich loeschen?</p>
<template #footer>
<Button label="Abbrechen" text @click="showDeleteAccount = false" />
<Button label="Loeschen" severity="danger" @click="doDeleteAccount" />
</template>
</Dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { useAuthStore } from '../stores/auth'
import { useToast } from 'primevue/usetoast'
import apiClient from '../api/client'
import Password from 'primevue/password'
import Button from 'primevue/button'
import Message from 'primevue/message'
import Tag from 'primevue/tag'
import Dialog from 'primevue/dialog'
import InputText from 'primevue/inputtext'
import InputSwitch from 'primevue/inputswitch'
const auth = useAuthStore()
const toast = useToast()
// --- Password change ---
const currentPassword = ref('')
const newPassword = ref('')
const newPassword2 = ref('')
@@ -83,22 +197,153 @@ async function handleChangePassword() {
pwLoading.value = false
}
}
// --- Email Accounts ---
const emailAccounts = ref([])
const showAccountDialog = ref(false)
const editingAccount = ref(null)
const accountForm = ref({
display_name: '', email_address: '', imap_host: '', imap_port: 993, imap_ssl: true,
smtp_host: '', smtp_port: 587, smtp_ssl: true, username: '', password: '', is_default: false,
})
const accountError = ref('')
const accountSaving = ref(false)
const showDeleteAccount = ref(false)
const deleteAccountTarget = ref(null)
function getEncKey() { return auth.masterKeySalt || '' }
async function loadAccounts() {
try {
const res = await apiClient.get('/email/accounts')
emailAccounts.value = res.data
} catch { emailAccounts.value = [] }
}
function openNewAccount() {
editingAccount.value = null
accountForm.value = {
display_name: '', email_address: '', imap_host: '', imap_port: 993, imap_ssl: true,
smtp_host: '', smtp_port: 587, smtp_ssl: true, username: '', password: '', is_default: false,
}
accountError.value = ''
showAccountDialog.value = true
}
function openEditAccount(account) {
editingAccount.value = account
accountForm.value = {
display_name: account.display_name,
email_address: account.email_address,
imap_host: account.imap_host,
imap_port: account.imap_port,
imap_ssl: account.imap_ssl,
smtp_host: account.smtp_host,
smtp_port: account.smtp_port,
smtp_ssl: account.smtp_ssl,
username: account.username,
password: '',
is_default: account.is_default,
}
accountError.value = ''
showAccountDialog.value = true
}
async function saveAccount() {
accountError.value = ''
accountSaving.value = true
try {
const payload = { ...accountForm.value }
const headers = { 'X-Encryption-Key': getEncKey() }
if (editingAccount.value) {
if (!payload.password) delete payload.password
await apiClient.put(`/email/accounts/${editingAccount.value.id}`, payload, { headers })
toast.add({ severity: 'success', summary: 'Konto aktualisiert', life: 3000 })
} else {
if (!payload.password) {
accountError.value = 'Passwort erforderlich'
return
}
await apiClient.post('/email/accounts', payload, { headers })
toast.add({ severity: 'success', summary: 'Konto hinzugefuegt', life: 3000 })
}
showAccountDialog.value = false
await loadAccounts()
await auth.fetchMe()
} catch (err) {
accountError.value = err.response?.data?.error || 'Fehler beim Speichern'
} finally {
accountSaving.value = false
}
}
async function testAccount(account) {
try {
await apiClient.post(`/email/accounts/${account.id}/test`, {}, {
headers: { 'X-Encryption-Key': getEncKey() },
})
toast.add({ severity: 'success', summary: 'Verbindung erfolgreich', detail: `${account.display_name}: IMAP-Verbindung OK`, life: 5000 })
} catch (err) {
toast.add({ severity: 'error', summary: 'Verbindung fehlgeschlagen', detail: err.response?.data?.error, life: 8000 })
}
}
function confirmDeleteAccount(account) {
deleteAccountTarget.value = account
showDeleteAccount.value = true
}
async function doDeleteAccount() {
if (!deleteAccountTarget.value) return
try {
await apiClient.delete(`/email/accounts/${deleteAccountTarget.value.id}`)
showDeleteAccount.value = false
toast.add({ severity: 'success', summary: 'Konto geloescht', life: 3000 })
await loadAccounts()
await auth.fetchMe()
} catch (err) {
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 })
}
}
onMounted(loadAccounts)
</script>
<style scoped>
.view-container { padding: 1.5rem; }
.view-header h2 { margin: 0 0 1.5rem; }
.settings-section {
background: var(--p-surface-0);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
background: var(--p-surface-0); border-radius: 8px; padding: 1.5rem; margin-bottom: 1.5rem;
}
.settings-section h3 { margin: 0 0 1rem; font-size: 1.125rem; }
.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
.section-header h3 { margin: 0; }
.settings-info { display: flex; flex-direction: column; gap: 0.5rem; }
.info-row { display: flex; align-items: center; gap: 0.5rem; }
.info-row .label { font-weight: 500; min-width: 120px; }
.password-form { max-width: 400px; }
.password-form .field { margin-bottom: 1rem; }
.password-form .field label { display: block; margin-bottom: 0.5rem; font-weight: 500; font-size: 0.875rem; }
.empty-hint {
display: flex; align-items: center; gap: 0.75rem; padding: 1.5rem;
background: var(--p-surface-50); border-radius: 6px; color: var(--p-text-muted-color);
}
.empty-hint i { font-size: 1.5rem; }
.empty-hint p { margin: 0; }
.email-account-card {
display: flex; align-items: center; justify-content: space-between;
padding: 1rem; border: 1px solid var(--p-surface-200); border-radius: 8px; margin-bottom: 0.5rem;
}
.account-name { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem; }
.account-detail { font-size: 0.825rem; color: var(--p-text-muted-color); }
.account-actions { display: flex; gap: 0; flex-shrink: 0; }
.section-title { margin: 1rem 0 0.5rem; font-size: 0.95rem; font-weight: 600; }
.field { margin-bottom: 1rem; }
.field label { display: block; margin-bottom: 0.5rem; font-weight: 500; font-size: 0.875rem; }
.field-row { display: flex; gap: 0.75rem; align-items: flex-end; }
.flex-grow { flex: 1; }
</style>