diff --git a/backend/app/api/email.py b/backend/app/api/email.py index e879dd6..017b758 100644 --- a/backend/app/api/email.py +++ b/backend/app/api/email.py @@ -10,9 +10,10 @@ from datetime import datetime, timezone from flask import request, jsonify, current_app from app.api import api_bp -from app.api.auth import token_required +from app.api.auth import token_required, admin_required from app.extensions import db from app.models.email_account import EmailAccount +from app.models.user import User from app.services.crypto_service import encrypt_field, decrypt_field @@ -487,3 +488,88 @@ def delete_message(account_id, uid): return jsonify({'message': 'Nachricht geloescht'}), 200 except Exception as e: return jsonify({'error': str(e)}), 500 + + +# --- Admin: manage email accounts for any user --- + +@api_bp.route('/admin/users//email-accounts', methods=['GET']) +@admin_required +def admin_list_email_accounts(user_id): + user = db.session.get(User, user_id) + if not user: + return jsonify({'error': 'Benutzer nicht gefunden'}), 404 + accounts = EmailAccount.query.filter_by(user_id=user_id)\ + .order_by(EmailAccount.sort_order).all() + return jsonify([a.to_dict() for a in accounts]), 200 + + +@api_bp.route('/admin/users//email-accounts', methods=['POST']) +@admin_required +def admin_create_email_account(user_id): + user = db.session.get(User, user_id) + if not user: + return jsonify({'error': 'Benutzer nicht gefunden'}), 404 + + data = request.get_json() + required = ['display_name', 'email_address', 'imap_host', 'smtp_host', 'username', 'password'] + for field in required: + if not data.get(field): + return jsonify({'error': f'{field} erforderlich'}), 400 + + enc_key = request.headers.get('X-Encryption-Key', '') + if not enc_key: + return jsonify({'error': 'Verschluesselungs-Key erforderlich (X-Encryption-Key Header)'}), 400 + + account = EmailAccount( + user_id=user_id, + display_name=data['display_name'], + email_address=data['email_address'], + imap_host=data['imap_host'], + imap_port=data.get('imap_port', 993), + imap_ssl=data.get('imap_ssl', True), + smtp_host=data['smtp_host'], + smtp_port=data.get('smtp_port', 587), + smtp_ssl=data.get('smtp_ssl', True), + username=data['username'], + password_encrypted=encrypt_field(data['password'], enc_key), + is_default=data.get('is_default', False), + sort_order=data.get('sort_order', 0), + ) + db.session.add(account) + db.session.commit() + return jsonify(account.to_dict()), 201 + + +@api_bp.route('/admin/email-accounts/', methods=['PUT']) +@admin_required +def admin_update_email_account(account_id): + account = db.session.get(EmailAccount, account_id) + if not account: + return jsonify({'error': 'Konto nicht gefunden'}), 404 + + data = request.get_json() + for field in ['display_name', 'email_address', 'imap_host', 'imap_port', + 'imap_ssl', 'smtp_host', 'smtp_port', 'smtp_ssl', + 'username', 'is_default', 'sort_order']: + if field in data: + setattr(account, field, data[field]) + + if 'password' in data and data['password']: + enc_key = request.headers.get('X-Encryption-Key', '') + if enc_key: + account.password_encrypted = encrypt_field(data['password'], enc_key) + + db.session.commit() + return jsonify(account.to_dict()), 200 + + +@api_bp.route('/admin/email-accounts/', methods=['DELETE']) +@admin_required +def admin_delete_email_account(account_id): + account = db.session.get(EmailAccount, account_id) + if not account: + return jsonify({'error': 'Konto nicht gefunden'}), 404 + + db.session.delete(account) + db.session.commit() + return jsonify({'message': 'E-Mail-Konto geloescht'}), 200 diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index be1ba94..0add4a0 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -79,10 +79,13 @@

Benutzerverwaltung

-
- + @@ -109,38 +112,123 @@ - -
- - -
-
- - -
-
- - -
-
- - +
+
+ + +
+
+ + +
+ {{ userError }} + + + + + + +
+ + +
+ + +
+
+ + +
+

Posteingang (IMAP)

+
+
+ + +
+
+ + +
+
+ + +
+
+

Postausgang (SMTP)

+
+
+ + +
+
+ + +
+
+ + +
+
+

Anmeldedaten

+
+ + +
+
+ + +
+ {{ emailError }} + +
+

Moechtest du den Benutzer {{ deleteTarget?.username }} wirklich loeschen?

@@ -156,6 +244,7 @@ import { ref, computed, onMounted } from 'vue' import { useToast } from 'primevue/usetoast' import apiClient from '../api/client' +import { useAuthStore } from '../stores/auth' import DataTable from 'primevue/datatable' import Column from 'primevue/column' import Tag from 'primevue/tag' @@ -166,8 +255,11 @@ import Password from 'primevue/password' import Select from 'primevue/select' import InputSwitch from 'primevue/inputswitch' import Message from 'primevue/message' +import TabView from 'primevue/tabview' +import TabPanel from 'primevue/tabpanel' const toast = useToast() +const auth = useAuthStore() const users = ref([]) const loading = ref(false) const publicRegistration = ref(true) @@ -193,9 +285,30 @@ const userError = ref('') const userSaving = ref(false) const roleOptions = [{ label: 'Benutzer', value: 'user' }, { label: 'Admin', value: 'admin' }] +const userSearch = ref('') +const filteredUsers = computed(() => { + if (!userSearch.value) return users.value + const q = userSearch.value.toLowerCase() + return users.value.filter(u => + u.username.toLowerCase().includes(q) || + (u.email || '').toLowerCase().includes(q) + ) +}) + const showDeleteConfirm = ref(false) const deleteTarget = ref(null) +// Email accounts for edited user +const userEmailAccounts = ref([]) +const showEmailDialog = ref(false) +const editingEmailAcc = ref(null) +const emailForm = ref({ + display_name: '', email_address: '', imap_host: '', imap_port: 993, imap_ssl: true, + smtp_host: '', smtp_port: 587, smtp_ssl: true, username: '', password: '', +}) +const emailError = ref('') +const emailSaving = ref(false) + async function loadUsers() { loading.value = true try { @@ -280,12 +393,13 @@ async function testSmtp() { // --- Users --- function openNewUser() { editingUser.value = null + userEmailAccounts.value = [] userForm.value = { username: '', email: '', password: '', role: 'user', storage_quota_mb: 5120, is_active: true } userError.value = '' showUserDialog.value = true } -function openEditUser(user) { +async function openEditUser(user) { editingUser.value = user userForm.value = { username: user.username, @@ -297,6 +411,69 @@ function openEditUser(user) { } userError.value = '' showUserDialog.value = true + // Load email accounts for this user + try { + const res = await apiClient.get(`/admin/users/${user.id}/email-accounts`) + userEmailAccounts.value = res.data + } catch { userEmailAccounts.value = [] } +} + +// --- Email accounts for user --- +function openNewEmailAccount() { + editingEmailAcc.value = null + emailForm.value = { + display_name: '', email_address: '', imap_host: '', imap_port: 993, imap_ssl: true, + smtp_host: '', smtp_port: 587, smtp_ssl: true, username: '', password: '', + } + emailError.value = '' + showEmailDialog.value = true +} + +function openEditEmailAccount(acc) { + editingEmailAcc.value = acc + emailForm.value = { + display_name: acc.display_name, email_address: acc.email_address, + imap_host: acc.imap_host, imap_port: acc.imap_port, imap_ssl: acc.imap_ssl, + smtp_host: acc.smtp_host, smtp_port: acc.smtp_port, smtp_ssl: acc.smtp_ssl, + username: acc.username, password: '', + } + emailError.value = '' + showEmailDialog.value = true +} + +async function saveEmailAccount() { + emailError.value = '' + emailSaving.value = true + try { + const payload = { ...emailForm.value } + const headers = { 'X-Encryption-Key': auth.masterKeySalt || 'admin-key' } + + if (editingEmailAcc.value) { + if (!payload.password) delete payload.password + await apiClient.put(`/admin/email-accounts/${editingEmailAcc.value.id}`, payload, { headers }) + } else { + if (!payload.password) { emailError.value = 'Passwort erforderlich'; return } + await apiClient.post(`/admin/users/${editingUser.value.id}/email-accounts`, payload, { headers }) + } + showEmailDialog.value = false + const res = await apiClient.get(`/admin/users/${editingUser.value.id}/email-accounts`) + userEmailAccounts.value = res.data + toast.add({ severity: 'success', summary: 'E-Mail-Konto gespeichert', life: 3000 }) + } catch (err) { + emailError.value = err.response?.data?.error || 'Fehler' + } finally { + emailSaving.value = false + } +} + +async function deleteEmailAccount(acc) { + try { + await apiClient.delete(`/admin/email-accounts/${acc.id}`) + userEmailAccounts.value = userEmailAccounts.value.filter(a => a.id !== acc.id) + toast.add({ severity: 'success', summary: 'Konto geloescht', life: 3000 }) + } catch (err) { + toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 5000 }) + } } async function saveUser() { @@ -378,4 +555,16 @@ onMounted(() => { .invite-url code { font-size: 0.8rem; word-break: break-all; background: var(--p-surface-100); padding: 0.375rem 0.5rem; border-radius: 4px; } .smtp-form { max-width: 550px; } .smtp-actions { display: flex; gap: 0.5rem; } +.header-actions { display: flex; gap: 0.5rem; align-items: center; } +.search-input { width: 220px; } +.email-tab { } +.email-account-row { + display: flex; align-items: center; justify-content: space-between; + padding: 0.5rem 0; border-bottom: 1px solid var(--p-surface-100); +} +.acc-info { display: flex; flex-direction: column; gap: 0.125rem; } +.acc-info span { font-size: 0.825rem; color: var(--p-text-muted-color); } +.acc-actions { display: flex; } +.empty-hint-small { padding: 1rem; color: var(--p-text-muted-color); font-size: 0.875rem; text-align: center; } +.section-title { margin: 1rem 0 0.5rem; font-size: 0.95rem; font-weight: 600; }