Auth-herding: roller, kontoadmin, token-revokering + søkbar planliste + neon
Auth (app-modus):
- Roller admin/standard. Admin oppretter/sletter kontoer, resetter passord,
endrer roller (konto-modal). Eksisterende brukere → admin ved migrering;
bootstrap-bruker via proxy-bypass → admin. Siste admin vernet.
- Tilbakekalling via per-bruker epoke i tokenet: passordbytte, admin-reset,
«Logg ut alle enheter» og sletting dreper alle aktive sesjoner. Token-format
endret (3- → 4-delt) — alle logges ut én gang ved oppgradering.
Dagsplan: søkbar/velgbar planliste («📋 Planer») avledet fra planIndex —
finn plan på navn/dato/person uten å gjette dato.
GUI: login-neon trukket inn i hovedappen (header/knapper/KPI-kanter), og
login-siden fikk mer tech (HUD-hjørner, glitch-tittel, scanlines).
40 integrasjonstester (6 nye), alle grønne. README + CLAUDE.md oppdatert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f6268440d9
commit
3f69cd4248
9 changed files with 518 additions and 36 deletions
|
|
@ -48,7 +48,7 @@ function catName(id) { return (catById(id) || {}).name || id; }
|
|||
const state = {
|
||||
user: 'anon',
|
||||
canSetName: false,
|
||||
auth: { mode: 'proxy', via: null, hasPassword: false },
|
||||
auth: { mode: 'proxy', via: null, hasPassword: false, role: 'standard', isAdmin: false },
|
||||
tasks: [],
|
||||
activity: [],
|
||||
knownUsers: [],
|
||||
|
|
@ -845,6 +845,65 @@ function renderPlan() {
|
|||
if (own) bindPlanDnD();
|
||||
}
|
||||
|
||||
// ---------- Søkbar planliste ----------
|
||||
// Avled plansammendrag fra badge-indeksen (allerede filtrert til synlige planer).
|
||||
function derivePlanList() {
|
||||
const map = new Map();
|
||||
for (const entries of Object.values(state.planIndex)) {
|
||||
for (const e of entries) {
|
||||
const key = e.user + '|' + e.date;
|
||||
let g = map.get(key);
|
||||
if (!g) { g = { user: e.user, date: e.date, name: e.name || '', count: 0 }; map.set(key, g); }
|
||||
g.count++;
|
||||
if (!g.name && e.name) g.name = e.name;
|
||||
}
|
||||
}
|
||||
return [...map.values()].sort((a, b) => {
|
||||
const am = a.user === state.user ? 0 : 1, bm = b.user === state.user ? 0 : 1;
|
||||
if (am !== bm) return am - bm; // mine planer først
|
||||
return b.date.localeCompare(a.date); // så nyeste dato først
|
||||
});
|
||||
}
|
||||
|
||||
function renderPlanPicker(filter = '') {
|
||||
const q = filter.trim().toLowerCase();
|
||||
const list = derivePlanList().filter(p => {
|
||||
if (!q) return true;
|
||||
return [p.name, p.date, p.user === state.user ? 'meg' : p.user].join(' ').toLowerCase().includes(q);
|
||||
});
|
||||
const el = $('plan-picker-list');
|
||||
if (!list.length) {
|
||||
el.innerHTML = `<div class="plan-pick-empty">${q ? 'Ingen planer matcher søket.' : 'Ingen planer ennå.'}</div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML = list.map(p => {
|
||||
const me = p.user === state.user;
|
||||
const d = p.date.slice(8, 10) + '.' + p.date.slice(5, 7) + '.' + p.date.slice(0, 4);
|
||||
return `<div class="plan-pick-row" onclick="pickPlan('${escapeHtml(p.user)}','${p.date}')">
|
||||
<span class="pp-date">${d}</span>
|
||||
<span class="pp-name ${p.name ? '' : 'unnamed'}">${p.name ? escapeHtml(p.name) : '(uten navn)'}</span>
|
||||
<span class="pp-who ${me ? 'me' : ''}">${me ? 'meg' : escapeHtml(p.user)}</span>
|
||||
<span class="pp-count">${p.count}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function togglePlanPicker(open) {
|
||||
const panel = $('plan-picker-panel');
|
||||
const show = open != null ? open : !panel.classList.contains('open');
|
||||
panel.classList.toggle('open', show);
|
||||
if (show) {
|
||||
$('plan-picker-search').value = '';
|
||||
renderPlanPicker('');
|
||||
setTimeout(() => $('plan-picker-search').focus(), 30);
|
||||
}
|
||||
}
|
||||
|
||||
window.pickPlan = function (user, date) {
|
||||
togglePlanPicker(false);
|
||||
refreshPlan(date, user);
|
||||
};
|
||||
|
||||
function populatePlanUserSelect() {
|
||||
const sel = $('plan-view-user');
|
||||
const users = new Set([state.user, ...state.knownUsers, ...state.online]);
|
||||
|
|
@ -1413,11 +1472,78 @@ function openAccount() {
|
|||
$('account-current').value = '';
|
||||
$('account-newpw').value = '';
|
||||
$('account-newpw2').value = '';
|
||||
// Logg ut alle enheter krever en lagret konto (ikke ren proxy-bypass)
|
||||
$('account-logout-all').style.display = has ? '' : 'none';
|
||||
// Rolle-badge
|
||||
const badge = $('account-role-badge');
|
||||
badge.textContent = state.auth.role || 'standard';
|
||||
badge.className = 'role-badge ' + (state.auth.isAdmin ? 'admin' : 'standard');
|
||||
badge.style.display = '';
|
||||
// Admin-seksjon
|
||||
$('account-admin').style.display = state.auth.isAdmin ? '' : 'none';
|
||||
if (state.auth.isAdmin) loadUserList();
|
||||
$('account-modal').classList.add('open');
|
||||
setTimeout(() => $('account-newpw').focus(), 50);
|
||||
}
|
||||
function closeAccount() { $('account-modal').classList.remove('open'); }
|
||||
|
||||
// ---------- Kontoadministrasjon (admin) ----------
|
||||
async function loadUserList() {
|
||||
const box = $('admin-userlist');
|
||||
try {
|
||||
const { users } = await api('GET', '/api/auth/users');
|
||||
box.innerHTML = users.map(u => {
|
||||
const me = u.user === state.user;
|
||||
const toRole = u.role === 'admin' ? 'standard' : 'admin';
|
||||
return `<div class="admin-user">
|
||||
<span class="au-name">${escapeHtml(u.user)}</span>
|
||||
<span class="role-badge ${u.role}">${u.role}</span>
|
||||
${me ? '<span class="au-me">deg</span>' : `<div class="au-actions">
|
||||
<button class="icon-btn" title="Gjør til ${toRole}" onclick="adminSetRole('${escapeHtml(u.user)}','${toRole}')">⇅</button>
|
||||
<button class="icon-btn" title="Tilbakestill passord" onclick="adminResetPassword('${escapeHtml(u.user)}')">🔑</button>
|
||||
<button class="icon-btn del" title="Slett konto" onclick="adminDeleteUser('${escapeHtml(u.user)}')">🗑</button>
|
||||
</div>`}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) { box.innerHTML = `<div class="plan-empty">Kunne ikke laste brukere: ${escapeHtml(e.message)}</div>`; }
|
||||
}
|
||||
|
||||
async function adminCreateUser() {
|
||||
const user = $('admin-new-user').value.trim();
|
||||
const password = $('admin-new-pass').value;
|
||||
const role = $('admin-new-role').value;
|
||||
if (!user) { toast('Skriv et brukernavn', { kind: 'warn' }); return; }
|
||||
if (password.length < 6) { toast('Passord må være minst 6 tegn', { kind: 'warn' }); return; }
|
||||
try {
|
||||
await api('POST', '/api/auth/users', { user, password, role });
|
||||
$('admin-new-user').value = ''; $('admin-new-pass').value = '';
|
||||
loadUserList();
|
||||
toast(`Konto «${user}» (${role}) opprettet`);
|
||||
} catch (e) { toast('Feil: ' + e.message, { kind: 'err' }); }
|
||||
}
|
||||
|
||||
window.adminSetRole = async function (user, role) {
|
||||
try { await api('PUT', `/api/auth/users/${encodeURIComponent(user)}/role`, { role }); loadUserList(); toast(`${user} er nå ${role}`); }
|
||||
catch (e) { toast('Feil: ' + e.message, { kind: 'err' }); }
|
||||
};
|
||||
window.adminResetPassword = async function (user) {
|
||||
const pw = prompt(`Nytt passord for ${user} (min 6 tegn). Brukeren blir logget ut overalt:`);
|
||||
if (pw == null) return;
|
||||
try { await api('POST', `/api/auth/users/${encodeURIComponent(user)}/password`, { password: pw }); toast(`Passord tilbakestilt for ${user}`); }
|
||||
catch (e) { toast('Feil: ' + e.message, { kind: 'err' }); }
|
||||
};
|
||||
window.adminDeleteUser = async function (user) {
|
||||
if (!confirm(`Slette kontoen «${user}»? Dette kan ikke angres.`)) return;
|
||||
try { await api('DELETE', `/api/auth/users/${encodeURIComponent(user)}`); loadUserList(); toast(`Konto «${user}» slettet`); }
|
||||
catch (e) { toast('Feil: ' + e.message, { kind: 'err' }); }
|
||||
};
|
||||
|
||||
async function logoutAll() {
|
||||
if (!confirm('Logg ut alle enheter? Du forblir innlogget her, men alle andre økter avsluttes.')) return;
|
||||
try { await api('POST', '/api/auth/logout-all'); toast('Alle andre økter er logget ut'); }
|
||||
catch (e) { toast('Feil: ' + e.message, { kind: 'err' }); }
|
||||
}
|
||||
|
||||
async function saveAccountPassword() {
|
||||
const pw = $('account-newpw').value;
|
||||
const pw2 = $('account-newpw2').value;
|
||||
|
|
@ -1426,9 +1552,10 @@ async function saveAccountPassword() {
|
|||
const body = { password: pw };
|
||||
if ($('account-current-field').style.display !== 'none') body.current = $('account-current').value;
|
||||
try {
|
||||
await api('POST', '/api/auth/password', body);
|
||||
const r = await api('POST', '/api/auth/password', body);
|
||||
state.auth.hasPassword = true;
|
||||
state.auth.via = 'session';
|
||||
if (r.role) { state.auth.role = r.role; state.auth.isAdmin = r.role === 'admin'; }
|
||||
closeAccount();
|
||||
renderHeader();
|
||||
toast('Passord lagret — du er nå innlogget med app-sesjon');
|
||||
|
|
@ -1479,6 +1606,10 @@ function bindUI() {
|
|||
$('plan-name').addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); e.target.blur(); } });
|
||||
$('plan-shared').addEventListener('change', togglePlanShared);
|
||||
$('plan-move-btn').addEventListener('click', movePlanToDate);
|
||||
$('plan-picker-btn').addEventListener('click', e => { e.stopPropagation(); togglePlanPicker(); });
|
||||
$('plan-picker-search').addEventListener('input', e => renderPlanPicker(e.target.value));
|
||||
$('plan-picker-panel').addEventListener('click', e => e.stopPropagation());
|
||||
document.addEventListener('click', () => togglePlanPicker(false)); // klikk utenfor lukker
|
||||
$('fieldBtn').addEventListener('click', toggleFieldMode);
|
||||
bindPlanContainer();
|
||||
|
||||
|
|
@ -1550,6 +1681,9 @@ function bindUI() {
|
|||
$('account-cancel').addEventListener('click', closeAccount);
|
||||
$('account-save').addEventListener('click', saveAccountPassword);
|
||||
$('account-logout').addEventListener('click', logout);
|
||||
$('account-logout-all').addEventListener('click', logoutAll);
|
||||
$('admin-create-btn').addEventListener('click', adminCreateUser);
|
||||
$('admin-new-pass').addEventListener('keydown', e => { if (e.key === 'Enter') adminCreateUser(); });
|
||||
$('account-modal').addEventListener('click', e => { if (e.target.id === 'account-modal') closeAccount(); });
|
||||
$('account-newpw2').addEventListener('keydown', e => { if (e.key === 'Enter') saveAccountPassword(); });
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue