- Rollensystem: Public → Mitarbeiter → Admin (Rolle user entfällt) - DB-Migration: must_change_password-Spalte, user→mitarbeiter - require_staff_api(): erlaubt mitarbeiter + admin - POST /api/admin/invite: Einladung mit auto-generiertem Passwort + E-Mail - POST /auth/change-password: Pflicht-Passwortwechsel - Login: force_password_change-Redirect - Sidebar: sidebar-staff für Scheduler/NINA/Einstellungen - Scheduler/NINA: read-only für Mitarbeiter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
174 lines
6.4 KiB
JavaScript
174 lines
6.4 KiB
JavaScript
// Theme toggle
|
|
const themeToggle = document.getElementById('themeToggle');
|
|
const themeIcon = document.getElementById('themeIcon');
|
|
|
|
function applyTheme(theme) {
|
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
|
themeIcon.className = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill';
|
|
localStorage.setItem('theme', theme);
|
|
}
|
|
|
|
applyTheme(localStorage.getItem('theme') || 'light');
|
|
|
|
themeToggle.addEventListener('click', () => {
|
|
const current = document.documentElement.getAttribute('data-bs-theme');
|
|
applyTheme(current === 'dark' ? 'light' : 'dark');
|
|
});
|
|
|
|
// Footer
|
|
(function () {
|
|
const footer = document.getElementById('pageFooter');
|
|
if (!footer) return;
|
|
const now = new Date();
|
|
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
const yyyy = now.getFullYear();
|
|
fetch('/api/stats').then(r => r.ok ? r.json() : null).then(d => {
|
|
const ver = d?.version ? ` · v${d.version}` : '';
|
|
footer.textContent = `© MeshDD / PPfeiffer${ver} · ${mm}/${yyyy}`;
|
|
}).catch(() => {
|
|
footer.textContent = `© MeshDD / PPfeiffer · ${mm}/${yyyy}`;
|
|
});
|
|
})();
|
|
|
|
// View switching
|
|
const views = {
|
|
login: document.getElementById('loginView'),
|
|
register: document.getElementById('registerView'),
|
|
forgot: document.getElementById('forgotView'),
|
|
setPassword: document.getElementById('setPasswordView'),
|
|
};
|
|
|
|
function showView(name) {
|
|
Object.values(views).forEach(v => v.classList.add('d-none'));
|
|
views[name].classList.remove('d-none');
|
|
}
|
|
|
|
document.getElementById('showRegister').addEventListener('click', (e) => { e.preventDefault(); showView('register'); });
|
|
document.getElementById('showLoginFromReg').addEventListener('click', (e) => { e.preventDefault(); showView('login'); });
|
|
document.getElementById('showForgot').addEventListener('click', (e) => { e.preventDefault(); showView('forgot'); });
|
|
document.getElementById('showLoginFromForgot').addEventListener('click', (e) => { e.preventDefault(); showView('login'); });
|
|
|
|
// Check URL for token (verify or reset-password)
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const token = urlParams.get('token');
|
|
const isVerify = window.location.pathname === '/auth/verify';
|
|
const isReset = window.location.pathname === '/auth/reset-password';
|
|
|
|
if (token && (isVerify || isReset)) {
|
|
showView('setPassword');
|
|
document.getElementById('setPasswordTitle').textContent = isVerify ? 'Passwort setzen' : 'Passwort zuruecksetzen';
|
|
}
|
|
|
|
// Show register view if on /register
|
|
if (window.location.pathname === '/register') {
|
|
showView('register');
|
|
}
|
|
|
|
function showAlert(id, message, type) {
|
|
const el = document.getElementById(id);
|
|
el.className = `alert alert-${type} py-1 small`;
|
|
el.textContent = message;
|
|
}
|
|
|
|
// Login
|
|
document.getElementById('btnLogin').addEventListener('click', async () => {
|
|
const email = document.getElementById('loginEmail').value.trim();
|
|
const password = document.getElementById('loginPassword').value;
|
|
if (!email || !password) return;
|
|
|
|
try {
|
|
const resp = await fetch('/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password })
|
|
});
|
|
const data = await resp.json();
|
|
if (resp.ok) {
|
|
window.location.href = data.force_password_change ? '/auth/change-password' : '/';
|
|
} else {
|
|
showAlert('loginAlert', data.error || 'Anmeldung fehlgeschlagen', 'danger');
|
|
}
|
|
} catch (e) {
|
|
showAlert('loginAlert', 'Verbindungsfehler', 'danger');
|
|
}
|
|
});
|
|
|
|
document.getElementById('loginPassword').addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') document.getElementById('btnLogin').click();
|
|
});
|
|
|
|
// Register
|
|
document.getElementById('btnRegister').addEventListener('click', async () => {
|
|
const name = document.getElementById('registerName').value.trim();
|
|
const email = document.getElementById('registerEmail').value.trim();
|
|
if (!name || !email) return;
|
|
|
|
try {
|
|
const resp = await fetch('/auth/register', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, email })
|
|
});
|
|
const data = await resp.json();
|
|
if (resp.ok) {
|
|
showAlert('registerAlert', data.message || 'Registrierung erfolgreich', 'success');
|
|
} else {
|
|
showAlert('registerAlert', data.error || 'Registrierung fehlgeschlagen', 'danger');
|
|
}
|
|
} catch (e) {
|
|
showAlert('registerAlert', 'Verbindungsfehler', 'danger');
|
|
}
|
|
});
|
|
|
|
// Forgot password
|
|
document.getElementById('btnForgot').addEventListener('click', async () => {
|
|
const email = document.getElementById('forgotEmail').value.trim();
|
|
if (!email) return;
|
|
|
|
try {
|
|
const resp = await fetch('/auth/forgot-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email })
|
|
});
|
|
const data = await resp.json();
|
|
showAlert('forgotAlert', data.message || 'Link gesendet', 'success');
|
|
} catch (e) {
|
|
showAlert('forgotAlert', 'Verbindungsfehler', 'danger');
|
|
}
|
|
});
|
|
|
|
// Set password (verify + reset)
|
|
document.getElementById('btnSetPassword').addEventListener('click', async () => {
|
|
const password = document.getElementById('newPassword').value;
|
|
const confirm = document.getElementById('confirmPassword').value;
|
|
|
|
if (password.length < 8) {
|
|
showAlert('setPasswordAlert', 'Passwort muss mindestens 8 Zeichen lang sein', 'danger');
|
|
return;
|
|
}
|
|
if (password !== confirm) {
|
|
showAlert('setPasswordAlert', 'Passwoerter stimmen nicht ueberein', 'danger');
|
|
return;
|
|
}
|
|
|
|
const endpoint = isReset ? '/auth/reset-password' : '/auth/set-password';
|
|
|
|
try {
|
|
const resp = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token, password })
|
|
});
|
|
const data = await resp.json();
|
|
if (resp.ok) {
|
|
showAlert('setPasswordAlert', data.message || 'Passwort gespeichert', 'success');
|
|
setTimeout(() => { window.location.href = '/login'; }, 2000);
|
|
} else {
|
|
showAlert('setPasswordAlert', data.error || 'Fehler', 'danger');
|
|
}
|
|
} catch (e) {
|
|
showAlert('setPasswordAlert', 'Verbindungsfehler', 'danger');
|
|
}
|
|
});
|