feat(cms): confirm-before-logout and branded login page
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
- logout asks for confirmation, then invalidates the server-side session and navigates to a public /login page (logo, form, error messages) - POST /login validates credentials (timing-safe) and issues an HttpOnly SameSite=Strict session cookie (8h, Secure behind HTTPS); Basic Auth stays valid in parallel for curl/API use - unauthenticated browser navigations redirect to /login; non-browser requests keep the 401 challenge - failed form logins share the auth rate-limit budget with Basic attempts - save/publish redirect to /login when the session expired - refactor: templates and browser script extracted to scripts/cms-pages.js and scripts/cms-editor-client.js, session logic to scripts/cms-session.js (content-editor.js back under the 400-line limit) - user guide updated (login page, confirmation, 8h session) Closes MITHOME-58
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// WHY: Basic Auth has no native logout and its dialog cannot be styled, so a
|
||||
// successful /login form submit receives a server-side session token in an
|
||||
// HttpOnly cookie. Basic Auth remains valid in parallel (curl, API use).
|
||||
const crypto = require('crypto');
|
||||
|
||||
const SESSION_COOKIE = 'cms_session';
|
||||
const SESSION_TTL_MS = 8 * 60 * 60 * 1000;
|
||||
const sessions = new Map(); // token -> expiresAt (ms)
|
||||
|
||||
function timingSafeMatch(candidate, expected) {
|
||||
if (typeof candidate !== 'string' || typeof expected !== 'string' || candidate.length !== expected.length) return false;
|
||||
return crypto.timingSafeEqual(Buffer.from(candidate), Buffer.from(expected));
|
||||
}
|
||||
|
||||
function validateLogin(user, pass, expectedUser, expectedPass) {
|
||||
if (!expectedUser || !expectedPass) return false;
|
||||
return timingSafeMatch(user, expectedUser) && timingSafeMatch(pass, expectedPass);
|
||||
}
|
||||
|
||||
function createSessionCookie(isSecure) {
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
sessions.set(token, Date.now() + SESSION_TTL_MS);
|
||||
return `${SESSION_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${Math.floor(SESSION_TTL_MS / 1000)}${isSecure ? '; Secure' : ''}`;
|
||||
}
|
||||
|
||||
function clearSessionCookie() {
|
||||
return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`;
|
||||
}
|
||||
|
||||
function getSessionToken(req) {
|
||||
const cookies = req.headers.cookie || '';
|
||||
const match = cookies.match(new RegExp(`(?:^|;\\s*)${SESSION_COOKIE}=([a-f0-9]+)`));
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function hasValidSession(req) {
|
||||
const token = getSessionToken(req);
|
||||
if (!token) return false;
|
||||
const expiresAt = sessions.get(token);
|
||||
if (!expiresAt) return false;
|
||||
if (Date.now() > expiresAt) {
|
||||
sessions.delete(token);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function deleteSession(req) {
|
||||
const token = getSessionToken(req);
|
||||
if (token) sessions.delete(token);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SESSION_COOKIE,
|
||||
SESSION_TTL_MS,
|
||||
timingSafeMatch,
|
||||
validateLogin,
|
||||
createSessionCookie,
|
||||
clearSessionCookie,
|
||||
hasValidSession,
|
||||
deleteSession,
|
||||
};
|
||||
Reference in New Issue
Block a user