StyxX65 1aaf400771 feat: role-scoped viewer tokens — restrict shared links to student or staff items
Add a Role scope dropdown to the Share modal (All roles / Ansatte / Elever).
Scope is stored as {"role": "student"|"staff"} in viewer_tokens.json and
enforced server-side in GET /api/db/flagged via session["viewer_scope"].
Client-side, #filterRole is pre-set and hidden for scoped viewers so the
constraint cannot be bypassed. Existing tokens and PIN sessions remain
unrestricted. Role badge shown on each scoped token row in the Active links list.

Files: app_config.py, routes/viewer.py, routes/database.py, gdpr_scanner.py,
templates/index.html, static/js/viewer.js, static/js/auth.js,
lang/en.json, lang/da.json, lang/de.json,
CLAUDE.md, CHANGELOG.md, README.md, MANUAL-EN.md, MANUAL-DA.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 09:30:38 +02:00

205 lines
8.8 KiB
JavaScript

import { S } from './state.js';
// ── Auth ─────────────────────────────────────────────────────────────────────
function handleSignIn() {
try {
startAuth().catch(function(e) {
alert('Sign-in error: ' + (e.message || String(e)));
});
} catch(e) {
alert('Sign-in error: ' + (e.message || String(e)));
}
}
async function startAuth() {
const clientId = document.getElementById('clientId').value.trim();
const tenantId = document.getElementById('tenantId').value.trim();
const clientSecret = document.getElementById('clientSecret').value.trim();
if (!clientId || !tenantId) { alert('Enter Client ID and Tenant ID'); return; }
// Persist credentials first so they survive restarts regardless of auth outcome
await fetch('/api/auth/config', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({client_id: clientId, tenant_id: tenantId, client_secret: clientSecret})
});
const r = await fetch('/api/auth/start', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({client_id: clientId, tenant_id: tenantId, client_secret: clientSecret})
});
const d = await r.json();
if (d.error) { alert(d.error); return; }
if (d.mode === 'application') {
// App mode — token acquired immediately, no device code step needed
document.getElementById('configForm').style.display = 'none';
document.getElementById('deviceCodeBackdrop').classList.add('open');
document.getElementById('deviceCode').textContent = '—';
document.getElementById('authStatus').className = 'auth-status success';
document.getElementById('authStatus').textContent = '✓ Connected (Application mode — org-wide access)';
setTimeout(onAuthenticated, 900);
return;
}
document.getElementById('configForm').style.display = 'none';
document.getElementById('deviceCodeBackdrop').classList.add('open');
document.getElementById('deviceCode').textContent = d.user_code;
pollAuth();
}
async function pollAuth() {
const r = await fetch('/api/auth/poll', {method: 'POST'});
const d = await r.json();
if (d.status === 'pending') {
setTimeout(pollAuth, 3000);
} else if (d.status === 'ok') {
document.getElementById('authStatus').className = 'auth-status success';
document.getElementById('authStatus').textContent = '✓ Signed in!';
setTimeout(onAuthenticated, 800);
} else {
document.getElementById('authStatus').className = 'auth-status error';
document.getElementById('authStatus').textContent = '✗ ' + (d.error || 'Sign-in failed');
document.getElementById('configForm').style.display = 'block';
document.getElementById('deviceCodeBackdrop').classList.remove('open');
}
}
function cancelAuth() {
document.getElementById('configForm').style.display = 'block';
document.getElementById('deviceCodeBackdrop').classList.remove('open');
}
let _currentDisplayName = '';
function _setModeBadge(isAppMode, displayName) {
S._currentAppMode = isAppMode;
_currentDisplayName = displayName || '';
// Keep Sources modal status dot in sync if it's open
const dot = document.getElementById('srcM365StatusDot');
if (dot) dot.className = 'srcmgmt-status ' + (isAppMode !== null && isAppMode !== undefined ? 'green' : 'grey');
}
async function onAuthenticated() {
const r = await fetch('/api/auth/status');
const d = await r.json();
if (d.display_name || d.displayName || d.email) {
_setModeBadge(d.app_mode, d.display_name || d.displayName || d.email);
}
document.getElementById('authScreen').style.display = 'none';
document.getElementById('scannerScreen').style.display = 'flex';
loadUsers();
loadTrend(); // show existing trend if DB has history
loadProfiles(); // populate profile dropdown (15c)
}
function reconfigure() {
// Show the auth screen with current credentials pre-filled so user can
// update the client secret without losing client_id / tenant_id.
document.getElementById('scannerScreen').style.display = 'none';
document.getElementById('authScreen').style.display = 'flex';
document.getElementById('configForm').style.display = 'block';
document.getElementById('deviceCodeBackdrop').classList.remove('open');
}
async function signOut() {
await fetch('/api/auth/signout', {method: 'POST'});
document.getElementById('scannerScreen').style.display = 'none';
document.getElementById('authScreen').style.display = 'flex';
document.getElementById('configForm').style.display = 'block';
document.getElementById('deviceCodeBackdrop').classList.remove('open');
S.flaggedData = []; S.filteredData = [];
document.getElementById('grid').innerHTML = '';
document.getElementById('grid').style.display = 'none';
const _lss2 = document.getElementById('lastScanSummary'); if (_lss2) _lss2.style.display = 'none';
document.getElementById('emptyState').style.display = 'flex';
}
// ── Check auth on load ────────────────────────────────────────────────────────
// Date presets
(function() {
const presets = document.querySelectorAll('.date-preset');
const hidden = document.getElementById('olderThan');
const dateIn = document.getElementById('olderThanDate');
function setPreset(btn) {
presets.forEach(p => p.classList.remove('selected'));
btn.classList.add('selected');
const years = parseInt(btn.dataset.years);
if (years === 0) {
hidden.value = '0';
dateIn.value = new Date().toISOString().slice(0, 10);
} else {
const d = new Date();
d.setFullYear(d.getFullYear() - years);
hidden.value = Math.round(years * 365.25).toString();
dateIn.value = d.toISOString().slice(0, 10);
}
}
presets.forEach(btn => btn.addEventListener('click', () => setPreset(btn)));
dateIn.addEventListener('change', () => {
presets.forEach(p => p.classList.remove('selected'));
if (dateIn.value) {
const diffDays = Math.round((Date.now() - new Date(dateIn.value)) / 86400000);
hidden.value = diffDays.toString();
} else {
hidden.value = '0';
}
});
// Trigger default (2yr selected)
const def = document.querySelector('.date-preset.selected');
if (def) setPreset(def);
// Toggle attach size row visibility
document.getElementById('optAttachments').addEventListener('change', function() {
document.getElementById('attachSizeRow').style.opacity = this.checked ? '1' : '0.4';
});
})();
// ── Viewer mode bootstrap ─────────────────────────────────────────────────────
if (window.VIEWER_MODE) {
document.body.classList.add('viewer-mode');
document.getElementById('authScreen').style.display = 'none';
document.getElementById('scannerScreen').style.display = 'flex';
// If this token is role-scoped, lock the filter to that role and hide the dropdown.
const _scopeRole = (window.VIEWER_SCOPE || {}).role || '';
if (_scopeRole) {
const _fr = document.getElementById('filterRole');
if (_fr) { _fr.value = _scopeRole; _fr.style.display = 'none'; }
}
try { loadTrend(); } catch(e) {}
} else {
(async function() {
try {
const r = await fetch('/api/auth/status');
const d = await r.json();
if (d.authenticated) {
// Load saved credentials into fields
if (d.client_id) document.getElementById('clientId').value = d.client_id;
if (d.tenant_id) document.getElementById('tenantId').value = d.tenant_id;
if (d.client_secret) document.getElementById('clientSecret').value = d.client_secret;
_setModeBadge(d.app_mode, d.display_name || d.email || '');
document.getElementById('authScreen').style.display = 'none';
document.getElementById('scannerScreen').style.display = 'flex';
try { loadUsers(); } catch(e) {}
try { loadProfiles(); } catch(e) {}
try { loadTrend(); } catch(e) {}
} else {
// Pre-fill saved credentials
if (d.client_id) document.getElementById('clientId').value = d.client_id;
if (d.tenant_id) document.getElementById('tenantId').value = d.tenant_id;
if (d.client_secret) document.getElementById('clientSecret').value = d.client_secret;
}
} catch(e) { console.error('Auth status check failed:', e); }
})();
}
// ── Window exports (HTML handlers + cross-module calls) ─────────────────────
window.handleSignIn = handleSignIn;
window.startAuth = startAuth;
window.pollAuth = pollAuth;
window.cancelAuth = cancelAuth;
window._setModeBadge = _setModeBadge;
window.onAuthenticated = onAuthenticated;
window.reconfigure = reconfigure;
window.signOut = signOut;
window._currentDisplayName = _currentDisplayName;