This commit is contained in:
2025-12-07 03:19:27 +01:00
parent 97e3ea1fc3
commit e341da4176
4 changed files with 204 additions and 161 deletions

167
public/account.php Normal file
View File

@@ -0,0 +1,167 @@
<?php
$base = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/') ?: '';
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Email Template System Konto</title>
<script>document.documentElement.classList.add('auth-pending');</script>
<style>html.auth-pending body{visibility:hidden;}</style>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/admin.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
<link rel="stylesheet" href="assets/css/toast.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
<style>
:root { color-scheme: light; }
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.35rem .7rem;border-radius:.7rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}
.btn:hover{background:#f8fafc}
.btn-avatar{padding:.35rem;border-radius:999px;width:42px;height:42px;justify-content:center;font-weight:600;background:#0ea5e9;color:#fff;border:none}
.btn-avatar[aria-disabled="true"]{pointer-events:none;}
.btn-avatar:hover{background:#0284c7}
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
.section-card h4{margin:0 0 1rem;font-size:1rem;font-weight:600;color:#0f172a}
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
.user-tabs{display:flex;gap:.5rem;margin-bottom:1.25rem}
.team-table{width:100%;border-collapse:collapse;font-size:.9rem}
.team-table th,.team-table td{padding:.35rem .5rem;border-bottom:1px solid #e2e8f0;text-align:left}
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
</style>
</head>
<body class="bg-slate-50 text-slate-800" data-page="account">
<header class="sticky top-0 z-30 bg-white/90 border-b backdrop-blur">
<div class="max-w-5xl mx-auto px-4 py-4 flex items-center gap-3">
<a href="./" class="btn" title="Zurück zur Übersicht">← Übersicht</a>
<h1 class="font-semibold text-lg">Mein Konto</h1>
<div class="ms-auto flex gap-2 items-center">
<a id="btn-user" href="account.php" class="btn-avatar" aria-disabled="true"><span id="userAvatar">U</span></a>
<button id="btn-logout" type="button" class="btn">Logout</button>
</div>
</div>
</header>
<main class="max-w-5xl mx-auto p-4 md:p-6">
<div class="user-tabs">
<button type="button" data-user-tab="profile" class="btn bg-sky-50 text-sky-700 flex-1">Profil</button>
<button type="button" data-user-tab="security" class="btn flex-1">Passwort</button>
<button type="button" data-user-tab="team" class="btn flex-1 owner-only hidden">Team</button>
<button type="button" data-user-tab="integration" class="btn flex-1 owner-only hidden">Integrationen</button>
</div>
<section data-user-panel="profile" class="section-card">
<h4>Profil</h4>
<form id="profileForm" class="space-y-3">
<label class="block text-sm text-slate-600">Name
<input type="text" id="profile_name" name="name" class="input mt-1" required>
</label>
<label class="block text-sm text-slate-600">E-Mail
<input type="email" id="profile_email" name="email" class="input mt-1" required>
</label>
<div class="flex justify-end">
<button type="submit" class="btn">Speichern</button>
</div>
</form>
</section>
<section data-user-panel="security" class="section-card hidden">
<h4>Passwort ändern</h4>
<form id="passwordForm" class="space-y-3">
<label class="block text-sm text-slate-600">Aktuelles Passwort
<input type="password" name="current_password" class="input mt-1" required>
</label>
<label class="block text-sm text-slate-600">Neues Passwort (min. 8 Zeichen)
<input type="password" name="new_password" class="input mt-1" required minlength="8">
</label>
<div class="flex justify-end">
<button type="submit" class="btn">Aktualisieren</button>
</div>
</form>
</section>
<section data-user-panel="team" class="section-card hidden owner-only">
<div class="flex items-center justify-between mb-3">
<h4>Team</h4>
<button type="button" id="btn-user-add" class="btn">+ Nutzer</button>
</div>
<div class="overflow-auto">
<table class="team-table" id="teamTable">
<thead>
<tr><th>Name</th><th>E-Mail</th><th>Rolle</th><th>Status</th><th class="text-right">Aktionen</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
<form id="userForm" class="space-y-3 mt-4 hidden">
<input type="hidden" name="user_id">
<label class="block text-sm text-slate-600">Name
<input type="text" name="name" class="input mt-1" required>
</label>
<label class="block text-sm text-slate-600">E-Mail
<input type="email" name="email" class="input mt-1" required>
</label>
<label class="block text-sm text-slate-600">Rolle
<select name="role" class="input mt-1">
<option value="owner">Owner</option>
<option value="admin">Admin</option>
<option value="editor">Editor</option>
<option value="viewer">Viewer</option>
</select>
</label>
<label class="inline-flex items-center gap-2 text-sm text-slate-600">
<input type="checkbox" name="is_active" checked> Aktiv
</label>
<label class="inline-flex items-center gap-2 text-sm text-slate-600 reset-only hidden">
<input type="checkbox" name="reset_password"> Passwort zurücksetzen
</label>
<div class="flex justify-end gap-2">
<button type="button" id="userFormCancel" class="btn">Abbrechen</button>
<button type="submit" class="btn">Speichern</button>
</div>
</form>
</section>
<section data-user-panel="integration" class="section-card hidden owner-only">
<h4>Integrationen & Tokens</h4>
<form id="settingsForm" class="space-y-3">
<label class="block text-sm text-slate-600">Bridge-URL
<input type="url" name="bridge_url" class="input mt-1" placeholder="https://domain.tld/emailtemplate_bridge.php">
</label>
<div>
<label class="block text-sm text-slate-600">Bridge Token</label>
<div class="flex gap-2 mt-1">
<input type="text" name="bridge_token" class="input" readonly>
<button type="button" class="btn" data-rotate="bridge">Neu erstellen</button>
</div>
</div>
<div>
<label class="block text-sm text-slate-600">Sender Token</label>
<div class="flex gap-2 mt-1">
<input type="text" name="sender_token" class="input" readonly>
<button type="button" class="btn" data-rotate="sender">Neu erstellen</button>
</div>
</div>
<div>
<label class="block text-sm text-slate-600">Externer API-Token</label>
<div class="flex gap-2 mt-1">
<input type="text" name="external_api_token" class="input" readonly>
<button type="button" class="btn" data-rotate="external">Neu erstellen</button>
</div>
</div>
<div class="flex justify-between gap-2 flex-wrap pt-2">
<div class="flex gap-2">
<button type="button" class="btn" data-download="bridge">Bridge-Datei</button>
<button type="button" class="btn" data-download="sender">Sender-Datei</button>
</div>
<button type="submit" class="btn ms-auto">Einstellungen speichern</button>
</div>
</form>
</section>
</main>
<div id="toast-root"></div>
<script src="assets/js/toast.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
<script type="module" src="assets/js/account.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
import { apiAction } from './api.js';
import { initUserPanel, initAccountPage } from './ui-user.js';
import { mountLogoutButton, ensureFloatingLogout } from './ui-auth.js';
async function ensureAuthenticated() {
try {
const me = await apiAction('auth.me', { method: 'GET' });
if (!me?.ok || !me?.user) {
window.location.href = '/login.php';
return false;
}
window.__currentUser = me.user;
document.documentElement.classList.remove('auth-pending');
return true;
} catch {
return false;
}
}
document.addEventListener('DOMContentLoaded', async () => {
const ok = await ensureAuthenticated();
if (!ok) return;
initUserPanel();
initAccountPage();
mountLogoutButton('#btn-logout', { redirect: '/login.php' });
ensureFloatingLogout({ redirect: '/login.php' });
});

View File

@@ -9,7 +9,6 @@ const state = {
loading: false, loading: false,
}; };
let dialog;
let avatarBtn; let avatarBtn;
let profileForm; let profileForm;
let passwordForm; let passwordForm;
@@ -18,31 +17,26 @@ let teamTable;
let userForm; let userForm;
export function initUserPanel() { export function initUserPanel() {
dialog = document.getElementById('userDialog');
avatarBtn = document.getElementById('btn-user'); avatarBtn = document.getElementById('btn-user');
if (!dialog || !avatarBtn) return; updateAvatar();
}
export function initAccountPage() {
profileForm = document.getElementById('profileForm'); profileForm = document.getElementById('profileForm');
passwordForm = document.getElementById('passwordForm'); passwordForm = document.getElementById('passwordForm');
settingsForm = document.getElementById('settingsForm'); settingsForm = document.getElementById('settingsForm');
teamTable = document.getElementById('teamTable'); teamTable = document.getElementById('teamTable');
userForm = document.getElementById('userForm'); 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('btn-user-add')?.addEventListener('click', () => openUserForm());
document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm()); document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm());
userForm?.addEventListener('submit', submitUserForm); userForm?.addEventListener('submit', submitUserForm);
profileForm?.addEventListener('submit', submitProfileForm);
passwordForm?.addEventListener('submit', submitPasswordForm);
settingsForm?.addEventListener('submit', submitSettingsForm);
teamTable?.addEventListener('click', handleTeamTableClick); teamTable?.addEventListener('click', handleTeamTableClick);
dialog.addEventListener('close', () => closeUserForm());
document.querySelectorAll('[data-user-tab]').forEach(btn => { document.querySelectorAll('[data-user-tab]').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab'))); btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
}); });
@@ -64,8 +58,8 @@ export function initUserPanel() {
}); });
}); });
updateAvatar(); switchTab(state.currentTab);
updateOwnerVisibility(); loadAccountData();
} }
function isOwner() { function isOwner() {
@@ -98,13 +92,6 @@ function switchTab(tab) {
}); });
} }
async function openUserDialog() {
if (dialog.open || state.loading) return;
dialog.showModal();
switchTab(state.currentTab);
await loadAccountData();
}
async function loadAccountData() { async function loadAccountData() {
try { try {
state.loading = true; state.loading = true;
@@ -123,7 +110,6 @@ async function loadAccountData() {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
toast(err.message || 'Fehler beim Laden', false); toast(err.message || 'Fehler beim Laden', false);
dialog.close();
} finally { } finally {
state.loading = false; state.loading = false;
} }
@@ -343,10 +329,6 @@ function copyToClipboard(value) {
} }
} }
function closeUserDialog() {
dialog?.close();
}
function escapeHtml(str) { function escapeHtml(str) {
return String(str || '') return String(str || '')
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')

View File

@@ -40,11 +40,6 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1rem;margin-bottom:1.25rem} .section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1rem;margin-bottom:1.25rem}
.section-card h4{margin:0 0 .75rem;font-size:1rem;font-weight:600;color:#0f172a} .section-card h4{margin:0 0 .75rem;font-size:1rem;font-weight:600;color:#0f172a}
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem} .input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
.user-tabs{display:flex;gap:.5rem;margin-bottom:1rem}
.user-panel{width:90vw;max-width:960px}
.team-table{width:100%;border-collapse:collapse;font-size:.9rem}
.team-table th,.team-table td{padding:.35rem .5rem;border-bottom:1px solid #e2e8f0;text-align:left}
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
</style> </style>
</head> </head>
<body class="page-admin bg-slate-50 text-slate-800"> <body class="page-admin bg-slate-50 text-slate-800">
@@ -59,9 +54,9 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
</nav> </nav>
<div class="ms-auto flex gap-2 items-center"> <div class="ms-auto flex gap-2 items-center">
<button id="btn-new" type="button" class="btn">Neu …</button> <button id="btn-new" type="button" class="btn">Neu …</button>
<button id="btn-user" type="button" class="btn-avatar" title="Mein Konto"> <a href="account.php" id="btn-user" class="btn-avatar" title="Mein Konto">
<span id="userAvatar">U</span> <span id="userAvatar">U</span>
</button> </a>
</div> </div>
</div> </div>
</header> </header>
@@ -165,137 +160,9 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
</form> </form>
</dialog> </dialog>
<!-- User Account Dialog -->
<dialog id="userDialog" class="rounded-2xl p-0 user-panel">
<div class="flex flex-col bg-white rounded-2xl max-h-[90vh]">
<div class="px-4 py-3 border-b flex items-center gap-3 bg-white/80 backdrop-blur rounded-t-2xl">
<strong class="text-lg me-auto">Mein Konto</strong>
<button type="button" id="userClose" class="btn">Schließen</button>
</div>
<div class="p-4 overflow-y-auto flex-1">
<div class="user-tabs">
<button type="button" data-user-tab="profile" class="btn bg-sky-50 text-sky-700 flex-1">Profil</button>
<button type="button" data-user-tab="security" class="btn flex-1">Passwort</button>
<button type="button" data-user-tab="team" class="btn flex-1 owner-only hidden">Team</button>
<button type="button" data-user-tab="integration" class="btn flex-1 owner-only hidden">Integrationen</button>
</div>
<section data-user-panel="profile" class="section-card">
<h4>Profil</h4>
<form id="profileForm" class="space-y-3">
<label class="block text-sm text-slate-600">Name
<input type="text" id="profile_name" name="name" class="input mt-1" required>
</label>
<label class="block text-sm text-slate-600">E-Mail
<input type="email" id="profile_email" name="email" class="input mt-1" required>
</label>
<div class="flex justify-end">
<button type="submit" class="btn">Speichern</button>
</div>
</form>
</section>
<section data-user-panel="security" class="section-card hidden">
<h4>Passwort ändern</h4>
<form id="passwordForm" class="space-y-3">
<label class="block text-sm text-slate-600">Aktuelles Passwort
<input type="password" name="current_password" class="input mt-1" required>
</label>
<label class="block text-sm text-slate-600">Neues Passwort (min. 8 Zeichen)
<input type="password" name="new_password" class="input mt-1" required minlength="8">
</label>
<div class="flex justify-end">
<button type="submit" class="btn">Aktualisieren</button>
</div>
</form>
</section>
<section data-user-panel="team" class="section-card hidden owner-only">
<div class="flex items-center justify-between mb-2">
<h4>Team</h4>
<button type="button" id="btn-user-add" class="btn">+ Nutzer</button>
</div>
<div class="overflow-auto">
<table class="team-table" id="teamTable">
<thead>
<tr><th>Name</th><th>E-Mail</th><th>Rolle</th><th>Status</th><th class="text-right">Aktionen</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
<form id="userForm" class="space-y-3 mt-4 hidden">
<input type="hidden" name="user_id">
<label class="block text-sm text-slate-600">Name
<input type="text" name="name" class="input mt-1" required>
</label>
<label class="block text-sm text-slate-600">E-Mail
<input type="email" name="email" class="input mt-1" required>
</label>
<label class="block text-sm text-slate-600">Rolle
<select name="role" class="input mt-1">
<option value="owner">Owner</option>
<option value="admin">Admin</option>
<option value="editor">Editor</option>
<option value="viewer">Viewer</option>
</select>
</label>
<label class="inline-flex items-center gap-2 text-sm text-slate-600">
<input type="checkbox" name="is_active" checked> Aktiv
</label>
<label class="inline-flex items-center gap-2 text-sm text-slate-600 reset-only hidden">
<input type="checkbox" name="reset_password"> Passwort zurücksetzen
</label>
<div class="flex justify-end gap-2">
<button type="button" id="userFormCancel" class="btn">Abbrechen</button>
<button type="submit" class="btn">Speichern</button>
</div>
</form>
</section>
<section data-user-panel="integration" class="section-card hidden owner-only">
<h4>Integrationen & Tokens</h4>
<form id="settingsForm" class="space-y-3">
<label class="block text-sm text-slate-600">Bridge-URL
<input type="url" name="bridge_url" class="input mt-1" placeholder="https://domain.tld/emailtemplate_bridge.php">
</label>
<div>
<label class="block text-sm text-slate-600">Bridge Token</label>
<div class="flex gap-2 mt-1">
<input type="text" name="bridge_token" class="input" readonly>
<button type="button" class="btn" data-rotate="bridge">Neu erstellen</button>
</div>
</div>
<div>
<label class="block text-sm text-slate-600">Sender Token</label>
<div class="flex gap-2 mt-1">
<input type="text" name="sender_token" class="input" readonly>
<button type="button" class="btn" data-rotate="sender">Neu erstellen</button>
</div>
</div>
<div>
<label class="block text-sm text-slate-600">Externer API-Token</label>
<div class="flex gap-2 mt-1">
<input type="text" name="external_api_token" class="input" readonly>
<button type="button" class="btn" data-rotate="external">Neu erstellen</button>
</div>
</div>
<div class="flex justify-between gap-2 flex-wrap pt-2">
<div class="flex gap-2">
<button type="button" class="btn" data-download="bridge">Bridge-Datei</button>
<button type="button" class="btn" data-download="sender">Sender-Datei</button>
</div>
<button type="submit" class="btn ms-auto">Einstellungen speichern</button>
</div>
</form>
</section>
</div>
</div>
</dialog>
<div id="toast-root"></div> <div id="toast-root"></div>
<script src="assets/js/toast.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script> <script src="assets/js/toast.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
<script type="module" src="assets/js/app.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script> <script type="module" src="assets/js/app.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
<script type="module" src="assets/js/ui-user.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
</body> </body>
</html> </html>