29cc00e284
Upload-Auth: - Akzeptiert jetzt sowohl SECRET_KEY als auch JWT_SECRET_KEY (BUILD_UPLOAD_TOKEN in Entwicklungs-.env kann einer von beiden sein) Settings-View: - Zeigt verfuegbare Desktop/Mobile Clients zum Download an (nur wenn mindestens ein Client vorhanden) - Pro Client: Name, Dateiname, Download-Button .env.example: - Klarere Kommentare: "SECRET_KEY oder JWT_SECRET_KEY des Zielservers" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
389 lines
14 KiB
Vue
389 lines
14 KiB
Vue
<template>
|
|
<div class="view-container">
|
|
<div class="view-header">
|
|
<h2>Einstellungen</h2>
|
|
</div>
|
|
|
|
<!-- Profile -->
|
|
<div class="settings-section">
|
|
<h3>Profil</h3>
|
|
<div class="settings-info">
|
|
<div class="info-row">
|
|
<span class="label">Benutzername:</span>
|
|
<span>{{ auth.user?.username }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="label">E-Mail:</span>
|
|
<span>{{ auth.user?.email || 'Nicht angegeben' }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="label">Rolle:</span>
|
|
<Tag :value="auth.user?.role" :severity="auth.user?.role === 'admin' ? 'danger' : 'info'" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Change Password -->
|
|
<div class="settings-section">
|
|
<h3>Passwort aendern</h3>
|
|
<form @submit.prevent="handleChangePassword" class="password-form">
|
|
<div class="field">
|
|
<label>Aktuelles Passwort</label>
|
|
<Password v-model="currentPassword" :feedback="false" toggle-mask fluid />
|
|
</div>
|
|
<div class="field">
|
|
<label>Neues Passwort</label>
|
|
<Password v-model="newPassword" toggle-mask fluid />
|
|
</div>
|
|
<div class="field">
|
|
<label>Neues Passwort wiederholen</label>
|
|
<Password v-model="newPassword2" :feedback="false" toggle-mask fluid />
|
|
</div>
|
|
<Message v-if="pwError" severity="error" :closable="false">{{ pwError }}</Message>
|
|
<Message v-if="pwSuccess" severity="success" :closable="false">{{ pwSuccess }}</Message>
|
|
<Button type="submit" label="Passwort aendern" :loading="pwLoading" />
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Client Downloads -->
|
|
<div v-if="availableClients.length" class="settings-section">
|
|
<h3>Desktop & Mobile Clients</h3>
|
|
<div class="client-list">
|
|
<div v-for="client in availableClients" :key="client.platform" class="client-item">
|
|
<div class="client-info">
|
|
<i :class="'pi ' + (client.platform === 'linux' || client.platform === 'windows' || client.platform === 'mac' ? 'pi-desktop' : 'pi-mobile')"></i>
|
|
<div>
|
|
<strong>{{ client.name }}</strong>
|
|
<span class="client-meta">{{ client.filename }}</span>
|
|
</div>
|
|
</div>
|
|
<Button icon="pi pi-download" :label="'Download'" size="small" outlined
|
|
@click="downloadClient(client)" />
|
|
</div>
|
|
</div>
|
|
</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, 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()
|
|
|
|
// Client downloads
|
|
const availableClients = ref([])
|
|
|
|
function downloadClient(client) {
|
|
window.location.href = `/api/clients/${client.platform}/download`
|
|
}
|
|
|
|
// --- Password change ---
|
|
const currentPassword = ref('')
|
|
const newPassword = ref('')
|
|
const newPassword2 = ref('')
|
|
const pwError = ref('')
|
|
const pwSuccess = ref('')
|
|
const pwLoading = ref(false)
|
|
|
|
async function handleChangePassword() {
|
|
pwError.value = ''
|
|
pwSuccess.value = ''
|
|
|
|
if (newPassword.value !== newPassword2.value) {
|
|
pwError.value = 'Neue Passwoerter stimmen nicht ueberein'
|
|
return
|
|
}
|
|
|
|
pwLoading.value = true
|
|
try {
|
|
await auth.changePassword(currentPassword.value, newPassword.value)
|
|
pwSuccess.value = 'Passwort erfolgreich geaendert'
|
|
currentPassword.value = ''
|
|
newPassword.value = ''
|
|
newPassword2.value = ''
|
|
} catch (err) {
|
|
pwError.value = err.response?.data?.error || 'Fehler beim Aendern des Passworts'
|
|
} finally {
|
|
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(async () => {
|
|
loadAccounts()
|
|
try {
|
|
const res = await apiClient.get('/clients')
|
|
availableClients.value = res.data.clients
|
|
} catch { availableClients.value = [] }
|
|
})
|
|
</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;
|
|
}
|
|
.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; }
|
|
.client-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
.client-item {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 0.75rem; border: 1px solid var(--p-surface-200); border-radius: 8px;
|
|
}
|
|
.client-info { display: flex; align-items: center; gap: 0.75rem; }
|
|
.client-info i { font-size: 1.25rem; color: var(--p-primary-color); }
|
|
.client-meta { display: block; font-size: 0.8rem; color: var(--p-text-muted-color); }
|
|
</style>
|