UNPKG

@e280/authlocal

Version:

User-sovereign login system for everybody

120 lines 4.2 kB
import { sub } from "@e280/stz"; import { signal } from "@benev/slate"; import { defaults } from "./parts/defaults.js"; import { AuthStores } from "./parts/stores.js"; import { Login } from "../trust/exports/app.js"; import { openPopup } from "./parts/open-popup.js"; import { setupInApp } from "../trust/postmessage/setup-in-app.js"; import { nullcatch } from "../common/utils/nullcatch.js"; /** * Authlocal's page-level auth control center. * - there should only be one instance on the page, shared across any authlocal elements. * - provides the `login` state * - handles persistence of the login session into storage * - coordinates and communicates with the Authlocal popup */ export class Auth { static version = 1; static defaults = defaults; /** The url that the login popups should use (defaults to "https://authlocal.org/") */ src; /** * Subscribe to changes in the login state. * - if the login is `null`, it means the user has logged out. * - usage: * auth.on(login => console.log(login)) */ on = sub(); #options; #stores; #ready; #login = signal(null); constructor(options = {}) { this.#options = Auth.defaults(options); this.src = this.#options.src; this.#stores = new AuthStores(this.#options.kv); this.#ready = this.#stores.versionMigration(Auth.version); this.#login.on(login => this.on.pub(login)); this.#options.onStorageChange(() => void this.loadLogin()); } /** Load and update the login state from storage */ async loadLogin() { const login = await this.#getStoredLogin(); return this.#updateLoginSignal(login); } /** Set the login state manually, saving it to storage */ async saveLogin(login) { const login2 = await this.#setStoredLogin(login); return this.#updateLoginSignal(login2); } /** Shortcut for `saveLogin(null)` */ async logout() { return this.saveLogin(null); } /** The current login state, either a `Login` object, or null if logged out */ get login() { const login = this.#login.value; if (login && login.isExpired()) this.#login.value = null; return this.#login.value; } /** * Spawn a login popup, requesting for the user to login. * `src`: * this is the url to open (defaults to "https://authlocal.org/") */ async popup(src = this.src) { const popupWindow = openPopup(src); const popupOrigin = new URL(src, window.location.href).origin; if (!popupWindow) return null; return new Promise((resolve, reject) => { const appWindow = window; const { dispose } = setupInApp(appWindow, popupWindow, popupOrigin, async (session) => { popupWindow.close(); if (!session) return undefined; try { const login = await this.#verify(session); await this.saveLogin(login); dispose(); resolve(login); } catch (err) { dispose(); reject(err); } }); popupWindow.onclose = () => { dispose(); resolve(this.login); }; }); } #updateLoginSignal(login) { const hasChanged = login?.sessionId !== this.#login.value?.sessionId; if (hasChanged) this.#login.value = login; return login; } async #verify(session) { return nullcatch(async () => Login.verify({ session, appOrigins: [window.origin], })); } async #getStoredLogin() { await this.#ready; const session = await this.#stores.session.get(); if (!session) return null; return this.#verify(session); } async #setStoredLogin(login) { await this.#ready; const session = login?.session; await this.#stores.session.set(session); return login; } } //# sourceMappingURL=auth.js.map