diff --git a/public/css/credentialManager.css b/public/css/credentialManager.css index d4fd1a7..e893aa0 100644 --- a/public/css/credentialManager.css +++ b/public/css/credentialManager.css @@ -62,7 +62,7 @@ background-color: var(--accent-primary); } -/* Manual Input Group */ +/* Token Input Group */ .input-group { display: flex; gap: 0.5rem; @@ -154,3 +154,58 @@ .hidden { display: none !important; } + +.input-group-vertical { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1.5rem; +} + +.alt-actions { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border-light); +} + +.btn-primary { + padding: 12px; + background-color: var(--action-primary); + color: var(--text-white); + border-radius: 4px; + font-weight: 700; +} + +.btn-outline { + padding: 10px; + background: transparent; + border: 1px solid var(--border-medium); + color: var(--text-muted-dark); + font-size: 0.85rem; + letter-spacing: 0.5px; +} + +.btn-outline:hover { + background-color: var(--bg-subtle); + border-color: var(--border-grey-dark); +} + +.back-link-btn { + background: none; + border: none; + color: var(--link-classic); + font-size: 0.9rem; + padding: 0; + text-decoration: underline; + margin-top: 1rem; +} + +.enforcement-zone { + margin-top: 1.5rem; + text-align: left; + background: var(--bg-subtle); + padding: 1.25rem; + border-radius: 4px; +} diff --git a/public/js/credentialManager.js b/public/js/credentialManager.js index 227ce8a..7d1a62c 100644 --- a/public/js/credentialManager.js +++ b/public/js/credentialManager.js @@ -5,28 +5,92 @@ class CredentialManager { constructor() { this.container = document.getElementById("manager-container"); - this.token = this.container ? this.container.dataset.token : null; + this.token = this.container?.dataset.token || null; + + // Capture redirect target from URL or default to root + const urlParams = new URLSearchParams(window.location.search); + this.hasRedirect = urlParams.has("rd"); + this.redirectUri = urlParams.get("rd") || "/"; } /** * Initializes event listeners for the reveal action. */ - init() { - const self = this; - const revealBtn = document.getElementById("reveal-btn"); + async init() { + // 1. Determine Identity State before binding listeners + await this.checkSession(); - if (revealBtn) { - revealBtn.addEventListener("click", () => self.handleReveal()); + const showTokenBtn = document.getElementById("show-token-entry-btn"); + const backBtns = document.querySelectorAll(".back-link-btn"); + const submitTokenBtn = document.getElementById("submit-token-btn"); + const saveCheck = document.getElementById("save-check"); + const returnLoginBtn = document.getElementById("return-to-login-btn"); + const authForm = document.getElementById("auth-form"); + const logoutBtn = document.getElementById("logout-btn"); + + if (showTokenBtn) { + showTokenBtn.addEventListener("click", () => + this._switchState("login-section", "token-entry-section"), + ); } + + backBtns.forEach((btn) => { + btn.addEventListener("click", () => + this._switchState("token-entry-section", "login-section"), + ); + }); + + if (submitTokenBtn) { + submitTokenBtn.addEventListener("click", () => { + const input = document.getElementById("token-input-field"); + this.token = input?.value.trim(); + if (this.token) this.handleReveal(); + }); + } + + if (this.token && this.token !== "") { + this._switchState("login-section", "token-entry-section"); + this.handleReveal(); + } + + if (saveCheck) { + saveCheck.addEventListener("change", (e) => { + if (returnLoginBtn) { + returnLoginBtn.disabled = !e.target.checked; + returnLoginBtn.classList.toggle("btn-disabled", !e.target.checked); + } + }); + } + + if (returnLoginBtn) { + returnLoginBtn.addEventListener("click", () => { + this._switchState("credential-section", "login-section"); + }); + } + + if (authForm) { + authForm.addEventListener("submit", (e) => this.handleLogin(e)); + } + + if (logoutBtn) { + logoutBtn.addEventListener("click", () => this.handleLogout()); + } + } + + _switchState(fromId, toId) { + const fromEl = document.getElementById(fromId); + const toEl = document.getElementById(toId); + if (fromEl) fromEl.classList.add("hidden"); + if (toEl) toEl.classList.remove("hidden"); } submitToken() { const submitBtn = document.getElementById("submit-token-btn"); - const manualInput = document.getElementById("token-input"); + const tokenInput = document.getElementById("token-input"); if (manualBtn) { manualBtn.addEventListener("click", () => { - const tokenValue = manualInput.value.trim(); + const tokenValue = tokenInput.value.trim(); if (tokenValue) { // Update container attribute for use by existing logic container.setAttribute("data-token", tokenValue); @@ -42,25 +106,19 @@ * Orchestrates the reveal process via POST request. */ async handleReveal() { - const self = this; - const btn = document.getElementById("reveal-btn"); - - self._toggleLoading(btn, true); + const btn = document.getElementById("submit-token-btn"); + this._toggleLoading(btn, true); try { const response = await fetch( - `https://access.jasonpoage.com/access/${self.token}`, - { - method: "GET", - headers: { "Content-Type": "application/json" }, - }, + `https://access.jasonpoage.com/access/${this.token}`, ); - const data = await self._processResponse(response); - self._displayCredentials(data); + const data = await this._processResponse(response); + this._displayCredentials(data); } catch (err) { - self._handleRevealError(err.message); + this._handleRevealError(err.message); } finally { - self._toggleLoading(btn, false); + this._toggleLoading(btn, false); } } @@ -68,7 +126,6 @@ * Validates response status and parses JSON payload. */ async _processResponse(response) { - const self = this; if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || `Error: ${response.status}`); @@ -80,18 +137,13 @@ * Updates DOM elements with credential data and transitions visibility. */ _displayCredentials(data) { - const self = this; - const revealSection = document.getElementById("reveal-section"); - const credSection = document.getElementById("credential-section"); + document.getElementById("token-entry-section").classList.add("hidden"); + document.getElementById("credential-section").classList.remove("hidden"); - document.getElementById("reveal-message").innerText = data.message; document.getElementById("username").innerText = data.username; document.getElementById("password").innerText = data.password; - revealSection.classList.add("hidden"); - credSection.classList.remove("hidden"); - - self._initCopyButtons(); + this._initCopyButtons(); } /** @@ -126,7 +178,112 @@ const messageEl = document.getElementById("reveal-section"); messageEl.innerHTML = `
${msg}
`; } + async handleLogin(e) { + e.preventDefault(); + const btn = document.getElementById("login-submit-btn"); + const errorEl = document.getElementById("login-error"); + + const username = document.getElementById("login-username").value; + const password = document.getElementById("login-password").value; + const keepMeLoggedIn = + document.getElementById("login-remember")?.checked || false; + + btn.disabled = true; + btn.innerText = "AUTHENTICATING..."; + errorEl?.classList.add("hidden"); + + try { + const response = await fetch( + "https://auth.jasonpoage.com/api/firstfactor", + { + method: "POST", + mode: "cors", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: username, + password: password, + keepMeLoggedIn: keepMeLoggedIn, + }), + credentials: "include", // Required for session cookie storage + }, + ); + + if (response.ok) { + window.location.href = this.redirectUri; + } else { + const data = await response.json(); + throw new Error(data.message || "Invalid credentials."); + } + } catch (err) { + if (errorEl) { + errorEl.innerText = err.message; + errorEl.classList.remove("hidden"); + } + btn.disabled = false; + btn.innerText = "SIGN IN"; + } + } + /** + * Checks if a session cookie is already active via Authelia. + */ + async checkSession() { + try { + // Authelia's internal identity endpoint + const response = await fetch( + "https://auth.jasonpoage.com/api/user/info", + { + method: "GET", + credentials: "include", + }, + ); + + if (response.ok) { + const user = await response.json(); + + if (this.hasRedirect && this.redirectUri !== "/") { + // Rule: Logged in + Redirect set -> GO + window.location.href = this.redirectUri; + } else { + this._displayLogoutState(user.data.display_name); + } + } + } catch (err) { + console.log("No active session detected.", err.stack); + } + } + + _displayLogoutState(username) { + document.getElementById("session-username").innerText = username; + this._switchState("login-section", "logout-section"); + + // Provide a way back if they ended up here by mistake + const continueBtn = document.getElementById("continue-btn"); + if (this.hasRedirect && continueBtn) { + continueBtn.classList.remove("hidden"); + continueBtn.onclick = () => (window.location.href = this.redirectUri); + } + } + + async handleLogout() { + const btn = document.getElementById("logout-btn"); + btn.disabled = true; + btn.innerText = "SIGNING OUT..."; + + try { + await fetch("https://auth.jasonpoage.com/api/logout", { + method: "POST", + credentials: "include", + }); + // Refresh to reset the UI to the clean login state + window.location.reload(); + } catch (err) { + btn.disabled = false; + btn.innerText = "SIGN OUT"; + console.error("Logout failed:", err); + } + } } -const manager = new CredentialManager(); -manager.init(); +document.addEventListener("DOMContentLoaded", () => { + new CredentialManager().init(); +}); diff --git a/src/config/securityConfig.js b/src/config/securityConfig.js index c6f3360..9f74a9f 100644 --- a/src/config/securityConfig.js +++ b/src/config/securityConfig.js @@ -33,5 +33,6 @@ frameSrc: ["'self'", "https://newassets.hcaptcha.com"], objectSrc: ["'none'"], upgradeInsecureRequests: [], + connectSrc: ["'self'", "https://auth.jasonpoage.com"], }, }; diff --git a/src/controllers/accessController.js b/src/controllers/accessController.js index 912e552..241e03a 100644 --- a/src/controllers/accessController.js +++ b/src/controllers/accessController.js @@ -7,7 +7,7 @@ try { res.renderWithBaseContext("pages/credentials.handlebars", { title: "Portfolio Access", - token, + token: token ?? "", }); } catch (err) { next(err); diff --git a/src/routes/guestAccessRouter.js b/src/routes/guestAccessRouter.js index 5b739fe..e189886 100644 --- a/src/routes/guestAccessRouter.js +++ b/src/routes/guestAccessRouter.js @@ -17,5 +17,6 @@ ); router.get("/guest-access/:token", logEvent("admin"), handleAccessConsumption); +router.get("/guest-access", logEvent("admin"), handleAccessConsumption); module.exports = router; diff --git a/src/views/pages/credentials.handlebars b/src/views/pages/credentials.handlebars index 6200448..1f49af3 100644 --- a/src/views/pages/credentials.handlebars +++ b/src/views/pages/credentials.handlebars @@ -7,41 +7,53 @@ {{/section}}Click the button below to generate your one-time credentials.
- -No access token detected. Please enter your token manually to continue.
-Authenticate to access protected services on jasonpoage.com.
+ + + +