up
This commit is contained in:
357
public/assets/js/ui-user.js
Normal file
357
public/assets/js/ui-user.js
Normal file
@@ -0,0 +1,357 @@
|
||||
import { apiAction, toast } from './api.js';
|
||||
|
||||
const state = {
|
||||
settings: {},
|
||||
rotate: { bridge: false, sender: false, external: false },
|
||||
users: [],
|
||||
userMap: new Map(),
|
||||
currentTab: 'profile',
|
||||
loading: false,
|
||||
};
|
||||
|
||||
let dialog;
|
||||
let avatarBtn;
|
||||
let profileForm;
|
||||
let passwordForm;
|
||||
let settingsForm;
|
||||
let teamTable;
|
||||
let userForm;
|
||||
|
||||
export function initUserPanel() {
|
||||
dialog = document.getElementById('userDialog');
|
||||
avatarBtn = document.getElementById('btn-user');
|
||||
if (!dialog || !avatarBtn) return;
|
||||
|
||||
profileForm = document.getElementById('profileForm');
|
||||
passwordForm = document.getElementById('passwordForm');
|
||||
settingsForm = document.getElementById('settingsForm');
|
||||
teamTable = document.getElementById('teamTable');
|
||||
userForm = document.getElementById('userForm');
|
||||
|
||||
avatarBtn.addEventListener('click', () => openUserDialog());
|
||||
document.getElementById('userClose')?.addEventListener('click', () => dialog.close());
|
||||
|
||||
profileForm?.addEventListener('submit', submitProfileForm);
|
||||
passwordForm?.addEventListener('submit', submitPasswordForm);
|
||||
settingsForm?.addEventListener('submit', submitSettingsForm);
|
||||
|
||||
document.getElementById('btn-user-add')?.addEventListener('click', () => openUserForm());
|
||||
document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm());
|
||||
userForm?.addEventListener('submit', submitUserForm);
|
||||
|
||||
teamTable?.addEventListener('click', handleTeamTableClick);
|
||||
|
||||
dialog.addEventListener('close', () => closeUserForm());
|
||||
|
||||
document.querySelectorAll('[data-user-tab]').forEach(btn => {
|
||||
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
|
||||
});
|
||||
|
||||
settingsForm?.querySelectorAll('button[data-rotate]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const type = btn.getAttribute('data-rotate');
|
||||
if (type && state.rotate[type] !== undefined) {
|
||||
state.rotate[type] = true;
|
||||
toast('Token wird nach dem Speichern erneuert.', true, { duration: 2000 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
settingsForm?.querySelectorAll('button[data-download]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const type = btn.getAttribute('data-download');
|
||||
if (type) downloadFile(type);
|
||||
});
|
||||
});
|
||||
|
||||
updateAvatar();
|
||||
updateOwnerVisibility();
|
||||
}
|
||||
|
||||
function isOwner() {
|
||||
return (window.__currentUser?.role || '').toLowerCase() === 'owner';
|
||||
}
|
||||
|
||||
function updateAvatar() {
|
||||
const target = document.getElementById('userAvatar');
|
||||
if (!target) return;
|
||||
const name = window.__currentUser?.name || window.__currentUser?.email || '';
|
||||
target.textContent = name ? name.trim().charAt(0).toUpperCase() : 'U';
|
||||
}
|
||||
|
||||
function updateOwnerVisibility() {
|
||||
document.querySelectorAll('.owner-only').forEach(el => {
|
||||
el.classList.toggle('hidden', !isOwner());
|
||||
});
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
if (!tab) return;
|
||||
state.currentTab = tab;
|
||||
document.querySelectorAll('[data-user-panel]').forEach(panel => {
|
||||
panel.classList.toggle('hidden', panel.getAttribute('data-user-panel') !== tab);
|
||||
});
|
||||
document.querySelectorAll('[data-user-tab]').forEach(btn => {
|
||||
const isActive = btn.getAttribute('data-user-tab') === tab;
|
||||
btn.classList.toggle('bg-sky-50', isActive);
|
||||
btn.classList.toggle('text-sky-700', isActive);
|
||||
});
|
||||
}
|
||||
|
||||
async function openUserDialog() {
|
||||
if (dialog.open || state.loading) return;
|
||||
dialog.showModal();
|
||||
switchTab(state.currentTab);
|
||||
await loadAccountData();
|
||||
}
|
||||
|
||||
async function loadAccountData() {
|
||||
try {
|
||||
state.loading = true;
|
||||
const res = await apiAction('account.profile.get', { method: 'GET' });
|
||||
if (!res?.ok) throw new Error(res?.error || 'Profil konnte nicht geladen werden');
|
||||
if (res.user) {
|
||||
window.__currentUser = res.user;
|
||||
updateAvatar();
|
||||
updateOwnerVisibility();
|
||||
}
|
||||
fillProfileForm(res.user);
|
||||
fillSettingsForm(res.settings || {});
|
||||
if (isOwner()) {
|
||||
await loadUsers();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast(err.message || 'Fehler beim Laden', false);
|
||||
dialog.close();
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fillProfileForm(user) {
|
||||
if (!profileForm) return;
|
||||
profileForm.name.value = user?.name || '';
|
||||
profileForm.email.value = user?.email || '';
|
||||
}
|
||||
|
||||
function fillSettingsForm(settings) {
|
||||
state.settings = settings;
|
||||
if (!settingsForm) return;
|
||||
settingsForm.bridge_url.value = settings.bridge_url || '';
|
||||
settingsForm.bridge_token.value = settings.bridge_token || '';
|
||||
settingsForm.sender_token.value = settings.sender_token || '';
|
||||
settingsForm.external_api_token.value = settings.external_api_token || '';
|
||||
state.rotate = { bridge: false, sender: false, external: false };
|
||||
}
|
||||
|
||||
async function submitProfileForm(ev) {
|
||||
ev.preventDefault();
|
||||
const data = {
|
||||
name: profileForm.name.value.trim(),
|
||||
email: profileForm.email.value.trim(),
|
||||
};
|
||||
try {
|
||||
const res = await apiAction('account.profile.update', { method: 'POST', data });
|
||||
if (!res?.ok) throw new Error(res?.error || 'Profil konnte nicht gespeichert werden');
|
||||
window.__currentUser = res.user;
|
||||
updateAvatar();
|
||||
toast('Profil aktualisiert', true);
|
||||
} catch (err) {
|
||||
toast(err.message || 'Fehler beim Speichern', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPasswordForm(ev) {
|
||||
ev.preventDefault();
|
||||
const data = {
|
||||
current_password: passwordForm.current_password.value,
|
||||
new_password: passwordForm.new_password.value,
|
||||
};
|
||||
try {
|
||||
const res = await apiAction('account.password.update', { method: 'POST', data });
|
||||
if (!res?.ok) throw new Error(res?.error || 'Passwort konnte nicht geändert werden');
|
||||
passwordForm.reset();
|
||||
toast('Passwort aktualisiert', true);
|
||||
} catch (err) {
|
||||
toast(err.message || 'Fehler beim Speichern', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitSettingsForm(ev) {
|
||||
ev.preventDefault();
|
||||
const data = {
|
||||
bridge_url: settingsForm.bridge_url.value.trim(),
|
||||
bridge_token: settingsForm.bridge_token.value.trim(),
|
||||
sender_token: settingsForm.sender_token.value.trim(),
|
||||
external_api_token: settingsForm.external_api_token.value.trim(),
|
||||
rotate_bridge_token: state.rotate.bridge ? 1 : 0,
|
||||
rotate_sender_token: state.rotate.sender ? 1 : 0,
|
||||
rotate_external_token: state.rotate.external ? 1 : 0,
|
||||
};
|
||||
try {
|
||||
const res = await apiAction('account.settings.update', { method: 'POST', data });
|
||||
if (!res?.ok) throw new Error(res?.error || 'Einstellungen konnten nicht gespeichert werden');
|
||||
fillSettingsForm(res.settings || {});
|
||||
toast('Integrationen gespeichert', true);
|
||||
} catch (err) {
|
||||
toast(err.message || 'Fehler beim Speichern', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(type) {
|
||||
try {
|
||||
const action = type === 'sender' ? 'downloads.sender' : 'downloads.bridge';
|
||||
const res = await apiAction(action, { method: 'POST', data: {} });
|
||||
if (!res?.ok || !res.content) throw new Error(res?.error || 'Download fehlgeschlagen');
|
||||
const bytes = atob(res.content);
|
||||
const buffer = new Uint8Array(bytes.length);
|
||||
for (let i = 0; i < bytes.length; i++) buffer[i] = bytes.charCodeAt(i);
|
||||
const blob = new Blob([buffer], { type: 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = res.file_name || `${type}.php`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
} catch (err) {
|
||||
toast(err.message || 'Download fehlgeschlagen', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const res = await apiAction('account.users.list', { method: 'GET' });
|
||||
if (!res?.ok) throw new Error(res?.error || 'Team konnte nicht geladen werden');
|
||||
state.users = res.items || [];
|
||||
state.userMap = new Map(state.users.map(u => [u.id, u]));
|
||||
renderUserList();
|
||||
} catch (err) {
|
||||
toast(err.message || 'Fehler beim Laden der Nutzer', false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderUserList() {
|
||||
if (!teamTable) return;
|
||||
const tbody = teamTable.querySelector('tbody');
|
||||
if (!tbody) return;
|
||||
if (!state.users.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="text-sm text-slate-500">Keine Nutzer vorhanden.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = state.users.map(user => {
|
||||
const badge = user.is_active ? '<span class="badge bg-green-100 text-green-800">Aktiv</span>' : '<span class="badge bg-slate-200 text-slate-700">Inaktiv</span>';
|
||||
return `<tr>
|
||||
<td>${escapeHtml(user.name)}</td>
|
||||
<td>${escapeHtml(user.email)}</td>
|
||||
<td>${escapeHtml(user.role)}</td>
|
||||
<td>${badge}</td>
|
||||
<td class="text-right flex gap-2 justify-end">
|
||||
<button class="btn" data-user-action="edit" data-user-id="${user.id}">Bearbeiten</button>
|
||||
<button class="btn" data-user-action="reset" data-user-id="${user.id}">Passwort</button>
|
||||
<button class="btn btn-danger" data-user-action="delete" data-user-id="${user.id}">Löschen</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function handleTeamTableClick(ev) {
|
||||
const btn = ev.target.closest('button[data-user-action]');
|
||||
if (!btn) return;
|
||||
const id = Number(btn.getAttribute('data-user-id'));
|
||||
const action = btn.getAttribute('data-user-action');
|
||||
const user = state.userMap.get(id);
|
||||
if (!user) return;
|
||||
if (action === 'edit') {
|
||||
openUserForm(user);
|
||||
} else if (action === 'delete') {
|
||||
if (confirm(`Soll ${user.name} wirklich entfernt werden?`)) deleteUser(id);
|
||||
} else if (action === 'reset') {
|
||||
openUserForm(user, true);
|
||||
}
|
||||
}
|
||||
|
||||
function openUserForm(user = null, resetOnly = false) {
|
||||
if (!userForm) return;
|
||||
userForm.classList.remove('hidden');
|
||||
userForm.user_id.value = user?.id || '';
|
||||
userForm.name.value = user?.name || '';
|
||||
userForm.email.value = user?.email || '';
|
||||
userForm.role.value = user?.role || 'user';
|
||||
userForm.is_active.checked = user ? !!user.is_active : true;
|
||||
const resetRow = userForm.querySelector('.reset-only');
|
||||
if (resetRow) resetRow.classList.toggle('hidden', !user);
|
||||
if (resetRow) resetRow.querySelector('input').checked = resetOnly;
|
||||
}
|
||||
|
||||
function closeUserForm() {
|
||||
if (!userForm) return;
|
||||
userForm.classList.add('hidden');
|
||||
userForm.reset();
|
||||
userForm.user_id.value = '';
|
||||
const resetInput = userForm.querySelector('.reset-only input');
|
||||
if (resetInput) resetInput.checked = false;
|
||||
}
|
||||
|
||||
async function submitUserForm(ev) {
|
||||
ev.preventDefault();
|
||||
const formData = new FormData(userForm);
|
||||
const payload = {
|
||||
name: formData.get('name')?.toString().trim() || '',
|
||||
email: formData.get('email')?.toString().trim() || '',
|
||||
role: formData.get('role')?.toString() || 'user',
|
||||
is_active: userForm.is_active.checked ? 1 : 0,
|
||||
};
|
||||
const userId = formData.get('user_id')?.toString();
|
||||
let action = 'account.users.create';
|
||||
if (userId) {
|
||||
action = 'account.users.update';
|
||||
payload.user_id = Number(userId);
|
||||
payload.reset_password = userForm.querySelector('input[name="reset_password"]').checked ? 1 : 0;
|
||||
}
|
||||
try {
|
||||
const res = await apiAction(action, { method: 'POST', data: payload });
|
||||
if (!res?.ok) throw new Error(res?.error || 'Speichern fehlgeschlagen');
|
||||
closeUserForm();
|
||||
await loadUsers();
|
||||
toast('Nutzer gespeichert', true);
|
||||
if (res.temp_password) {
|
||||
copyToClipboard(res.temp_password);
|
||||
alert(`Neues Passwort: ${res.temp_password}`);
|
||||
}
|
||||
} catch (err) {
|
||||
toast(err.message || 'Fehler beim Speichern', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(userId) {
|
||||
try {
|
||||
const res = await apiAction('account.users.delete', { method: 'POST', data: { user_id: userId } });
|
||||
if (!res?.ok) throw new Error(res?.error || 'Löschen fehlgeschlagen');
|
||||
await loadUsers();
|
||||
toast('Nutzer gelöscht', true);
|
||||
} catch (err) {
|
||||
toast(err.message || 'Fehler beim Löschen', false);
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(value) {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
navigator.clipboard.writeText(value).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function closeUserDialog() {
|
||||
dialog?.close();
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
Reference in New Issue
Block a user