diff options
| author | Ruben Beltran del Rio <jj@r.bdr.sh> | 2025-12-23 22:14:44 +0100 |
|---|---|---|
| committer | Ruben Beltran del Rio <jj@r.bdr.sh> | 2025-12-23 22:17:09 +0100 |
| commit | aeb9ecadf80fd50290571071311877b39c80e962 (patch) | |
| tree | b9a5cbe979fbd4fb14c97b00534b6918df6b0d85 /src/client.js | |
Initial implementation1.0.0
Diffstat (limited to 'src/client.js')
| -rw-r--r-- | src/client.js | 203 |
1 files changed, 203 insertions, 0 deletions
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: + '<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" fill="none"><defs><clipPath id="a" class="frame-clip frame-clip-def"><rect width="128" height="128" rx="0" ry="0"/></clipPath></defs><g class="frame-container-wrapper"><g class="frame-container-blur"><g class="frame-container-shadows" clip-path="url(#a)"><g class="fills"><rect width="128" height="128" class="frame-background" rx="0" ry="0"/></g><g class="frame-children"><rect width="90" height="72" x="19" y="56" class="fills" rx="4" ry="4" style="fill:currentColor"/><path d="M28 39c2-16 1.001-38 37-38s35 21 34.997 56h-12C88 30 89 13 65 13c-24 0-24 21-25 26H28" class="fills" style="fill:currentColor"/></g></g></g></g></svg>', + kLockSVG: + '<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" fill="none"><defs><clipPath id="a" class="frame-clip frame-clip-def"><rect width="128" height="128" rx="0" ry="0"/></clipPath></defs><g class="frame-container-wrapper"><g class="frame-container-blur"><g class="frame-container-shadows" clip-path="url(#a)"><g class="fills"><rect width="128" height="128" class="frame-background" rx="0" ry="0"/></g><g class="frame-children"><rect width="90" height="72" x="19" y="56" class="fills" rx="4" ry="4" style="fill:currentColor"/><path d="M28 57c0-25 1-48 37-48s35 23 35 48H88c0-17 1-36-23-36S40 40 40 57H28" class="fills" style="fill:currentColor"/></g></g></g></g></svg>', + kKeySVG: + '<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" fill="none"><defs><clipPath id="a" class="frame-clip frame-clip-def"><rect width="128" height="128" rx="0" ry="0"/></clipPath></defs><g class="frame-container-wrapper"><g class="frame-container-blur"><g class="frame-container-shadows" clip-path="url(#a)"><g class="fills"><rect width="128" height="128" class="frame-background" rx="0" ry="0"/></g><g class="frame-children"><path d="M23.414 127.414A1.994 1.994 0 0 1 22 128H2c-1.104 0-2-.896-2-2v-20c0-.544.217-1.037.57-1.397l-.07-.07 48.392-48.392c-5.845-14.747-2.806-32.21 9.117-44.132 16-16.001 41.982-16.001 57.982 0 16.001 16 16.001 41.982 0 57.982-11.613 11.614-28.484 14.798-42.98 9.554L73 79.56a3.99 3.99 0 0 0-2.968 1.168l-5.304 5.304a4.022 4.022 0 0 0-.42.495l-5.423 5.422a2.02 2.02 0 0 1-1.385.551H46c-1.104 0-2 .896-2 2v11a4.004 4.004 0 0 1-3.108 3.9H33c-1.104 0-2 .896-2 2v8a1.913 1.913 0 0 1-.532.974l-6.788 6.789M105.14 22.859c-4-4-10.495-4-14.495 0s-4 10.495 0 14.495 10.495 4 14.495 0 4-10.495 0-14.495" class="fills" style="fill:currentColor"/></g></g></g></g></svg>', + + 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: <span id="site-owner">${ownerName}</span>`; + } + + 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; |