Owner accounts: auth, password reset, CRM dashboard visibility (#19)

Flask-session login scoped to one client_id (never a request param),
self-service + operator-triggered password reset via single-use tokens,
and an Owner accounts tab on the CRM dashboard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 17:00:37 +02:00
parent 644c99ee30
commit 59c35ce39f
14 changed files with 635 additions and 0 deletions
+28
View File
@@ -82,6 +82,7 @@
<button class="tab active" data-e="leads">Leads<span class="n" id="n-leads"></span></button>
<button class="tab" data-e="clients">Clients<span class="n" id="n-clients"></span></button>
<button class="tab" data-e="credentials">Credentials<span class="n" id="n-credentials"></span></button>
<button class="tab" data-e="owner_users">Eigentümer-Konten<span class="n" id="n-owner_users"></span></button>
</div>
<div class="bar">
<input id="search" placeholder="Filtern …" />
@@ -144,6 +145,7 @@
}
function render(){
if(current === 'owner_users'){ renderOwnerUsers(); return; }
const cols = COLS[current];
let rows = cache[current] || [];
const q = document.getElementById('search').value.trim().toLowerCase();
@@ -170,6 +172,31 @@
document.getElementById('table').innerHTML = `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
}
function renderOwnerUsers(){
let rows = cache.owner_users || [];
const q = document.getElementById('search').value.trim().toLowerCase();
if(q) rows = rows.filter(r => ['client_id','email'].some(c => String(r[c]??'').toLowerCase().includes(q)));
if(!rows.length){ document.getElementById('table').innerHTML = '<div class="empty">Keine Einträge.</div>'; return; }
const head = '<tr><th>client_id</th><th>email</th><th>created_at</th><th></th></tr>';
const body = rows.map(r => {
const created = r.created_at ? String(r.created_at).replace('T',' ').slice(0,16) : '';
return `<tr><td title="${esc(r.client_id)}">${esc(r.client_id)}</td><td>${esc(r.email)}</td>` +
`<td>${esc(created)}</td><td class="actcol">` +
`<button class="act" onclick="sendOwnerReset('${esc(r.user_id)}')">Reset-Link senden</button></td></tr>`;
}).join('');
document.getElementById('table').innerHTML = `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
}
async function sendOwnerReset(userId){
clearErr();
try {
const r = await fetch(`${API}/owner_users/${encodeURIComponent(userId)}/reset-password`, {
method: 'POST', headers: { 'X-CRM-Token': TOKEN } });
if(!r.ok) throw new Error('HTTP '+r.status);
showOk('✓ Reset-Link gesendet');
} catch(e){ showErr('Senden fehlgeschlagen: '+e.message); }
}
function toggleSecret(id){
if(revealed.has(id)) revealed.delete(id); else revealed.add(id);
render();
@@ -276,6 +303,7 @@
document.querySelectorAll('.tab').forEach(x => x.classList.remove('active'));
t.classList.add('active');
current = t.dataset.e;
document.getElementById('add').style.display = (current === 'owner_users') ? 'none' : '';
if(cache[current]) render(); else load(current);
});
document.getElementById('search').oninput = render;