From aeb9ecadf80fd50290571071311877b39c80e962 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Tue, 23 Dec 2025 22:14:44 +0100 Subject: Initial implementation --- src/client.js | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 src/client.js (limited to 'src/client.js') diff --git a/src/client.js b/src/client.js new file mode 100644 index 0000000..7c0de4a --- /dev/null +++ b/src/client.js @@ -0,0 +1,203 @@ +const internals = { + kStyleSheet: "/security/style.css", + kClientSettingsURL: "/auth/client-settings.json", + kOpenLockSVG: + '', + kLockSVG: + '', + kKeySVG: + '', + + settings: {}, + + makeLinkWithIcon(href, id, classes, title, icon) { + const link = document.createElement("a"); + link.href = href; + link.id = id; + link.title = title; + link.classList.add(...classes); + link.innerHTML = icon; + + return link; + }, +}; + +const oidcSecurity = { + async setup() { + const hasStyleSheet = [...document.styleSheets].some((styleSheet) => + styleSheet.href.endsWith(internals.kStyleSheet), + ); + + if (!hasStyleSheet) { + console.log("Adding security stylesheet."); + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = internals.kStyleSheet; + link.type = "text/css"; + document.getElementsByTagName("head")[0].appendChild(link); + } + + const response = await fetch(internals.kClientSettingsURL, { + cache: "no-cache", + mode: "same-origin", + }); + + if (response.ok) { + const clientSettings = await response.json(); + window.isOwner = clientSettings.isOwner; + internals.settings = clientSettings; + + let dialogHost = window.location.hostname; + if (internals.settings.wikiHost) { + dialogHost = clientSettings.wikiHost; + } + + internals.settings.cookieDomain = dialogHost; + + if (window.location.port) { + dialogHost = `${dialogHost}:${window.location.port}`; + } + + let dialogProtocol = window.location.protocol; + if (internals.settings.useHttps) { + dialogProtocol = "https:"; + } + const dialogOrigin = `${dialogProtocol}//${dialogHost}`; + internals.settings.dialogOrigin = dialogOrigin; + internals.settings.dialogURL = `${dialogOrigin}${internals.settings.loginURL}`; + oidcSecurity.updateFooter(window.ownerName, window.isAuthenticated); + + // Subscription to the dialog messages. + window.addEventListener("message", async (event) => { + if (event.origin !== internals.settings.dialogOrigin) { + return; + } + if (event.data?.event === "loginDone") { + if ( + internals.settings.claimOnFirstLogin && + !window.isClaimed + ) { + await oidcSecurity.claimWiki(); + } + window.location.reload(); + } + }); + } else { + console.log("Unable to fetch client settings: ", response); + } + }, + + async claimWiki() { + if (!window.isClaimed) { + const response = await fetch(internals.settings.claimURL, { + cache: "no-cache", + mode: "same-origin", + credentials: "same-origin", + }); + if (response.ok) { + const json = await response.json(); + if (wiki.lineup.bestTitle() === "Login Required") { + location.reload(); + } else { + const { owner } = json; + window.isClaimed = true; + window.isOwner = true; + oidcSecurity.updateFooter(owner, true); + } + } else { + console.log("Attempt to claim site failed", response); + } + } + }, + + updateFooter(ownerName, isAuthenticated) { + if (ownerName) { + document.querySelector("footer > #site-owner").innerHTML = + `Site Owned by: ${ownerName}`; + } + + document.querySelector("footer > #security").innerHTML = ""; + if (isAuthenticated) { + let logoutTitle = "Not Owner : Sign-out"; + let logoutIcon = internals.kLockSVG; + let logoutClass = ["footer-item", "notOwner"]; + if (window.isOwner) { + logoutTitle = "Sign-out"; + logoutIcon = internals.kOpenLockSVG; + logoutClass = ["footer-item"]; + } + + const logoutLink = internals.makeLinkWithIcon( + internals.settings.logoutURL, + "logout", + logoutClass, + logoutTitle, + logoutIcon, + ); + document.querySelector("footer > #security").append(logoutLink); + + logoutLink.addEventListener("click", async (event) => { + event.preventDefault(); + const response = await fetch(internals.settings.logoutURL, { + cache: "no-cache", + mode: "same-origin", + credentials: "same-origin", + }); + if (response.ok) { + window.isAuthenticated = false; + window.user = ""; + document.cookie = `state=loggedOut;domain=.${internals.settings.cookieDomain}; path=/; max-age=60; sameSite=Strict;`; + oidcSecurity.updateFooter(ownerName, false); + } else { + console.log("Logout failed: ", response); + } + }); + + if (!window.isClaimed) { + const claimLink = internals.makeLinkWithIcon( + internals.settings.claimURL, + "claim", + ["footer-item", "claim"], + "Claim this Wiki", + internals.kKeySVG, + ); + document.querySelector("footer > #security").append(claimLink); + + claimLink.addEventListener("click", (event) => { + event.preventDefault(); + oidcSecurity.claimWiki(); + }); + } + } else { + let signOnTitle = "Claim this wiki"; + if (window.isClaimed) { + signOnTitle = "Wiki Owner Sign-on"; + } + + const securityLink = internals.makeLinkWithIcon( + internals.settings.dialogURL, + "show-security-dialog", + ["footer-item"], + signOnTitle, + internals.kLockSVG, + ); + document.querySelector("footer > #security").append(securityLink); + + securityLink.addEventListener("click", (event) => { + event.preventDefault(); + + document.cookie = `wikiName=${window.location.host};domain=.${internals.settings.cookieDomain}; path=/; max-age=300; sameSite=Strict;`; + + // This dialog will emit a loginDone event when done. + // We listen to it to refresh. + window.open( + internals.settings.dialogURL, + "oidc", + "popup,height=600,width=375", + ); + }); + } + }, +}; + +window.plugins.security = oidcSecurity; -- cgit