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
JavaScript
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
};