Gate the suite behind a self-contained login (no external IdP): - User model with bcrypt-hashed passwords; admin/user roles - /api/auth endpoints: login, logout, me, change-password, and admin-only user management (list/create/delete/reset/enable) - Stateless JWT session in an HttpOnly, SameSite=Lax, auto-Secure cookie; middleware refuses every /api data route without a session - login.html + auth-guard.js: login page and per-page guard with a top-right "name / Admin / Sign out" pill - Admin Console now gated on admin role (passphrase gate removed) with a User administration card - manage_users.py CLI to bootstrap the first admin - Rebuilt help.js into a searchable, multi-topic help center - Local-dev convenience: app serves html/ so the site + API share one origin under uvicorn (inactive in the prod container) - Docs/env: AUTH_SECRET_KEY, requirements (bcrypt, PyJWT), README Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
60 lines
2.3 KiB
JavaScript
60 lines
2.3 KiB
JavaScript
/* Login page logic for the Work Package Suite.
|
|
Posts credentials to /api/auth/login. On success the server sets an HttpOnly
|
|
session cookie (not readable here — that's the point) and we redirect to the
|
|
page the user was trying to reach, or the home page. */
|
|
(function () {
|
|
'use strict';
|
|
|
|
var form = document.getElementById('login-form');
|
|
var errorBox = document.getElementById('error');
|
|
var submitBtn = document.getElementById('submit');
|
|
|
|
// Where to go after signing in: the ?next= param if it's a safe same-site
|
|
// path, otherwise the home page. (Reject absolute/scheme URLs to avoid an
|
|
// open-redirect.)
|
|
function nextTarget() {
|
|
try {
|
|
var next = new URLSearchParams(location.search).get('next') || '';
|
|
if (next && next.charAt(0) === '/' && next.charAt(1) !== '/') return next;
|
|
} catch (e) {}
|
|
return 'index.html';
|
|
}
|
|
|
|
function showError(msg) {
|
|
errorBox.textContent = msg;
|
|
errorBox.classList.add('show');
|
|
}
|
|
|
|
form.addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
errorBox.classList.remove('show');
|
|
var username = document.getElementById('username').value.trim();
|
|
var password = document.getElementById('password').value;
|
|
if (!username || !password) { showError('Enter your username and password.'); return; }
|
|
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Signing in…';
|
|
|
|
fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: username, password: password })
|
|
})
|
|
.then(function (r) {
|
|
if (r.ok) { location.replace(nextTarget()); return null; }
|
|
return r.json().catch(function () { return null; }).then(function (j) {
|
|
if (r.status === 401) showError('Invalid username or password.');
|
|
else if (r.status === 403) showError((j && j.detail) || 'Your account is disabled.');
|
|
else showError((j && j.detail) || ('Sign-in failed (HTTP ' + r.status + ').'));
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Sign in';
|
|
});
|
|
})
|
|
.catch(function () {
|
|
showError('Could not reach the server. Check your connection and try again.');
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Sign in';
|
|
});
|
|
});
|
|
})();
|