Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds config file and AuthTokenStorage #49

Closed
wants to merge 10 commits into from
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@
"vue-tsc": "^1.8.26"
},
"packageManager": "[email protected]"
}
}
1 change: 0 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
sanctum: {
baseUrl: 'http://localhost:80',
userStateKey: 'sanctum.user.identity',
},
},
Expand Down
3 changes: 3 additions & 0 deletions playground/sanctum.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
baseUrl: 'http://localhost:80',
};
2 changes: 1 addition & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { SanctumModuleOptions } from './types';

declare module 'nuxt/schema' {
interface PublicRuntimeConfig {
sanctum: Partial<SanctumModuleOptions>;
sanctum: SanctumModuleOptions;
fenilli marked this conversation as resolved.
Show resolved Hide resolved
}
}
36 changes: 26 additions & 10 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { existsSync } from 'node:fs';
import type { SanctumModuleOptions } from './types';
import {
defineNuxtModule,
addPlugin,
createResolver,
addImportsDir,
addRouteMiddleware,
addPluginTemplate,
} from '@nuxt/kit';
import { defu } from 'defu';
import type { SanctumModuleOptions } from './types';
import defu from 'defu';

export default defineNuxtModule<Partial<SanctumModuleOptions>>({
meta: {
Expand Down Expand Up @@ -45,13 +47,29 @@ export default defineNuxtModule<Partial<SanctumModuleOptions>>({
setup(options, nuxt) {
const resolver = createResolver(import.meta.url);

const publicConfig = nuxt.options.runtimeConfig.public;
const userModuleConfig = publicConfig.sanctum;
addPlugin(resolver.resolve('./runtime/plugin'));

addPluginTemplate({
manchenkoff marked this conversation as resolved.
Show resolved Hide resolved
filename: 'sanctum-plugin.mjs',
async getContents() {
const configPath = await resolver.resolvePath(
resolver.resolve(nuxt.options.rootDir, 'sanctum.config')
);
const configPathExists = existsSync(configPath);

nuxt.options.runtimeConfig.public.sanctum = defu(
userModuleConfig as any,
options
);
return `
import { defineNuxtPlugin } from '#imports';
${configPathExists ? "import defu from 'defu';" : ''}
${configPathExists ? `import sanctumConfig from '${configPath}';` : ''}

export default defineNuxtPlugin((nuxtApp) => {
const defaultConfig = ${JSON.stringify(defu(nuxt.options.runtimeConfig.public.sanctum, options))};
const config = ${configPathExists ? `defu(typeof sanctumConfig === 'function' ? sanctumConfig() : sanctumConfig, defaultConfig)` : `defaultConfig`};
nuxtApp.provide('sanctumConfig', config);
});
`;
},
});

addImportsDir(resolver.resolve('./runtime/composables'));

Expand All @@ -63,7 +81,5 @@ export default defineNuxtModule<Partial<SanctumModuleOptions>>({
name: 'sanctum:guest',
path: resolver.resolve('./runtime/middleware/sanctum.guest'),
});

addPlugin(resolver.resolve('./runtime/plugin'));
},
});
33 changes: 19 additions & 14 deletions src/runtime/composables/useSanctumAuth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { type Ref, computed } from 'vue';
import { useSanctumClient } from './useSanctumClient';
import { useSanctumUser } from './useSanctumUser';
import { navigateTo, useNuxtApp, useRoute, useRuntimeConfig } from '#app';
import type { SanctumModuleOptions } from '../../types';
import { useSanctumConfig } from './useSanctumConfig';
import { navigateTo, useNuxtApp, useRoute } from '#app';

export interface SanctumAuth<T> {
user: Ref<T | null>;
Expand All @@ -22,14 +22,14 @@ export const useSanctumAuth = <T>(): SanctumAuth<T> => {

const user = useSanctumUser<T>();
const client = useSanctumClient();
const options = useRuntimeConfig().public.sanctum as SanctumModuleOptions;
const config = useSanctumConfig();

const isAuthenticated = computed(() => {
return user.value !== null;
});

async function refreshIdentity() {
user.value = await client<T>(options.endpoints.user);
user.value = await client<T>(config.endpoints.user);
}

/**
Expand All @@ -39,27 +39,30 @@ export const useSanctumAuth = <T>(): SanctumAuth<T> => {
*/
async function login(credentials: Record<string, any>) {
if (isAuthenticated.value === true) {
if (options.redirectIfAuthenticated === false) {
if (config.redirectIfAuthenticated === false) {
throw new Error('User is already authenticated');
}

if (options.redirect.onLogin === false) {
if (config.redirect.onLogin === false) {
return;
}

const redirect = options.redirect.onLogin as string;
const redirect = config.redirect.onLogin as string;

await nuxtApp.runWithContext(() => navigateTo(redirect));
}

await client(options.endpoints.login, {
const endpointResult = await client(config.endpoints.login, {
method: 'post',
body: credentials,
});

if (config.authTokenStorage)
config.authTokenStorage.add(endpointResult);

await refreshIdentity();

if (options.redirect.keepRequestedRoute) {
if (config.redirect.keepRequestedRoute) {
const route = useRoute();
const requestedRoute = route.query.redirect as string | undefined;
if (requestedRoute) {
Expand All @@ -68,8 +71,8 @@ export const useSanctumAuth = <T>(): SanctumAuth<T> => {
}
}

if (options.redirect.onLogin) {
const redirect = options.redirect.onLogin as string;
if (config.redirect.onLogin) {
const redirect = config.redirect.onLogin as string;
await nuxtApp.runWithContext(() => navigateTo(redirect));
}
}
Expand All @@ -82,12 +85,14 @@ export const useSanctumAuth = <T>(): SanctumAuth<T> => {
throw new Error('User is not authenticated');
}

await client(options.endpoints.logout, { method: 'post' });
await client(config.endpoints.logout, { method: 'post' });

if (config.authTokenStorage) config.authTokenStorage.delete();

user.value = null;

if (options.redirect.onLogout) {
const redirect = options.redirect.onLogout as string;
if (config.redirect.onLogout) {
const redirect = config.redirect.onLogout as string;

await nuxtApp.runWithContext(() => navigateTo(redirect));
}
Expand Down
8 changes: 8 additions & 0 deletions src/runtime/composables/useSanctumConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { SanctumConfigOptions } from '../../types';
import { useNuxtApp } from '#app';

export const useSanctumConfig = () => {
const { $sanctumConfig } = useNuxtApp();

return $sanctumConfig as Readonly<Required<SanctumConfigOptions>>;
};
8 changes: 4 additions & 4 deletions src/runtime/composables/useSanctumUser.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useState, useRuntimeConfig } from '#app';
import { useState } from '#app';
import { useSanctumConfig } from './useSanctumConfig';
import { type Ref } from 'vue';
import type { SanctumModuleOptions } from '../../types';

/**
* Returns a current authenticated user information.
* @returns Reference to the user state as T.
*/
export const useSanctumUser = <T>(): Ref<T | null> => {
const options = useRuntimeConfig().public.sanctum as SanctumModuleOptions;
const config = useSanctumConfig();

const user = useState<T | null>(options.userStateKey, () => null);
const user = useState<T | null>(config.userStateKey, () => null);

return user;
};
38 changes: 23 additions & 15 deletions src/runtime/httpFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@ import {
useCookie,
useRequestEvent,
useRequestHeaders,
useRuntimeConfig,
navigateTo,
useNuxtApp,
} from '#app';
import type { SanctumModuleOptions } from '../types';
import { useSanctumUser } from './composables/useSanctumUser';
import { useSanctumConfig } from './composables/useSanctumConfig';
import { useRequestURL } from 'nuxt/app';

export const SECURE_METHODS = new Set(['post', 'delete', 'put', 'patch']);

export function createHttpClient(): $Fetch {
const options = useRuntimeConfig().public.sanctum as SanctumModuleOptions;
const config = useSanctumConfig();
const event = useRequestEvent();
const user = useSanctumUser();
const nuxtApp = useNuxtApp();
Expand All @@ -27,18 +26,18 @@ export function createHttpClient(): $Fetch {
async function buildClientHeaders(
headers: HeadersInit | undefined
): Promise<HeadersInit> {
await $fetch(options.endpoints.csrf, {
baseURL: options.baseUrl,
await $fetch(config.endpoints.csrf, {
baseURL: config.baseUrl,
credentials: 'include',
});

const csrfToken = useCookie(options.csrf.cookie, {
const csrfToken = useCookie(config.csrf.cookie, {
readonly: true,
}).value;

return {
...headers,
...(csrfToken && { [options.csrf.header]: csrfToken }),
...(csrfToken && { [config.csrf.header]: csrfToken }),
};
}

Expand All @@ -48,26 +47,26 @@ export function createHttpClient(): $Fetch {
* @returns { HeadersInit }
*/
function buildServerHeaders(headers: HeadersInit | undefined): HeadersInit {
const csrfToken = useCookie(options.csrf.cookie, {
const csrfToken = useCookie(config.csrf.cookie, {
readonly: true,
}).value;
const clientCookies = useRequestHeaders(['cookie']);
const origin = options.origin ?? useRequestURL().origin;
const origin = config.origin ?? useRequestURL().origin;

return {
...headers,
// use the origin from the request headers if not set
Referer: origin,
...(clientCookies.cookie && clientCookies),
...(csrfToken && { [options.csrf.header]: csrfToken }),
...(csrfToken && { [config.csrf.header]: csrfToken }),
};
}

const httpOptions: FetchOptions = {
baseURL: options.baseUrl,
baseURL: config.baseUrl,
credentials: 'include',
redirect: 'manual',
retry: options.client.retry,
retry: config.client.retry,

async onRequest({ options }): Promise<void> {
const method = options.method?.toLowerCase() ?? 'get';
Expand All @@ -77,6 +76,15 @@ export function createHttpClient(): $Fetch {
...options.headers,
};

if (config.authTokenStorage) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be a bit overwhelming to have both bearer token and later csrf (in buildServerHeaders) since we know that only one strategy should be applied. By the way, I am unsure that it will work for both CSR/SSR, have you checked with some implementation of the storage?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now I'm curious if it's even possible to use token storage with SSR? 😄 I've never used it that way

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll have to look into that. but I think with a mobile app there is no SSR? I haven’t been able to test this with SSR because of previous issues. But I will research this, I think there are some things like asyncData for sharing data between Serve and Client

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd appreciate that, thanks!

const authToken = await config.authTokenStorage.get();
if (authToken)
options.headers = {
...options.headers,
Authorization: `Bearer ${authToken}`,
};
}

// https://laravel.com/docs/10.x/routing#form-method-spoofing
if (options.body instanceof FormData && method === 'put') {
options.method = 'POST';
Expand Down Expand Up @@ -119,15 +127,15 @@ export function createHttpClient(): $Fetch {
if (response.status === 401) {
// do not redirect when requesting the user endpoint
// this prevents an infinite loop (ERR_TOO_MANY_REDIRECTS)
if (request.toString().endsWith(options.endpoints.user)) {
if (request.toString().endsWith(config.endpoints.user)) {
return;
}

user.value = null;

if (options.redirect.onLogout) {
if (config.redirect.onLogout) {
await nuxtApp.runWithContext(() =>
navigateTo(options.redirect.onLogout as string)
navigateTo(config.redirect.onLogout as string)
);
}
}
Expand Down
15 changes: 5 additions & 10 deletions src/runtime/middleware/sanctum.auth.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
import {
defineNuxtRouteMiddleware,
navigateTo,
useRuntimeConfig,
createError,
} from '#app';
import { defineNuxtRouteMiddleware, navigateTo, createError } from '#app';
import type { RouteLocationRaw } from 'vue-router';
import { useSanctumUser } from '../composables/useSanctumUser';
import type { SanctumModuleOptions } from '../../types';
import { useSanctumConfig } from '../composables/useSanctumConfig';

export default defineNuxtRouteMiddleware((to) => {
const user = useSanctumUser();
const options = useRuntimeConfig().public.sanctum as SanctumModuleOptions;
const config = useSanctumConfig();

const isAuthenticated = user.value !== null;

if (isAuthenticated === true) {
return;
}

const endpoint = options.redirect.onAuthOnly;
const endpoint = config.redirect.onAuthOnly;

if (endpoint === false) {
throw createError({ statusCode: 403 });
}

const redirect: RouteLocationRaw = { path: endpoint };

if (options.redirect.keepRequestedRoute) {
if (config.redirect.keepRequestedRoute) {
redirect.query = { redirect: to.fullPath };
}

Expand Down
13 changes: 4 additions & 9 deletions src/runtime/middleware/sanctum.guest.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import {
defineNuxtRouteMiddleware,
navigateTo,
useRuntimeConfig,
createError,
} from '#app';
import type { SanctumModuleOptions } from '../../types';
import { defineNuxtRouteMiddleware, navigateTo, createError } from '#app';
import { useSanctumUser } from '../composables/useSanctumUser';
import { useSanctumConfig } from '../composables/useSanctumConfig';

export default defineNuxtRouteMiddleware(() => {
const user = useSanctumUser();
const options = useRuntimeConfig().public.sanctum as SanctumModuleOptions;
const config = useSanctumConfig();

const isAuthenticated = user.value !== null;

if (isAuthenticated === false) {
return;
}

const endpoint = options.redirect.onGuestOnly;
const endpoint = config.redirect.onGuestOnly;

if (endpoint === false) {
throw createError({ statusCode: 403 });
Expand Down
Loading
Loading