@lucasroll62/nuxt3-auth
Version:
An alternative module to @nuxtjs/auth
329 lines (328 loc) • 10.2 kB
JavaScript
import { isSet, getProp, routeMeta, isRelativeURL } from "../../utils";
import { useRouter, useRoute } from "#imports";
import { Storage } from "./storage.mjs";
import { isSamePath } from "ufo";
import requrl from "requrl";
export class Auth {
constructor(ctx, options) {
this.strategies = {};
this.#errorListeners = [];
this.#redirectListeners = [];
this.ctx = ctx;
this.options = options;
const initialState = {
user: null,
loggedIn: false
};
const storage = new Storage(ctx, {
...options,
initialState
});
this.$storage = storage;
this.$state = storage.state;
}
#errorListeners;
#redirectListeners;
getStrategy(throwException = true) {
if (throwException) {
if (!this.$state.strategy) {
throw new Error("No strategy is set!");
}
if (!this.strategies[this.$state.strategy]) {
throw new Error("Strategy not supported: " + this.$state.strategy);
}
}
return this.strategies[this.$state.strategy];
}
get tokenStrategy() {
return this.getStrategy();
}
get refreshStrategy() {
return this.getStrategy();
}
get strategy() {
return this.getStrategy();
}
get user() {
return this.$state.user;
}
// ---------------------------------------------------------------
// Strategy and Scheme
// ---------------------------------------------------------------
get loggedIn() {
return this.$state.loggedIn;
}
get busy() {
return this.$storage.getState("busy");
}
async init() {
if (this.options.resetOnError) {
this.onError((...args) => {
if (typeof this.options.resetOnError !== "function" || this.options.resetOnError(...args)) {
this.reset();
}
});
}
this.$storage.syncUniversal("strategy", this.options.defaultStrategy);
if (!this.getStrategy(false)) {
this.$storage.setUniversal("strategy", this.options.defaultStrategy);
if (!this.getStrategy(false)) {
return Promise.resolve();
}
}
try {
await this.mounted();
} catch (error) {
this.callOnError(error);
} finally {
if (process.client && this.options.watchLoggedIn) {
this.$storage.watchState("loggedIn", (loggedIn) => {
if (this.$state.loggedIn === loggedIn)
return;
if (Object.hasOwn(useRoute().meta, "auth") && !routeMeta(useRoute(), "auth", false)) {
this.redirect(loggedIn ? "home" : "logout");
}
});
}
}
}
registerStrategy(name, strategy) {
this.strategies[name] = strategy;
}
async setStrategy(name) {
if (name === this.$storage.getUniversal("strategy")) {
return Promise.resolve();
}
if (!this.strategies[name]) {
throw new Error(`Strategy ${name} is not defined!`);
}
this.reset();
this.$storage.setUniversal("strategy", name);
return this.mounted();
}
async mounted(...args) {
if (!this.strategy.mounted) {
return this.fetchUserOnce();
}
return Promise.resolve(this.strategy.mounted(...args)).catch(
(error) => {
this.callOnError(error, { method: "mounted" });
return Promise.reject(error);
}
);
}
async loginWith(name, ...args) {
return this.setStrategy(name).then(() => this.login(...args));
}
async login(...args) {
if (!this.strategy.login) {
return Promise.resolve();
}
return this.wrapLogin(this.strategy.login(...args)).catch(
(error) => {
this.callOnError(error, { method: "login" });
return Promise.reject(error);
}
);
}
async fetchUser(...args) {
if (!this.strategy.fetchUser) {
return Promise.resolve();
}
return Promise.resolve(this.strategy.fetchUser(...args)).catch(
(error) => {
this.callOnError(error, { method: "fetchUser" });
return Promise.reject(error);
}
);
}
async logout(...args) {
if (!this.strategy.logout) {
this.reset();
return Promise.resolve();
}
return Promise.resolve(this.strategy.logout(...args)).catch(
(error) => {
this.callOnError(error, { method: "logout" });
return Promise.reject(error);
}
);
}
// ---------------------------------------------------------------
// User helpers
// ---------------------------------------------------------------
async setUserToken(token, refreshToken) {
if (!this.tokenStrategy.setUserToken) {
this.tokenStrategy.token.set(token);
return Promise.resolve();
}
return Promise.resolve(this.tokenStrategy.setUserToken(token, refreshToken)).catch((error) => {
this.callOnError(error, { method: "setUserToken" });
return Promise.reject(error);
});
}
reset(...args) {
if (this.tokenStrategy.token && !this.strategy.reset) {
this.setUser(false);
this.tokenStrategy.token.reset();
this.refreshStrategy.refreshToken.reset();
}
return this.strategy.reset(
...args
);
}
async refreshTokens() {
if (!this.refreshStrategy.refreshController) {
return Promise.resolve();
}
return Promise.resolve(this.refreshStrategy.refreshController.handleRefresh()).catch((error) => {
this.callOnError(error, { method: "refreshTokens" });
return Promise.reject(error);
});
}
check(...args) {
if (!this.strategy.check) {
return { valid: true };
}
return this.strategy.check(...args);
}
async fetchUserOnce(...args) {
if (!this.$state.user) {
return this.fetchUser(...args);
}
return Promise.resolve();
}
// ---------------------------------------------------------------
// Utils
// ---------------------------------------------------------------
setUser(user) {
this.$storage.setState("user", user);
let check = { valid: Boolean(user) };
if (check.valid) {
check = this.check();
}
this.$storage.setState("loggedIn", check.valid);
}
async request(endpoint, defaults = {}) {
const request = typeof defaults === "object" ? Object.assign({}, defaults, endpoint) : endpoint;
if (request.baseURL === "") {
request.baseURL = requrl(process.server ? this.ctx.ssrContext?.event.req : void 0);
}
if (!this.ctx.$http) {
return Promise.reject(new Error("[AUTH] add the @nuxtjs-alt/http module to nuxt.config file"));
}
return this.ctx.$http.request(request).catch((error) => {
this.callOnError(error, { method: "request" });
return Promise.reject(error);
});
}
async requestWith(endpoint, defaults) {
const request = Object.assign({}, defaults, endpoint);
if (this.tokenStrategy.token) {
const token = this.tokenStrategy.token.get();
const tokenName = this.tokenStrategy.options.token.name || "Authorization";
if (!request.headers) {
request.headers = {};
}
if (!request.headers[tokenName] && isSet(token) && token && typeof token === "string") {
request.headers[tokenName] = token;
}
}
return this.request(request);
}
async wrapLogin(promise) {
this.$storage.setState("busy", true);
this.error = void 0;
return Promise.resolve(promise).then((response) => {
this.$storage.setState("busy", false);
return response;
}).catch((error) => {
this.$storage.setState("busy", false);
return Promise.reject(error);
});
}
onError(listener) {
this.#errorListeners.push(listener);
}
callOnError(error, payload = {}) {
this.error = error;
for (const fn of this.#errorListeners) {
fn(error, payload);
}
}
/**
*
* @param name redirect name
* @param route (default: false) Internal useRoute() (false) or manually specify
* @param router (default: true) Whether to use nuxt redirect (true) or window redirect (false)
*
* @returns
*/
redirect(name, route = false, router = true) {
const activeRouter = useRouter();
const activeRoute = useRoute();
if (!this.options.redirect) {
return;
}
const nuxtRoute = this.options.fullPathRedirect ? activeRoute.fullPath : activeRoute.path;
const from = route ? this.options.fullPathRedirect ? route.fullPath : route.path : nuxtRoute;
let to = this.options.redirect[name];
if (!to) {
return;
}
const queryReturnTo = activeRoute.query.to;
if (this.options.rewriteRedirects) {
if (["logout", "login"].includes(name) && isRelativeURL(from) && !isSamePath(to, from)) {
if (this.options.redirectStrategy === "query") {
to = to + "?to=" + encodeURIComponent(queryReturnTo ? queryReturnTo : from);
}
if (this.options.redirectStrategy === "storage") {
this.$storage.setUniversal("redirect", from);
}
}
if (name === "home") {
let redirect = activeRoute.query.to ? decodeURIComponent(activeRoute.query.to) : void 0;
if (this.options.redirectStrategy === "storage") {
redirect = this.$storage.getUniversal("redirect");
this.$storage.setUniversal("redirect", null);
}
if (redirect) {
to = redirect;
}
}
}
to = this.callOnRedirect(to, from) || to;
if (isSamePath(to, from)) {
return;
}
const query = activeRoute.query;
const queryString = Object.keys(query).map((key) => key + "=" + query[key]).join("&");
if (this.options.redirectStrategy === "storage") {
to += queryString ? "?" + queryString : "";
}
if (!router || !isRelativeURL(to)) {
window.location.replace(to);
} else {
activeRouter.push(to);
}
}
onRedirect(listener) {
this.#redirectListeners.push(listener);
}
callOnRedirect(to, from) {
for (const fn of this.#redirectListeners) {
to = fn(to, from) || to;
}
return to;
}
hasScope(scope) {
const userScopes = this.$state.user && getProp(this.$state.user, this.options.scopeKey);
if (!userScopes) {
return false;
}
if (Array.isArray(userScopes)) {
return userScopes.includes(scope);
}
return Boolean(getProp(userScopes, scope));
}
}