UNPKG

weightxreps-oauth

Version:

Module to let your javascript app get user's credentials of [weightxreps.net](https://weightxreps.net/). It only focus on obtaining a valid access token, you are then responsible of adding it to your request's headers when connecting to the GraphQL endpoi

346 lines (345 loc) 11.5 kB
var m = Object.defineProperty; var T = (n, t, e) => t in n ? m(n, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : n[t] = e; var i = (n, t, e) => T(n, typeof t != "symbol" ? t + "" : t, e); import { useState as w, useEffect as v } from "react"; class l { constructor(t) { i(this, "listeners", /* @__PURE__ */ new Set()); this._value = t; } get value() { return this._value; } set value(t) { this._value = t, Array.from(this.listeners).forEach((e) => e(t)); } listen(t, e = !1) { return this.listeners.add(t), e && t(this._value), () => { this.listeners.delete(t); }; } get asReadOnly() { return this; } } class S { constructor(t, e) { i(this, "k"); this.store = e, this.k = t + "--"; } getItem(t) { return this.store.getItem(this.k + t); } setItem(t, e) { if (e == null) { this.removeItem(t); return; } this.store.setItem(this.k + t, e); } getObject(t) { let e = this.getItem(t); if (e) try { return JSON.parse(e); } catch (r) { console.error("Failed to recover stored object <" + (this.k + t) + "> from the localstore, got: ", r.message); } } setObject(t, e) { this.setItem(t, JSON.stringify(e)); } removeItem(t) { this.store.removeItem(this.k + t); } } const U = { fetch: window.fetch.bind(window), endpoint: "https://weightxreps.net/api/auth", asPopup: !1, redirectUri: void 0, store: localStorage, scope: "not-set" }, h = class h extends EventTarget { constructor(e, r) { super(); i(this, "options"); i(this, "instanceID", "_" + Math.random().toString(36).substr(2, 9)); i(this, "_pkceCodeVerifier"); i(this, "_token"); i(this, "store"); i(this, "_user", new l(void 0)); i(this, "_onTokenUpdated", new l(void 0)); i(this, "_isLoading", new l(!1)); i(this, "_error", new l(void 0)); this.clientId = e, this.options = Object.assign({ ...U }, r), this.store = new S(e, this.options.store), this.setup(); } get onLogged() { return this._user.asReadOnly; } get onLoading() { return this._isLoading.asReadOnly; } get onError() { return this._error.asReadOnly; } get pkceCodeVerifier() { return this._pkceCodeVerifier ?? this.store.getItem("wxr-pkce-code-verifier"); } set pkceCodeVerifier(e) { this._pkceCodeVerifier = e, this.store.setItem("wxr-pkce-code-verifier", e); } set token(e) { this._token = e, e || (this.pkceCodeVerifier = null), this.store.setObject("wxr-accessToken", e), this._onTokenUpdated.value = e; } get token() { if (this._token) return this._token; let e = this.store.getObject("wxr-accessToken"); return this._token = e, e; } async getRequestHeadersAsync(e = !1) { return await this.getFreshToken(e), this.getRequestHeadersSync(); } getRequestHeadersSync() { return { Authorization: `${this.token.token_type} ${this.token.access_token}` }; } setup() { this._isLoading.listen((e) => { e && (this._error.value = void 0); }), requestAnimationFrame(async () => { try { await this.continueLoginFlow(); } catch (e) { e.message == h.USER_MUST_LOGIN || (this._error.value = e.message); } }); } /** * Check if the querystring has a `code` param which means we landed here with an authorization code provided... * @returns true if we are logged. */ async continueLoginFlow() { try { if (await this.getFreshToken(!1)) return !0; } catch (a) { if (a.message != h.USER_MUST_LOGIN) throw a; } const e = window.location.href, r = new URL(e), o = new URLSearchParams(r.search), s = o.get("code"), d = o.get("error"); if (s) { await this.onAuthorizationCode(Object.fromEntries(o.entries())); const a = window.location.origin + window.location.pathname; return window.history.replaceState({}, document.title, a), !0; } else if (d) return this._error.value = d, !0; return !1; } /** * request an access token using this authorization code. */ async onAuthorizationCode(e) { if ("error" in e) throw new Error(e.error); return await this.getAccessToken(e.code); } /** * Will activate in case the login opens a popup window and the popup will send us a message after login or error. */ onWindowMessage() { return new Promise((e, r) => { const o = (s) => { let d = new URL(this.options.endpoint); if (s.origin === `${d.protocol}//${d.host}`) { const a = s.data.weightxrepsOnOAuthResult; a && (window.removeEventListener("message", o), e(a)); } }; window.addEventListener("message", o); }); } /** * @see https://github.com/node-oauth/node-oauth2-server/blob/6a1cf188d5e19faa7ae7569faed1dd51f191a802/lib/token-types/bearer-token-type.js#L33 */ onGetTokenResponse(e) { if (e && this.token == e) return this.token; if (e.error) throw new Error(e.error_description || e.error); return e.expirationTime = Date.now() + e.expires_in * 1e3, this.token = e, this.token; } /** * Fetchs a token. When a new token is fetched we also get the user info. */ async fetchToken(e = "", r = !1) { if (!r && !this.pkceCodeVerifier) throw new Error("Code verifier was not found (you probably cleared the localStorage?)"); let o = `${e ? e + "&" : ""}client_id=${this.clientId}&`; o += r ? `refresh_token=${this.token.refresh_token}&grant_type=refresh_token` : `grant_type=authorization_code&code_verifier=${this.pkceCodeVerifier}`, r || (this.pkceCodeVerifier = null), this._isLoading.value = !0; try { return await this.options.fetch(this.options.endpoint + "/token", { method: "POST", body: o, headers: { "Content-Type": "application/x-www-form-urlencoded" } }).then((s) => s.json()).then((s) => this.onGetTokenResponse(s)).then((s) => r ? s : this.getUser().then(() => s)); } catch (s) { throw s; } finally { this._isLoading.value = !1; } } /** * Get the token that will allow us to act on behalf of the user! */ async getAccessToken(e) { return await this.fetchToken(`code=${e}`, !1); } async refreshToken(e = !0) { if (!this.token) throw new Error("There's no token to refresh! (weird...)"); try { var r = await this.fetchToken("", !0); } catch (o) { if (o.message.startsWith("Refresh token not found")) { if (e) return await this.redirectUserToLogin(); throw new Error(h.USER_MUST_LOGIN); } else throw o; } } mustBeHTTPS() { if (window.location.protocol !== "https:") throw new Error("Login requires HTTPS. Please access this site over a secure connection."); } /** * Sends user to weigthxreps to login and grant us access so we can get an access token */ async redirectUserToLogin() { const e = `${window.location.protocol}//${window.location.host}${window.location.pathname}`, r = await E(), o = await L(r), s = "S256", d = this.options.asPopup ? e : this.options.redirectUri ?? e; this.pkceCodeVerifier = r; const a = this.options.endpoint + "?grant_type=authorization_code&response_type=code&client_id=" + encodeURIComponent(this.clientId) + "&redirect_uri=" + encodeURIComponent(d) + "&state=" + this.instanceID + "&code_challenge=" + o + "&code_challenge_method=" + s + "&scope=" + this.options.scope; if (this.options.asPopup) { const u = (screen.width - 600) / 2, _ = (screen.height - 600) / 2; this._isLoading.value = !0, window.open(a, "weightxreps_oauth", `width=600,height=600,top=${_},left=${u},resizable=yes,scrollbars=yes`); try { var c = await this.onWindowMessage(); } catch (y) { throw y; } finally { this._isLoading.value = !1; } return await this.onAuthorizationCode(c); } else window.open(a, "_self"), await new Promise(() => { }); } /** * Gets the access token and makes sure it is not expired. * * @param {boolean} loginIfNeeded If true, it will ask user to login if necesary, else, it will throw error if a login is needed... * @returns {string} a fresh access token */ async getFreshToken(e) { if (this.token) return Date.now() >= this.token.expirationTime - 5 * 60 * 1e3 ? (await this.refreshToken(e)).access_token : (this._user.value || await this.getUser(), this.token.access_token); if (e) return (await this.redirectUserToLogin()).access_token; throw new Error(h.USER_MUST_LOGIN); } async getUser() { var d, a; const e = await this.getRequestHeadersSync(); this._isLoading.value = !0; try { var r = await fetch(this.options.endpoint.replace("auth", "graphql"), { method: "POST", headers: { "Content-Type": "application/json", ...e }, body: JSON.stringify({ operationName: "GetSession", query: `query GetSession { getSession { user { id uname email } } } ` }) }); } catch (c) { throw c; } finally { this._isLoading.value = !1; } r.status == 401 && this.logout(); const o = await r.json(); if (o.errors) throw new Error(o.errors.flatMap((c) => c.message).join(` `)); if (o.error) throw new Error(o.error); const s = (a = (d = o.data) == null ? void 0 : d.getSession) == null ? void 0 : a.user; if (!s) throw new Error("Unexpected server rersponse..."); return this._user.value = s, s; } /** * Connect with weightxreps user. */ async login() { this._isLoading.value = !0; try { await this.getFreshToken(!0); } catch (e) { this._error.value = e.message; } finally { this._isLoading.value = !1; } } /** * Logout user from this client. */ logout() { this.token = void 0, this._user.value = void 0; } }; i(h, "USER_CANCELED_AUTH_ERROR", "user_canceled"), i(h, "USER_DECLINED_AUTH_ERROR", "user_declined"), i(h, "USER_MUST_LOGIN", "must_login"), i(h, "dicc", /* @__PURE__ */ new Map()), i(h, "get", (e, r) => { if (h.dicc.has(e)) return h.dicc.get(e); const o = new h(e, r); return h.dicc.set(e, o), o; }); let g = h; const k = (n) => btoa(String.fromCharCode(...new Uint8Array(n))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), E = async () => { const n = new Uint8Array(32); return window.crypto.getRandomValues(n), k(n); }, L = async (n) => { const e = new TextEncoder().encode(n), r = await window.crypto.subtle.digest("SHA-256", e); return k(r); }, b = (n, t) => { const [e, r] = w(void 0), [o, s] = w(), [d, a] = w(), c = g.get(n, t); return v(() => { let p = c.onLogged.listen(r, !0), f = c.onError.listen(s, !0), u = c.onLoading.listen(a, !0); return () => { p(), f(), u(); }; }, []), { login: c.login.bind(c), getAuthHeaders: c.getRequestHeadersAsync.bind(c), user: e, error: o, logout: c.logout.bind(c), loading: d }; }; export { g as OAuthClient, b as useWeightxrepsOAuth };