feat: Admin kann Email-Konten pro Benutzer verwalten + Benutzersuche
- Admin-Benutzer-Dialog hat jetzt zwei Tabs: Allgemein + E-Mail-Konten - Im E-Mail-Konten-Tab: Konten fuer jeden Benutzer hinzufuegen, bearbeiten und loeschen (ohne sich als Benutzer einloggen zu muessen) - Benutzersuche in der Benutzerverwaltung (filtert nach Name und Email) - Backend: /admin/users/<id>/email-accounts GET/POST und /admin/email-accounts/<id> PUT/DELETE Endpunkte Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,9 +10,10 @@ from datetime import datetime, timezone
|
|||||||
from flask import request, jsonify, current_app
|
from flask import request, jsonify, current_app
|
||||||
|
|
||||||
from app.api import api_bp
|
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.extensions import db
|
||||||
from app.models.email_account import EmailAccount
|
from app.models.email_account import EmailAccount
|
||||||
|
from app.models.user import User
|
||||||
from app.services.crypto_service import encrypt_field, decrypt_field
|
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
|
return jsonify({'message': 'Nachricht geloescht'}), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# --- Admin: manage email accounts for any user ---
|
||||||
|
|
||||||
|
@api_bp.route('/admin/users/<int:user_id>/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/<int:user_id>/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/<int:account_id>', 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/<int:account_id>', 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
|
||||||
|
|||||||
@@ -79,10 +79,13 @@
|
|||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>Benutzerverwaltung</h3>
|
<h3>Benutzerverwaltung</h3>
|
||||||
<Button icon="pi pi-user-plus" label="Benutzer hinzufuegen" size="small" @click="openNewUser" />
|
<div class="header-actions">
|
||||||
|
<InputText v-model="userSearch" placeholder="Benutzer suchen..." class="search-input" />
|
||||||
|
<Button icon="pi pi-user-plus" label="Benutzer hinzufuegen" size="small" @click="openNewUser" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable :value="users" :loading="loading" striped-rows>
|
<DataTable :value="filteredUsers" :loading="loading" striped-rows>
|
||||||
<Column field="id" header="ID" style="width: 60px" />
|
<Column field="id" header="ID" style="width: 60px" />
|
||||||
<Column field="username" header="Benutzername" />
|
<Column field="username" header="Benutzername" />
|
||||||
<Column field="email" header="E-Mail">
|
<Column field="email" header="E-Mail">
|
||||||
@@ -109,38 +112,123 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New/Edit User Dialog -->
|
<!-- New/Edit User Dialog -->
|
||||||
<Dialog v-model:visible="showUserDialog" :header="editingUser ? 'Benutzer bearbeiten' : 'Neuer Benutzer'" modal :style="{ width: '450px' }">
|
<Dialog v-model:visible="showUserDialog" :header="editingUser ? 'Benutzer bearbeiten' : 'Neuer Benutzer'" modal :style="{ width: '600px' }">
|
||||||
<div class="field">
|
<TabView>
|
||||||
<label>Benutzername</label>
|
<TabPanel header="Allgemein">
|
||||||
<InputText v-model="userForm.username" fluid :disabled="!!editingUser" />
|
<div class="field">
|
||||||
</div>
|
<label>Benutzername</label>
|
||||||
<div class="field">
|
<InputText v-model="userForm.username" fluid :disabled="!!editingUser" />
|
||||||
<label>E-Mail (optional)</label>
|
</div>
|
||||||
<InputText v-model="userForm.email" type="email" fluid />
|
<div class="field">
|
||||||
</div>
|
<label>E-Mail (optional)</label>
|
||||||
<div class="field">
|
<InputText v-model="userForm.email" type="email" fluid />
|
||||||
<label>{{ editingUser ? 'Neues Passwort (leer lassen = nicht aendern)' : 'Passwort' }}</label>
|
</div>
|
||||||
<Password v-model="userForm.password" :feedback="false" toggle-mask fluid />
|
<div class="field">
|
||||||
</div>
|
<label>{{ editingUser ? 'Neues Passwort (leer lassen = nicht aendern)' : 'Passwort' }}</label>
|
||||||
<div class="field">
|
<Password v-model="userForm.password" :feedback="false" toggle-mask fluid />
|
||||||
<label>Rolle</label>
|
</div>
|
||||||
<Select v-model="userForm.role" :options="roleOptions" optionLabel="label" optionValue="value" fluid />
|
<div class="field">
|
||||||
</div>
|
<label>Rolle</label>
|
||||||
<div class="field">
|
<Select v-model="userForm.role" :options="roleOptions" optionLabel="label" optionValue="value" fluid />
|
||||||
<label>Speicher-Quota (MB)</label>
|
</div>
|
||||||
<InputText v-model.number="userForm.storage_quota_mb" type="number" fluid />
|
<div class="field">
|
||||||
</div>
|
<label>Speicher-Quota (MB)</label>
|
||||||
<div v-if="editingUser" class="field">
|
<InputText v-model.number="userForm.storage_quota_mb" type="number" fluid />
|
||||||
<label>Aktiv</label>
|
</div>
|
||||||
<InputSwitch v-model="userForm.is_active" />
|
<div v-if="editingUser" class="field">
|
||||||
</div>
|
<label>Aktiv</label>
|
||||||
<Message v-if="userError" severity="error" :closable="false">{{ userError }}</Message>
|
<InputSwitch v-model="userForm.is_active" />
|
||||||
|
</div>
|
||||||
|
<Message v-if="userError" severity="error" :closable="false">{{ userError }}</Message>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel v-if="editingUser" header="E-Mail-Konten">
|
||||||
|
<div class="email-tab">
|
||||||
|
<div class="section-header">
|
||||||
|
<span>Konten fuer {{ editingUser.username }}</span>
|
||||||
|
<Button icon="pi pi-plus" label="Konto hinzufuegen" size="small" @click="openNewEmailAccount" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!userEmailAccounts.length" class="empty-hint-small">
|
||||||
|
Keine E-Mail-Konten konfiguriert.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="acc in userEmailAccounts" :key="acc.id" class="email-account-row">
|
||||||
|
<div class="acc-info">
|
||||||
|
<strong>{{ acc.display_name }}</strong>
|
||||||
|
<span>{{ acc.email_address }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="acc-actions">
|
||||||
|
<Button icon="pi pi-pencil" text size="small" @click="openEditEmailAccount(acc)" />
|
||||||
|
<Button icon="pi pi-trash" text size="small" severity="danger" @click="deleteEmailAccount(acc)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
</TabView>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button label="Abbrechen" text @click="showUserDialog = false" />
|
<Button label="Abbrechen" text @click="showUserDialog = false" />
|
||||||
<Button :label="editingUser ? 'Speichern' : 'Erstellen'" @click="saveUser" :loading="userSaving" />
|
<Button :label="editingUser ? 'Speichern' : 'Erstellen'" @click="saveUser" :loading="userSaving" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Email Account Sub-Dialog -->
|
||||||
|
<Dialog v-model:visible="showEmailDialog" :header="editingEmailAcc ? 'E-Mail-Konto bearbeiten' : 'E-Mail-Konto hinzufuegen'" modal :style="{ width: '550px' }">
|
||||||
|
<div class="field">
|
||||||
|
<label>Anzeigename</label>
|
||||||
|
<InputText v-model="emailForm.display_name" placeholder="z.B. Arbeit, Privat" fluid autofocus />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>E-Mail-Adresse</label>
|
||||||
|
<InputText v-model="emailForm.email_address" type="email" fluid />
|
||||||
|
</div>
|
||||||
|
<h4 class="section-title">Posteingang (IMAP)</h4>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field flex-grow">
|
||||||
|
<label>Server</label>
|
||||||
|
<InputText v-model="emailForm.imap_host" fluid />
|
||||||
|
</div>
|
||||||
|
<div class="field" style="width: 90px">
|
||||||
|
<label>Port</label>
|
||||||
|
<InputText v-model.number="emailForm.imap_port" type="number" fluid />
|
||||||
|
</div>
|
||||||
|
<div class="field" style="width: 70px">
|
||||||
|
<label>SSL</label>
|
||||||
|
<InputSwitch v-model="emailForm.imap_ssl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4 class="section-title">Postausgang (SMTP)</h4>
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field flex-grow">
|
||||||
|
<label>Server</label>
|
||||||
|
<InputText v-model="emailForm.smtp_host" fluid />
|
||||||
|
</div>
|
||||||
|
<div class="field" style="width: 90px">
|
||||||
|
<label>Port</label>
|
||||||
|
<InputText v-model.number="emailForm.smtp_port" type="number" fluid />
|
||||||
|
</div>
|
||||||
|
<div class="field" style="width: 70px">
|
||||||
|
<label>SSL</label>
|
||||||
|
<InputSwitch v-model="emailForm.smtp_ssl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4 class="section-title">Anmeldedaten</h4>
|
||||||
|
<div class="field">
|
||||||
|
<label>Benutzername</label>
|
||||||
|
<InputText v-model="emailForm.username" fluid />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ editingEmailAcc ? 'Passwort (leer = nicht aendern)' : 'Passwort' }}</label>
|
||||||
|
<Password v-model="emailForm.password" :feedback="false" toggle-mask fluid />
|
||||||
|
</div>
|
||||||
|
<Message v-if="emailError" severity="error" :closable="false">{{ emailError }}</Message>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="Abbrechen" text @click="showEmailDialog = false" />
|
||||||
|
<Button :label="editingEmailAcc ? 'Speichern' : 'Hinzufuegen'" @click="saveEmailAccount" :loading="emailSaving" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<!-- Delete Confirm -->
|
<!-- Delete Confirm -->
|
||||||
<Dialog v-model:visible="showDeleteConfirm" header="Benutzer loeschen" modal :style="{ width: '400px' }">
|
<Dialog v-model:visible="showDeleteConfirm" header="Benutzer loeschen" modal :style="{ width: '400px' }">
|
||||||
<p>Moechtest du den Benutzer <strong>{{ deleteTarget?.username }}</strong> wirklich loeschen?</p>
|
<p>Moechtest du den Benutzer <strong>{{ deleteTarget?.username }}</strong> wirklich loeschen?</p>
|
||||||
@@ -156,6 +244,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useToast } from 'primevue/usetoast'
|
import { useToast } from 'primevue/usetoast'
|
||||||
import apiClient from '../api/client'
|
import apiClient from '../api/client'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
import DataTable from 'primevue/datatable'
|
import DataTable from 'primevue/datatable'
|
||||||
import Column from 'primevue/column'
|
import Column from 'primevue/column'
|
||||||
import Tag from 'primevue/tag'
|
import Tag from 'primevue/tag'
|
||||||
@@ -166,8 +255,11 @@ import Password from 'primevue/password'
|
|||||||
import Select from 'primevue/select'
|
import Select from 'primevue/select'
|
||||||
import InputSwitch from 'primevue/inputswitch'
|
import InputSwitch from 'primevue/inputswitch'
|
||||||
import Message from 'primevue/message'
|
import Message from 'primevue/message'
|
||||||
|
import TabView from 'primevue/tabview'
|
||||||
|
import TabPanel from 'primevue/tabpanel'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const auth = useAuthStore()
|
||||||
const users = ref([])
|
const users = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const publicRegistration = ref(true)
|
const publicRegistration = ref(true)
|
||||||
@@ -193,9 +285,30 @@ const userError = ref('')
|
|||||||
const userSaving = ref(false)
|
const userSaving = ref(false)
|
||||||
const roleOptions = [{ label: 'Benutzer', value: 'user' }, { label: 'Admin', value: 'admin' }]
|
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 showDeleteConfirm = ref(false)
|
||||||
const deleteTarget = ref(null)
|
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() {
|
async function loadUsers() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -280,12 +393,13 @@ async function testSmtp() {
|
|||||||
// --- Users ---
|
// --- Users ---
|
||||||
function openNewUser() {
|
function openNewUser() {
|
||||||
editingUser.value = null
|
editingUser.value = null
|
||||||
|
userEmailAccounts.value = []
|
||||||
userForm.value = { username: '', email: '', password: '', role: 'user', storage_quota_mb: 5120, is_active: true }
|
userForm.value = { username: '', email: '', password: '', role: 'user', storage_quota_mb: 5120, is_active: true }
|
||||||
userError.value = ''
|
userError.value = ''
|
||||||
showUserDialog.value = true
|
showUserDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditUser(user) {
|
async function openEditUser(user) {
|
||||||
editingUser.value = user
|
editingUser.value = user
|
||||||
userForm.value = {
|
userForm.value = {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
@@ -297,6 +411,69 @@ function openEditUser(user) {
|
|||||||
}
|
}
|
||||||
userError.value = ''
|
userError.value = ''
|
||||||
showUserDialog.value = true
|
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() {
|
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; }
|
.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-form { max-width: 550px; }
|
||||||
.smtp-actions { display: flex; gap: 0.5rem; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user