diff --git a/content b/content index aa8507a..44f1bc7 160000 --- a/content +++ b/content @@ -1 +1 @@ -Subproject commit aa8507aa61e119eb555de138d8399bda1e21c1ba +Subproject commit 44f1bc75bb646bf15914671c300b704790a4e27a diff --git a/public/css/styles.css b/public/css/styles.css index cd85c80..0e6c7f7 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -334,7 +334,7 @@ background-color: var(--bg-main); padding: 2rem; border-radius: 6px; - box-shadow: 0 2px 6px rgba(0,0,0,0.05); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); } main h1 { font-size: 1.5rem; @@ -362,7 +362,6 @@ display: flex; gap: 1rem; position: relative; - z-index: auto; } header#site-header nav.site-nav a, nav.site-nav .dropdown-content form button { diff --git a/public/js/credentialManager.js b/public/js/credentialManager.js index 7d1a62c..a068221 100644 --- a/public/js/credentialManager.js +++ b/public/js/credentialManager.js @@ -11,6 +11,7 @@ const urlParams = new URLSearchParams(window.location.search); this.hasRedirect = urlParams.has("rd"); this.redirectUri = urlParams.get("rd") || "/"; + this.action = urlParams.get("action"); } /** @@ -18,6 +19,12 @@ */ async init() { // 1. Determine Identity State before binding listeners + if (this.action === "logout") { + await this.handleLogout(); + window.location.href = window.location.pathname; + console.log(window.location.pathname); + return; + } await this.checkSession(); const showTokenBtn = document.getElementById("show-token-entry-btn"); diff --git a/src/css/main.css b/src/css/main.css index aca7e10..65dbc48 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -4,7 +4,7 @@ background-color: var(--bg-main); padding: 2rem; border-radius: 6px; - box-shadow: 0 2px 6px rgba(0,0,0,0.05); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); } main h1 { diff --git a/src/css/nav.css b/src/css/nav.css index cf6aa75..f8dc4a0 100644 --- a/src/css/nav.css +++ b/src/css/nav.css @@ -2,7 +2,6 @@ display: flex; gap: 1rem; position: relative; - z-index: auto; } header#site-header nav.site-nav a, nav.site-nav .dropdown-content form button { @@ -130,6 +129,7 @@ nav.site-nav .dropdown-content > .dropdown:hover > .dropdown-content { display: block; } + /* Nested Submenu Positioning */ nav.site-nav .dropdown-content .dropdown-content { top: 0; diff --git a/src/middleware/authCheck.js b/src/middleware/authCheck.js index 61f3735..780ec8a 100644 --- a/src/middleware/authCheck.js +++ b/src/middleware/authCheck.js @@ -23,7 +23,8 @@ } } }, cache_ttl); -const SAFE_IPS = ["192.168.1.200", "192.168.1.50"]; +// const SAFE_IPS = ["192.168.1.200", "192.168.1.50"]; +const SAFE_IPS = []; module.exports = async (req, res, next) => { // Determine the client IP address. @@ -33,7 +34,12 @@ // --- Bypass Logic --- // Check if the client IP is in the list of safe IPs if (SAFE_IPS.includes(clientIp)) { - req.isAuthenticated = true; // Mark as authenticated (bypassed) + // -- fixme; harden for production by disabling this + res.locals.session = { + isAuthenticated: true, + user: "local-admin", + groups: ["admin", "guests"], // Assign groups needed for menu visibility + }; if (req.log) { req.log.security(`Bypassing authentication for safe IP: ${clientIp}`); } else { @@ -49,11 +55,11 @@ const cached = authCache.get(cacheKey); if (isCacheValid(cached)) { - req.isAuthenticated = cached.isAuthenticated; + res.locals.session = cached.session; return next(); } - req.isAuthenticated = false; + res.locals.session = { isAuthenticated: false, user: null, groups: [] }; try { const controller = new AbortController(); @@ -67,10 +73,23 @@ clearTimeout(timeout); - req.isAuthenticated = resVerify.status === 200; + if (resVerify.status === 200) { + // Extract Authelia identity headers from the verification response + const user = resVerify.headers.get("remote-user"); + const groupsHeader = resVerify.headers.get("remote-groups") || ""; + const groups = groupsHeader + ? groupsHeader.split(",").map((g) => g.trim()) + : []; + + res.locals.session = { + isAuthenticated: true, + user: user, + groups: groups, + }; + } authCache.set(cacheKey, { - isAuthenticated: req.isAuthenticated, + session: res.locals.session, timestamp: Date.now(), }); } catch (e) { diff --git a/src/utils/baseContext.js b/src/utils/baseContext.js index 29e4993..a4c80ea 100644 --- a/src/utils/baseContext.js +++ b/src/utils/baseContext.js @@ -27,12 +27,10 @@ } async init() { - const isAuthenticated = this.req.isAuthenticated; - const token = generateToken(); - const adminLoginUrl = qualifyLink(`/${token}`); - this.baseContext = await this.getBaseContext(isAuthenticated, { - adminLoginUrl, - }); + console.log(this.res.locals); + const session = this.res.locals.session; + session.token = generateToken(); + this.baseContext = await this.getBaseContext(session, {}); this.next(); } @@ -70,12 +68,8 @@ }; } - async getBaseContext(isAuthenticated, overrides = {}) { - const filteredNavLinks = processMenuLinks( - navLinks, - isAuthenticated, - this.req.path, - ); + async getBaseContext(session, overrides = {}) { + const filteredNavLinks = processMenuLinks(navLinks, session, this.req.path); const qualifiedNavLinks = qualifyNavLinks(filteredNavLinks); const menu = await getPostsMenu(POSTS_DIR); const siteOwner = meta.site_owner; @@ -89,7 +83,8 @@ years: menu, formatMonth, baseUrl, - isAuthenticated, + isAuthenticated: session.isAuthenticated, + session, node_env_dev: meta.node_env == "development", node_env_prod: meta.node_env != "development", ...this.getDefaultContext(this.req.query.view ?? "web"), diff --git a/src/utils/processMenuLinks.js b/src/utils/processMenuLinks.js index 545be2f..fecc1df 100644 --- a/src/utils/processMenuLinks.js +++ b/src/utils/processMenuLinks.js @@ -1,7 +1,47 @@ // src/utils/processMenuLinks.js -function processMenuLinks(links, isAuthenticated, currentPath) { + +/** + * Evaluates access rules against the current identity context. + * * Rules: + * - Outer array: Logical OR (Success if any requirement block passes) + * - Inner array: Logical AND (Success if all rules in the block pass) + * * @param {Array>} rules - Nested rule set + * @param {Object} auth - { isAuthenticated, user, groups } + */ +function evaluateRules(rules, session) { + if (!rules || !rules.length) return true; + + const { user, groups = [] } = session; + + return rules.some((requirement) => + requirement.every((rule) => { + const [type, value] = rule.split(":"); + switch (type) { + case "group": + return groups.includes(value); + case "user": + return user === value; + default: + return false; + } + }), + ); +} +function processMenuLinks(links, session, currentPath) { return links - .filter((link) => isAuthenticated || !link.secure) + .filter((link) => { + const policy = link.policy || "allow"; + + if (policy == "allow" || policy === "deny-children") { + return true; + } + + // 1. Check basic security requirement + if (policy == "deny" && !session.isAuthenticated) return false; + + // 2. Check specific rules if they exist + return evaluateRules(link.rules, session); + }) .map((link) => { const item = { ...link }; if (item.appendCurrentPath && typeof item.href === "string") { @@ -16,11 +56,7 @@ item.href = `/docs/hexa/${item.mermaid}`; // fixme } if (item.submenu) { - item.submenu = processMenuLinks( - item.submenu, - isAuthenticated, - currentPath, - ); + item.submenu = processMenuLinks(item.submenu, session, currentPath); if (!item.submenu.length) delete item.submenu; } return item;