diff --git a/config/emailtemplate.conf.php b/config/emailtemplate.conf.php index 4655572..6b8edb4 100644 --- a/config/emailtemplate.conf.php +++ b/config/emailtemplate.conf.php @@ -68,6 +68,15 @@ $authDefaults = [ 'col_name' => 'name', 'col_id' => 'id', 'col_status' => 'is_active', + 'col_role' => 'role', + 'customer_fk' => 'customer_id', + 'customer_table'=> 'customers', + 'customer_cols' => [ + 'name' => 'name', + 'slug' => 'slug', + 'status' => 'status', + 'plan' => 'plan', + ], 'active_values' => ['active','1',1], 'legacy' => 'md5', ], diff --git a/public/assets/js/app.js b/public/assets/js/app.js index f6f2dc5..52c76cb 100644 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -3,6 +3,7 @@ import { initTabs } from './ui-tabs.js'; import { initLists } from './ui-list.js'; import { initCreate } from './ui-create.js'; import { initEditor } from './ui-editor.js'; +import { initUserPanel } from './ui-user.js'; import { mountLogoutButton, ensureFloatingLogout } from './ui-auth.js'; import { apiAction } from './api.js'; @@ -18,6 +19,7 @@ async function ensureAuthenticated() { window.location.href = '/login.php'; return false; } + window.__currentUser = me.user; // ✅ nur für eingeloggte Nutzer: UI freigebensss document.documentElement.classList.remove('auth-pending'); const appRoot = document.getElementById('app'); @@ -34,6 +36,7 @@ function initAppFeatures() { initLists(); initCreate(); initEditor(); + initUserPanel(); // Logout-Buttons mountLogoutButton('#btn-logout', { redirect: '/login.php' }); @@ -81,4 +84,3 @@ document.addEventListener('DOMContentLoaded', async () => { }); window.addEventListener('message', handleEditorMessages); - diff --git a/public/assets/js/ui-user.js b/public/assets/js/ui-user.js new file mode 100644 index 0000000..0be17fc --- /dev/null +++ b/public/assets/js/ui-user.js @@ -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 = '