aboutsummaryrefslogtreecommitdiff
path: root/src/client.js
diff options
context:
space:
mode:
authorRuben Beltran del Rio <jj@r.bdr.sh>2025-12-23 22:14:44 +0100
committerRuben Beltran del Rio <jj@r.bdr.sh>2025-12-23 22:17:09 +0100
commitaeb9ecadf80fd50290571071311877b39c80e962 (patch)
treeb9a5cbe979fbd4fb14c97b00534b6918df6b0d85 /src/client.js
Initial implementation1.0.0
Diffstat (limited to 'src/client.js')
-rw-r--r--src/client.js203
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;