From 8e805fe75102dc2293612008a15cd5f6cdacca8a Mon Sep 17 00:00:00 2001 From: Tera <1527149+Denoder@users.noreply.github.com> Date: Fri, 24 Nov 2023 01:20:05 +0200 Subject: [PATCH] v3.0.0 (#79) * Auth Refactor - Changed storage reliance on pinia: Pinia is no longer required to use the auth module, but can still be used. By default it is disabled and instead nuxt's useState will be used instead. - Moved login watch to plugin: This is still in testing however I found that watching loggedIn works better when it's in a plugin. - Consolidated store/storage options: localStorage, sessionStorage, cookie, and pinia have all been moved under on property called 'stores'. Theves also been shortened in name so localStorage is 'local', cookie is 'cookie', and sessionStorage is 'session'. The strategy cookie, will no longer be present if you're not logged in. The syncUniversal/setUniversal now accept a third object parameter that dictates whether or not you want to exclude a store (the store wont be added in any case if its disabled in your options). In the case for cookies you can either use a boolean or an object which contains the cookie's options. * Cookie Scheme Refactor - The Cookie scheme has been added back under the local scheme. It still retains the same functionality if you're not using the token property. - The Laravel Sanctum provider will use the token method, if you'd like to use the SPA method set token.type to false. - Documentation stackblitz has been update to reflect changes: https://stackblitz.com/edit/github-nufjhw?file=README.md * Improve type support - When configuring the auth options the strategies would have no type hinting. This update aims to fix this. * preparation: v3.0.0 Release --- .gitignore | 60 +++++++++-- .yarnrc.yml | 7 ++ README.md | 96 +++++++++++++----- build.config.ts | 91 ----------------- commands/build.ts | 124 +++++++++++++++++++++++ commands/cli.ts | 28 +++++ commands/prepare.ts | 47 +++++++++ package.json | 27 +++-- playground/app.vue | 6 +- playground/nuxt.config.ts | 15 ++- playground/package.json | 12 ++- playground/pages/index.vue | 12 +++ src/module.ts | 31 ++++-- src/options.ts | 50 +++++---- src/plugin.ts | 8 +- src/resolve.ts | 6 +- src/runtime/core/auth.ts | 67 ++++++------ src/runtime/core/middleware.ts | 35 ++++--- src/runtime/core/storage.ts | 116 +++++++++++---------- src/runtime/inc/request-handler.ts | 6 +- src/runtime/providers/google.ts | 3 +- src/runtime/providers/index.ts | 1 + src/runtime/providers/laravel-sanctum.ts | 3 + src/runtime/schemes/cookie.ts | 90 ++++++---------- src/runtime/schemes/oauth2.ts | 11 +- src/runtime/schemes/openIDConnect.ts | 5 +- src/runtime/watch.plugin.ts | 14 +++ src/types/index.d.ts | 43 +------- src/types/options.d.ts | 44 ++++---- src/types/provider.d.ts | 2 + src/types/scheme.d.ts | 9 +- src/types/store.d.ts | 42 ++++++++ src/types/strategy.d.ts | 17 ++-- src/utils/provider.ts | 4 +- 34 files changed, 696 insertions(+), 436 deletions(-) create mode 100644 .yarnrc.yml delete mode 100644 build.config.ts create mode 100644 commands/build.ts create mode 100644 commands/cli.ts create mode 100644 commands/prepare.ts create mode 100644 playground/pages/index.vue create mode 100644 src/runtime/watch.plugin.ts create mode 100644 src/types/store.d.ts diff --git a/.gitignore b/.gitignore index 44289fe..9477353 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,56 @@ +# Dependencies node_modules -*.iml -.idea + +# Logs *.log* + +# Temp directories +.temp +.tmp +.cache + +# Yarn +**/.yarn/cache +**/.yarn/*state* + +# Generated dirs +dist + +# Nuxt .nuxt -.vscode -.DS_STORE +.output +.data +.vercel_build_output +.build-* +.netlify + +# Env +.env + +# Testing +reports coverage -dist -package-lock.json -temp -tsdoc-metadata.json \ No newline at end of file +*.lcov +.nyc_output + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Intellij idea +*.iml +.idea + +# OSX +.DS_Store +.AppleDouble +.LSOverride +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk \ No newline at end of file diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..4b8bff8 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,7 @@ +compressionLevel: mixed + +enableGlobalCache: false + +nodeLinker: node-modules + +yarnPath: .yarn/releases/yarn-4.0.2.cjs diff --git a/README.md b/README.md index 4b2d084..4c0db75 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ -> Alternative Auth module for [Nuxt](https://nuxt.com) +

Auth

+

Alternative Auth module for Nuxt

+ +

+ + + + + + +

## Info -This module is meant as an alternative to @nuxtjs/auth, except this is for nuxt3 only with no backwards compatibility support. This will only work with pinia, I had originally had it work with vuex, but since that is in maintenece mode, I decided to switch to pinia. If you find any bugs please do tell me, I'm still working on this. +This module is meant as an alternative to @nuxtjs/auth, except this is for nuxt3 only with no backwards compatibility support. ## Setup @@ -14,7 +24,8 @@ yarn add @nuxt-alt/auth @nuxt-alt/http @pinia/nuxt pinia 2. Add `@nuxt-alt/auth` and `@pinia/nuxt` to the `modules` section of `nuxt.config.ts` -**Note:** you dont need to specify `@nuxt-alt/http`, it will automatically be added but if you want to manually add it, make sure it is below the auth module (and above the proxy module if you are using it) +**Note:** you dont need to specify `@nuxt-alt/http`, it will automatically be added but if you want to manually add it, make sure it is below the auth module (and above the proxy module if you are using it). It also doesn't need pinia +it will use nuxt's `useState` by default. ```ts export default defineNuxtConfig({ @@ -54,52 +65,81 @@ Enables/disables the middleware to be used globally. Enables/disables the built-in middleware. -### `pinia.namespace` +### `stores.state.namespace` - Type: `String` - Default: `auth` -Changed from vuex to pinia, this is the namespace to use for the pinia store. +This is the namespace to use for nuxt useState. -### `sessionStorage` +### `stores.pinia.enabled` +- Type: `Boolean` +- Default: `false` + +Enable this option to use the pinia store, bey default this is disabled and nuxt's `useState` is used instead. + +### `stores.pinia.namespace` + +- Type: `String` +- Default: `auth` + +This is the namespace to use for the pinia store. + +### `stores.local.enabled` +- Type: `Boolean` +- Default: `true` -- Type: `String | False` +Enable this option to use the localStorage store. + +### `stores.local.prefix` + +- Type: `String` - Default: `auth.` -Similar to the localstorage option, there is a session storage options available for you to use. +This sets the localStorage prefix. -### `routerStrategy` +### `stores.session.enabled` +- Type: `Boolean` +- Default: `true` -- Type: `router | navigateTo` -- Default: `router` +Enable this option to use the sessionStorage store. -By default it will use `router` (`navigateTo` has an issue; I'm assuming with SSR that I don't have the time to check into at the moment, but I'll eventually want to replace with at some point.) +### `stores.session.prefix` -### `redirectStrategy` +- Type: `String` +- Default: `auth.` -- Type: `query | storage` -- Default: `storage` +Similar to the localstorage option, this is the prefix for session storage. -The type of redirection strategy you want to use, `storage` utilizng localStorage for redirects, `query` utilizing the route query parameters. +### `stores.cookie.enabled` +- Type: `Boolean` +- Default: `true` -## Tokens (Types) +Enable this option to use the cookie storage. -In addition to [Auth Tokens](https://auth.nuxtjs.org/api/tokens); +### `stores.cookie.prefix` -By default the `$auth.strategy` getter uses the `Scheme` type which does not have `token` or `refreshToken` property types. To help with this, a `$auth.refreshStrategy` and a `$auth.tokenStrategy` getter have been added for typing. They all do the same thing, this is just meant for type hinting. +- Type: `String` +- Default: `auth.` -## Cookie-based auth (Update: 2.5.0+) +Similar to the localstorage option, this is the prefix for the cookie storage. -The cookie scheme has been decoupled from the local scheme as it does not utitlize tokens, rather it it uses cookies. +### `stores.cookie.options` -~~There is a new `cookie.server` property, this indicates that the cookie we will be looking for will be set upon login otherwise we will be looking at a client/browser cookie. There has also been 2 user properties one for the client/browser and one for the server. An example config looks like this:~~ +- Type: `Object` +- Default: `{ path: '/' }` -The `cookie.server` param has been removed. This was meant as a workaround to decouple the server and client user request when logging in because the check was being overriden. This should be fixed in 2.5.0. The `user.property` param no longer needs to be separated by server and client so use `user.property` instead of `user.property.server` and `user.property.client`. +The default cookie storage options. -## TypeScript (2.6.0+) +### `redirectStrategy` -The user information can be edited like so for TypeScript: +- Type: `query | storage` +- Default: `storage` + +The type of redirection strategy you want to use, `storage` utilizng localStorage for redirects, `query` utilizing the route query parameters. +## TypeScript (2.6.0+) +The user information can be edited like so for TypeScript: ```ts declare module '@nuxt-alt/auth' { interface UserInfo { @@ -109,6 +149,12 @@ declare module '@nuxt-alt/auth' { } ``` +## Tokens (Types) + +In addition to [Auth Tokens](https://auth.nuxtjs.org/api/tokens); + +By default the `$auth.strategy` getter uses the `Scheme` type which does not have `token` or `refreshToken` property types. To help with this, a `$auth.refreshStrategy` and a `$auth.tokenStrategy` getter have been added for typing. They all do the same thing, this is just meant for type hinting. + ## Oauth2 Oauth2 now has client window authentication thanks to this pull request: https://github.com/nuxt-community/auth-module/pull/1746 diff --git a/build.config.ts b/build.config.ts deleted file mode 100644 index f1b780e..0000000 --- a/build.config.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { NuxtModule } from '@nuxt/schema' -import { existsSync, promises as fsp } from 'node:fs' -import { defineBuildConfig } from 'unbuild' -import { pathToFileURL } from 'url' -import { resolve } from 'path' -import mri from 'mri' - -const args = mri(process.argv.slice(2)) - -export default defineBuildConfig({ - declaration: true, - stub: args.stub, - entries: [ - 'src/module', - // @ts-ignore - { input: 'src/types/', outDir: 'dist/types', ext: 'd.ts' }, - { input: 'src/runtime/', outDir: 'dist/runtime', ext: 'mjs' }, - { input: 'src/utils/', outDir: 'dist/utils', ext: 'mjs' }, - ], - rollup: { - emitCJS: false, - cjsBridge: true, - }, - externals: [ - '#app', - '@refactorjs/ofetch', - 'ofetch', - 'vue-router', - '@nuxt/schema', - '@nuxt/schema-edge', - '@nuxt/kit', - '@nuxt/kit-edge', - 'nuxt', - 'nuxt-edge', - 'nuxt3', - 'vue', - 'vue-demi' - ], - hooks: { - - async 'rollup:done'(ctx) { - // Generate CommonJS stup - await writeCJSStub(ctx.options.outDir) - - // Load module meta - const moduleEntryPath = resolve(ctx.options.outDir, 'module.mjs') - const moduleFn: NuxtModule = await import( - pathToFileURL(moduleEntryPath).toString() - ).then(r => r.default || r).catch((err) => { - console.error(err) - console.error('Cannot load module. Please check dist:', moduleEntryPath) - return null - }) - if (!moduleFn) { - return - } - const moduleMeta = await moduleFn.getMeta!() - - // Enhance meta using package.json - if (ctx.pkg) { - if (!moduleMeta.name) { - moduleMeta.name = ctx.pkg.name - } - if (!moduleMeta.version) { - moduleMeta.version = ctx.pkg.version - } - } - - // Write meta - const metaFile = resolve(ctx.options.outDir, 'module.json') - await fsp.writeFile(metaFile, JSON.stringify(moduleMeta, null, 2), 'utf8') - } - } -}); - -async function writeCJSStub(distDir: string) { - const cjsStubFile = resolve(distDir, 'module.cjs') - if (existsSync(cjsStubFile)) { - return - } - - const cjsStub = - `module.exports = function(...args) { - return import('./module.mjs').then(m => m.default.call(this, ...args)) -} - -const _meta = module.exports.meta = require('./module.json') -module.exports.getMeta = () => Promise.resolve(_meta)` - - await fsp.writeFile(cjsStubFile, cjsStub, 'utf8') -} diff --git a/commands/build.ts b/commands/build.ts new file mode 100644 index 0000000..8effdcf --- /dev/null +++ b/commands/build.ts @@ -0,0 +1,124 @@ +import type { NuxtModule } from '@nuxt/schema' +import { existsSync, promises as fsp } from 'node:fs' +import { pathToFileURL } from 'url' +import { resolve } from 'path' +import { defineCommand } from 'citty' + +export default defineCommand({ + meta: { + name: 'build', + description: 'Build module for distribution' + }, + args: { + cwd: { + type: 'string', + description: 'Current working directory' + }, + rootDir: { + type: 'positional', + description: 'Root directory', + required: false + }, + outDir: { + type: 'string' + }, + sourcemap: { + type: 'boolean' + }, + stub: { + type: 'boolean' + } + }, + async run(context) { + const { build } = await import('unbuild') + + const cwd = resolve(context.args.cwd || context.args.rootDir || '.') + + const outDir = context.args.outDir || 'dist' + + await build(cwd, false, { + declaration: true, + sourcemap: context.args.sourcemap, + stub: context.args.stub, + outDir, + entries: [ + 'src/module', + // @ts-ignore + { input: 'src/types/', outDir: `${outDir}/types`, ext: 'd.ts' }, + { input: 'src/runtime/', outDir: `${outDir}/runtime`, ext: 'mjs' }, + { input: 'src/utils/', outDir: `${outDir}/utils`, ext: 'mjs' }, + ], + rollup: { + esbuild: { + target: 'esnext' + }, + emitCJS: false, + cjsBridge: true + }, + externals: [ + '#app', + '@refactorjs/ofetch', + 'ofetch', + 'vue-router', + '@nuxt/schema', + '@nuxt/schema-edge', + '@nuxt/kit', + '@nuxt/kit-edge', + 'nuxt', + 'nuxt-edge', + 'nuxt3', + 'vue', + 'vue-demi' + ], + hooks: { + async 'rollup:done'(ctx) { + // Generate CommonJS stub + await writeCJSStub(ctx.options.outDir) + + // Load module meta + const moduleEntryPath = resolve(ctx.options.outDir, 'module.mjs') + const moduleFn: NuxtModule = await import( + pathToFileURL(moduleEntryPath).toString() + ).then(r => r.default || r).catch((err) => { + console.error(err) + console.error('Cannot load module. Please check dist:', moduleEntryPath) + return null + }) + + if (!moduleFn) { + return + } + const moduleMeta = await moduleFn.getMeta!() + + // Enhance meta using package.json + if (ctx.pkg) { + if (!moduleMeta?.name) { + moduleMeta.name = ctx.pkg.name + } + if (!moduleMeta?.version) { + moduleMeta.version = ctx.pkg.version + } + } + + // Write meta + const metaFile = resolve(ctx.options.outDir, 'module.json') + await fsp.writeFile(metaFile, JSON.stringify(moduleMeta, null, 2), 'utf8') + } + } + }) + } +}) + +async function writeCJSStub (distDir: string) { + const cjsStubFile = resolve(distDir, 'module.cjs') + if (existsSync(cjsStubFile)) { + return + } + const cjsStub = `module.exports = function(...args) { + return import('./module.mjs').then(m => m.default.call(this, ...args)) +} +const _meta = module.exports.meta = require('./module.json') +module.exports.getMeta = () => Promise.resolve(_meta) +` + await fsp.writeFile(cjsStubFile, cjsStub, 'utf8') +} diff --git a/commands/cli.ts b/commands/cli.ts new file mode 100644 index 0000000..91b9da7 --- /dev/null +++ b/commands/cli.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node +import { defineCommand, runMain } from 'citty' +import type { CommandDef } from 'citty' +import { name, description, version } from '../package.json' + +const _rDefault = (r: any) => (r.default || r) as Promise + +const main = defineCommand({ + meta: { + name, + description, + version + }, + subCommands: { + prepare: () => import('./prepare').then(_rDefault), + build: () => import('./build').then(_rDefault) + }, + setup(context) { + // TODO: support 'default command' in citty? + const firstArg = context.rawArgs[0] + if (!(firstArg in context.cmd.subCommands!)) { + console.warn('Please specify the `build` command explicitly.') + context.rawArgs.unshift('build') + } + } +}) + +runMain(main) \ No newline at end of file diff --git a/commands/prepare.ts b/commands/prepare.ts new file mode 100644 index 0000000..47df76b --- /dev/null +++ b/commands/prepare.ts @@ -0,0 +1,47 @@ +import type { NuxtConfig } from '@nuxt/schema' +import { defineCommand } from 'citty' +import { resolve } from 'pathe' + +export default defineCommand({ + meta: { + name: 'prepare', + description: 'Prepare environment by writing types and stubs' + }, + args: { + cwd: { + type: 'string', + description: 'Current working directory' + }, + rootDir: { + type: 'positional', + description: 'Root directory', + required: false + } + }, + async run(context) { + const { runCommand } = await import('nuxi') + + const cwd = resolve(context.args.cwd || context.args.rootDir || '.') + + return runCommand('prepare', [cwd], { + overrides: { + typescript: { + builder: 'shared' + }, + imports: { + autoImport: false + }, + modules: [ + resolve(cwd, './src/module'), + function (_options, nuxt) { + nuxt.hooks.hook('app:templates', (app) => { + for (const template of app.templates) { + template.write = true + } + }) + } + ] + } satisfies NuxtConfig + }) + } +}) \ No newline at end of file diff --git a/package.json b/package.json index fdc691e..749b014 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,24 @@ { "name": "@nuxt-alt/auth", - "version": "2.7.7-beta.0", + "version": "3.0.0", "description": "An alternative module to @nuxtjs/auth", "homepage": "https://github.com/nuxt-alt/auth", "author": "Denoder", "keywords": [ + "auth", "nuxt", + "nuxt3", "nuxtjs", "nuxt-module", "nuxt-plugin", - "nuxt-module-alternatives", - "@nuxtjs/auth" + "@nuxtjs/auth", + "@nuxt-alt/auth" ], "license": "MIT", "type": "module", - "sideEffects": false, "exports": { ".": { + "types": "./dist/types/index.d.ts", "import": "./dist/module.mjs", "require": "./dist/module.cjs" } @@ -30,12 +32,12 @@ "scripts": { "dev": "nuxi dev playground", "dev:build": "nuxi build playground", - "dev:prepare": "unbuild --stub && nuxi prepare playground", - "prepack": "unbuild" + "dev:prepare": "JITI_ESM_RESOLVE=1 jiti ./commands/cli.ts build --stub && JITI_ESM_RESOLVE=1 jiti ./commands/cli.ts prepare", + "prepack": "JITI_ESM_RESOLVE=1 jiti ./commands/cli.ts build" }, "dependencies": { "@nuxt-alt/http": "latest", - "@nuxt/kit": "^3.8.1", + "@nuxt/kit": "^3.8.2", "cookie-es": "^1.0.0", "defu": "^6.1.3", "jwt-decode": "^4.0.0", @@ -45,10 +47,13 @@ "requrl": "^3.0.2" }, "devDependencies": { - "@pinia/nuxt": "^0.5.1", + "@nuxt-alt/proxy": "^2.4.2", + "@nuxt/schema": "^3.8.2", + "@nuxt/ui": "^2.10.0", "@types/node": "^18", - "nuxt": "^3.8.1", - "typescript": "^5.2.2", + "jiti": "^1.21.0", + "nuxt": "^3.8.2", + "typescript": "^5.3.2", "unbuild": "^2.0.0" }, "repository": { @@ -62,5 +67,5 @@ "publishConfig": { "access": "public" }, - "packageManager": "yarn@3.3.1" + "packageManager": "yarn@4.0.2" } diff --git a/playground/app.vue b/playground/app.vue index 93f8486..f637a28 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -1,12 +1,8 @@ diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index c73e93c..fa299f9 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -1,10 +1,17 @@ -import Module from '..' +import AuthModule from '..' export default defineNuxtConfig({ modules: [ - Module, + AuthModule as any, "@nuxt-alt/http", - "@pinia/nuxt", + "@nuxt-alt/proxy", + '@nuxt/ui' ], - auth: {} + auth: { + strategies: { + social: { + provider: 'google', + } + } + } }); diff --git a/playground/package.json b/playground/package.json index ecc2a17..1e91268 100644 --- a/playground/package.json +++ b/playground/package.json @@ -1,4 +1,14 @@ { "private": true, - "name": "@nuxt-alt/auth" + "name": "@nuxt-alt/auth-playgound", + "type": "module", + "scripts": { + "dev": "nuxi dev", + "build": "nuxi build", + "generate": "nuxi generate" + }, + "dependencies": { + "nuxt": "^3.8.2", + "@nuxt-alt/auth": "latest" + } } \ No newline at end of file diff --git a/playground/pages/index.vue b/playground/pages/index.vue new file mode 100644 index 0000000..40e8eca --- /dev/null +++ b/playground/pages/index.vue @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/src/module.ts b/src/module.ts index f48b12f..d694c58 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,5 +1,5 @@ import type { ModuleOptions } from './types'; -import { addImports, addPluginTemplate, addTemplate, createResolver, defineNuxtModule, installModule, addRouteMiddleware } from '@nuxt/kit'; +import { addImports, addPlugin, addPluginTemplate, addTemplate, createResolver, defineNuxtModule, installModule, addRouteMiddleware } from '@nuxt/kit'; import { name, version } from '../package.json'; import { resolveStrategies } from './resolve'; import { moduleDefaults } from './options'; @@ -17,14 +17,24 @@ export default defineNuxtModule({ nuxt: '^3.0.0', }, }, - defaults: moduleDefaults, + defaults: ({ options }) => ({ + ...moduleDefaults, + stores: { + cookie: { + secure: options.dev ? false : true + } + }, + }), async setup(moduleOptions, nuxt) { - // Merge all option sources - const options = defu(nuxt.options.runtimeConfig[CONFIG_KEY] as ModuleOptions, moduleOptions, moduleDefaults) - // Resolver const resolver = createResolver(import.meta.url); + // Runtime + const runtime = resolver.resolve('runtime'); + + // Merge all option sources + const options = defu(nuxt.options.runtimeConfig[CONFIG_KEY] as ModuleOptions, moduleOptions, moduleDefaults) as ModuleOptions + // Resolve strategies const { strategies, strategyScheme } = await resolveStrategies(nuxt, options); delete options.strategies; @@ -48,11 +58,18 @@ export default defineNuxtModule({ installModule('@nuxt-alt/http') } + if (options.watchLoggedIn) { + addPlugin({ + src: resolver.resolve(runtime, 'watch.plugin'), + mode: 'client' + }) + } + // Add auth plugin addPluginTemplate({ getContents: () => getAuthPlugin({ options, strategies, strategyScheme, schemeImports }), filename: 'auth.plugin.mjs', - write: true + write: true, }); addTemplate({ @@ -66,8 +83,6 @@ export default defineNuxtModule({ { from: resolver.resolve('runtime/composables'), name: 'useAuth' }, ]) - // Runtime - const runtime = resolver.resolve('runtime'); nuxt.options.alias['#auth/runtime'] = runtime; // Providers diff --git a/src/options.ts b/src/options.ts index a9f4397..b16dee7 100644 --- a/src/options.ts +++ b/src/options.ts @@ -24,8 +24,6 @@ export const moduleDefaults: ModuleOptions = { redirectStrategy: 'storage', - routerStrategy: 'router', - watchLoggedIn: true, redirect: { @@ -35,31 +33,31 @@ export const moduleDefaults: ModuleOptions = { callback: '/login', }, - // -- Pinia Store -- - - pinia: { - namespace: 'auth', - }, - - // -- Cookie Store -- - - cookie: { - prefix: 'auth.', - options: { - path: '/', + stores: { + state: { + namespace: 'auth' }, - }, - - // -- localStorage Store -- - - localStorage: { - prefix: 'auth.', - }, - - // -- sessionStorage Store -- - - sessionStorage: { - prefix: 'auth.', + pinia: { + enabled: false, + namespace: 'auth', + }, + cookie: { + enabled: true, + prefix: 'auth.', + options: { + path: '/', + sameSite: 'lax', + maxAge: 31536000, + }, + }, + local: { + enabled: false, + prefix: 'auth.', + }, + session: { + enabled: false, + prefix: 'auth.', + }, }, // -- Strategies -- diff --git a/src/plugin.ts b/src/plugin.ts index c57ab60..fd366a2 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -26,19 +26,19 @@ import { defu } from 'defu'; // Active schemes ${options.schemeImports.map((i) => `import { ${i.name}${i.name !== i.as ? ' as ' + i.as : ''} } from '${i.from}'`).join('\n')} +// Options +const options = ${JSON.stringify(options.options, null, 4)} + export default defineNuxtPlugin({ name: 'nuxt-alt:auth', async setup(nuxtApp) { - // Options - const options = ${JSON.stringify(options.options, null, 2)} - // Create a new Auth instance const auth = new Auth(nuxtApp, options) // Register strategies ${options.strategies.map((strategy) => { const scheme = options.strategyScheme[strategy.name!] - const schemeOptions = JSON.stringify(strategy, null, 2) + const schemeOptions = JSON.stringify(strategy) return `auth.registerStrategy('${strategy.name}', new ${scheme.as}(auth, defu(useRuntimeConfig()?.public?.auth?.strategies?.['${strategy.name}'], ${schemeOptions})))` }).join(';\n')} diff --git a/src/resolve.ts b/src/resolve.ts index 16cd27d..041d9ab 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -1,4 +1,4 @@ -import type { Strategy, ModuleOptions } from './types'; +import type { Strategy, ModuleOptions, ProviderNames, SchemeNames } from './types'; import type { Nuxt } from '@nuxt/schema'; import { ProviderAliases } from './runtime/providers'; import * as AUTH_PROVIDERS from './runtime/providers'; @@ -41,7 +41,7 @@ export async function resolveStrategies(nuxt: Nuxt, options: ModuleOptions) { // Default provider (same as name) if (!strategy.provider) { - strategy.provider = strategy.name; + strategy.provider = strategy.name as ProviderNames; } // Try to resolve provider @@ -56,7 +56,7 @@ export async function resolveStrategies(nuxt: Nuxt, options: ModuleOptions) { // Default scheme (same as name) if (!strategy.scheme) { - strategy.scheme = strategy.name; + strategy.scheme = strategy.name as SchemeNames; } try { diff --git a/src/runtime/core/auth.ts b/src/runtime/core/auth.ts index b2bf648..d7b405f 100644 --- a/src/runtime/core/auth.ts +++ b/src/runtime/core/auth.ts @@ -1,7 +1,6 @@ import type { HTTPRequest, HTTPResponse, Scheme, SchemeCheck, TokenableScheme, RefreshableScheme, ModuleOptions, Route, AuthState } from '../../types'; import type { NuxtApp } from '#app'; -import { isSet, getProp, routeMeta, isRelativeURL, hasOwn } from '../../utils'; -import { navigateTo, useRoute, useRouter } from '#imports'; +import { isSet, getProp, isRelativeURL } from '../../utils'; import { Storage } from './storage'; import { isSamePath, withQuery } from 'ufo'; import requrl from 'requrl'; @@ -21,16 +20,29 @@ export class Auth { constructor(ctx: NuxtApp, options: ModuleOptions) { this.ctx = ctx; + + if (typeof this.ctx.$localePath === 'function') { + // @ts-expect-error - package may or may not be installed + this.ctx.hook('i18n:localeSwitched', () => { + this.#transformRedirect(this.options.redirect); + }) + + // Apply to initial options + this.#transformRedirect(options.redirect); + } + this.options = options; // Storage & State const initialState = { user: undefined, - loggedIn: false + loggedIn: false, + strategy: undefined, + busy: false }; const storage = new Storage(ctx, { - ...options, + ...this.options, initialState }); @@ -38,6 +50,15 @@ export class Auth { this.$state = storage.state; } + #transformRedirect (redirects: typeof this.options.redirect) { + for (const key in redirects) { + const value = redirects[key as keyof typeof this.options.redirect]; + if (typeof value === 'string' && typeof this.ctx.$localePath === 'function') { + redirects[key as keyof typeof this.options.redirect] = this.ctx.$localePath(value); + } + } + } + getStrategy(throwException = true): Scheme { if (throwException) { if (!this.$state.strategy) { @@ -90,11 +111,11 @@ export class Auth { } // Restore strategy - this.$storage.syncUniversal('strategy', this.options.defaultStrategy, { cookie: this.loggedIn ? true : false }); + this.$storage.syncUniversal('strategy', this.options.defaultStrategy, { cookie: this.$state.loggedIn }); // Set default strategy if current one is invalid if (!this.getStrategy(false)) { - this.$storage.setUniversal('strategy', this.options.defaultStrategy, { cookie: this.loggedIn ? true : false }); + this.$storage.setUniversal('strategy', this.options.defaultStrategy, { cookie: this.$state.loggedIn }); // Give up if still invalid if (!this.getStrategy(false)) { @@ -109,16 +130,6 @@ export class Auth { catch (error: any) { this.callOnError(error); } - finally { - if (process.client && this.options.watchLoggedIn) { - this.$storage.watchState('loggedIn', (loggedIn: boolean) => { - if (this.$state.loggedIn === loggedIn) return; - if (hasOwn(useRoute().meta, 'auth') && !routeMeta(useRoute(), 'auth', false)) { - this.redirect(loggedIn ? 'home' : 'logout'); - } - }); - } - } } registerStrategy(name: string, strategy: Scheme): void { @@ -138,7 +149,7 @@ export class Auth { this.reset(); // Set new strategy - this.$storage.setUniversal('strategy', name, { cookie: this.loggedIn ? true : false }); + this.$storage.setUniversal('strategy', name, { cookie: this.$state.loggedIn }); // Call mounted hook on active strategy return this.mounted(); @@ -162,7 +173,7 @@ export class Auth { } async login(...args: any[]): Promise | void> { - this.$storage.syncUniversal('strategy', this.strategy.name) + this.$storage.syncUniversal('strategy', this.strategy.name, { cookie: this.$state.loggedIn }); if (!this.strategy.login) { return Promise.resolve(); @@ -279,7 +290,6 @@ export class Auth { } async request(endpoint: HTTPRequest, defaults: HTTPRequest = {}): Promise> { - const request = typeof defaults === 'object' ? Object.assign({}, defaults, endpoint) : endpoint; if (request.baseURL === '') { @@ -327,10 +337,10 @@ export class Auth { this.$storage.setState('busy', false) return response }) - .catch((error) => { - this.$storage.setState('busy', false) - return Promise.reject(error) - }) + .catch((error) => { + this.$storage.setState('busy', false) + return Promise.reject(error) + }) } onError(listener: ErrorListener): void { @@ -364,9 +374,7 @@ export class Auth { return; } - const currentRoute = useRoute(); - const currentRouter = useRouter(); - + const currentRoute = this.ctx.$router.currentRoute.value; const nuxtRoute = this.options.fullPathRedirect ? currentRoute.fullPath : currentRoute.path const from = route ? (this.options.fullPathRedirect ? route.fullPath : route.path) : nuxtRoute; @@ -378,6 +386,7 @@ export class Auth { if (this.options.redirectStrategy === 'query') { to = to + '?to=' + encodeURIComponent((queryReturnTo ? queryReturnTo : from) as string); } + if (this.options.redirectStrategy === 'storage') { this.$storage.setUniversal('redirect', from); } @@ -388,7 +397,7 @@ export class Auth { if (this.options.redirectStrategy === 'storage') { redirect = this.$storage.getUniversal('redirect') as string; - this.$storage.setUniversal('redirect', null); + this.$storage.setUniversal('redirect', null) } if (redirect) { @@ -412,10 +421,10 @@ export class Auth { } if (process.client && (!router || !isRelativeURL(to))) { - window.location.replace(to); + return globalThis.location.replace(to) } else { - return this.ctx.runWithContext(() => this.options.routerStrategy === 'navigateTo' ? navigateTo(to) : currentRouter.push(to)); + return this.ctx.$router.push(typeof this.ctx.$localePath === 'function' ? this.ctx.$localePath(to) : to); } } diff --git a/src/runtime/core/middleware.ts b/src/runtime/core/middleware.ts index 7744f20..c7cd740 100644 --- a/src/runtime/core/middleware.ts +++ b/src/runtime/core/middleware.ts @@ -1,5 +1,5 @@ -import { routeMeta, getMatchedComponents, normalizePath, hasOwn } from '../../utils'; -import { useNuxtApp, defineNuxtRouteMiddleware } from '#imports'; +import { routeMeta, getMatchedComponents, hasOwn, normalizePath } from '../../utils'; +import { useAuth, defineNuxtRouteMiddleware } from '#imports'; export default defineNuxtRouteMiddleware(async (to, from) => { // Disable middleware if options: { auth: false } is set on the route @@ -15,47 +15,50 @@ export default defineNuxtRouteMiddleware(async (to, from) => { return; } - const ctx = useNuxtApp(); + const auth = useAuth(); - const { login, callback } = ctx.$auth.options.redirect; + const { login, callback } = auth.options.redirect; const pageIsInGuestMode = hasOwn(to.meta, 'auth') && routeMeta(to, 'auth', 'guest'); const insidePage = (page: string) => normalizePath(to.path) === normalizePath(page); - if (ctx.$auth.$state.loggedIn) { + if (auth.$state.loggedIn) { // Perform scheme checks. - const { tokenExpired, refreshTokenExpired, isRefreshable } = ctx.$auth.check(true); + const { tokenExpired, refreshTokenExpired, isRefreshable } = auth.check(true); + + // -- Authorized -- + if (!login || insidePage(login) || pageIsInGuestMode) { + return auth.redirect('home', to) + } // Refresh token has expired. There is no way to refresh. Force reset. if (refreshTokenExpired) { - ctx.$auth.reset(); + auth.reset(); + return auth.redirect('login', to); } else if (tokenExpired) { // Token has expired. Check if refresh token is available. if (isRefreshable) { // Refresh token is available. Attempt refresh. try { - await ctx.$auth.refreshTokens(); + await auth.refreshTokens(); } catch (error) { // Reset when refresh was not successfull - ctx.$auth.reset(); + auth.reset(); + return auth.redirect('login', to); } } else { // Refresh token is not available. Force reset. - ctx.$auth.reset(); + auth.reset(); + return auth.redirect('login', to); } } - - // -- Authorized -- - if (!login || insidePage(login) || pageIsInGuestMode) { - return ctx.$auth.redirect('home', to); - } } // -- Guest -- // (Those passing `callback` at runtime need to mark their callback component // with `auth: false` to avoid an unnecessary redirect from callback to login) else if (!pageIsInGuestMode && (!callback || !insidePage(callback))) { - return ctx.$auth.redirect('login', to); + return auth.redirect('login', to); } }); diff --git a/src/runtime/core/storage.ts b/src/runtime/core/storage.ts index 586caf9..3e23b68 100644 --- a/src/runtime/core/storage.ts +++ b/src/runtime/core/storage.ts @@ -2,20 +2,25 @@ import type { ModuleOptions, AuthStoreDefinition, AuthState, StoreMethod, StoreI import type { NuxtApp } from '#app'; import { isUnset, isSet, decodeValue, encodeValue, setH3Cookie } from '../../utils'; import { defineStore, type Pinia } from 'pinia'; -import { parse, serialize } from 'cookie-es'; +import { parse, serialize, type CookieSerializeOptions } from 'cookie-es'; +import { useState } from '#imports'; +import { watch } from 'vue'; export class Storage { ctx: NuxtApp; options: ModuleOptions; - #store!: AuthStoreDefinition; - #initStore!: AuthStoreDefinition; - state: AuthState = {}; - #state: AuthState = {}; + #PiniaStore!: AuthStoreDefinition; + #PiniaInitStore!: AuthStoreDefinition; + #initStore?: Ref; + state: AuthState; + #state: AuthState; #piniaEnabled: boolean = false; constructor(ctx: NuxtApp, options: ModuleOptions) { this.ctx = ctx; this.options = options; + this.state = options.initialState! + this.#state = options.initialState! this.#initState(); } @@ -32,12 +37,18 @@ export class Storage { // Set in all included stores const storeMethods: Record = { - cookie: (k: string, v: V) => this.setCookie(k, v), + cookie: (k: string, v: V, o: CookieSerializeOptions) => this.setCookie(k, v, o), session: (k: string, v: V) => this.setSessionStorage(k, v), local: (k: string, v: V) => this.setLocalStorage(k, v) } - Object.entries(include).filter(([_, shouldInclude]) => shouldInclude).forEach(([method]) => storeMethods[method as StoreMethod]?.(key, value)); + Object.entries(include).filter(([_, shouldInclude]) => shouldInclude).forEach(([method, opts]) => { + if (method === 'cookie' && typeof opts === 'object') { + return storeMethods[method as StoreMethod]?.(key, value, opts) + } + + return storeMethods[method as StoreMethod]?.(key, value) + }); // Local state this.setState(key, value); @@ -73,7 +84,7 @@ export class Storage { } if (isSet(value)) { - this.setUniversal(key, value, include); + this.getCookie(key) ? this.setUniversal(key, value, { ...include, cookie: false }) : this.setUniversal(key, value, include); } return value; @@ -90,17 +101,13 @@ export class Storage { // Local state (reactive) // ------------------------------------ - #initState(): void { - // Private state is suitable to keep information not being exposed to pinia store - // This helps prevent stealing token from SSR response HTML - this.#state = {}; - + async #initState() { // Use pinia for local state's if possible const pinia = this.ctx.$pinia as Pinia - this.#piniaEnabled = this.options.pinia && !!pinia; + this.#piniaEnabled = this.options.stores.pinia!.enabled && !!pinia; if (this.#piniaEnabled) { - this.#store = defineStore(this.options.pinia.namespace, { + this.#PiniaStore = defineStore(this.options.stores.pinia?.namespace!, { state: () => ({ ...this.options.initialState }), actions: { SET(payload: any) { @@ -109,17 +116,19 @@ export class Storage { } }) as unknown as AuthStoreDefinition; - this.#initStore = this.#store(pinia); - this.state = this.#initStore.$state; + this.#PiniaInitStore = this.#PiniaStore(pinia); + this.state = this.#PiniaInitStore.$state; } else { - this.state = {}; + this.#initStore = useState(this.options.stores.state?.namespace, () => ({ + ...this.options.initialState + })) - console.warn('[AUTH] The pinia store is not activated. This might cause issues in auth module behavior, like redirects not working properly. To activate it, please install it and add it to your config after this module'); + this.state = this.#initStore.value } } get store() { - return this.#initStore; + return this.#piniaEnabled ? this.#PiniaInitStore : this.#initStore; } setState(key: string, value: any) { @@ -127,7 +136,7 @@ export class Storage { this.#state[key] = value; } else if (this.#piniaEnabled) { - const { SET } = this.#initStore; + const { SET } = this.#PiniaInitStore; SET({ key, value }); } else { @@ -147,7 +156,7 @@ export class Storage { watchState(watchKey: string, fn: (value: any) => void) { if (this.#piniaEnabled) { - return this.#initStore.$onAction((context) => { + return this.#PiniaInitStore.$onAction((context) => { if (context.name === 'SET') { const { key, value } = context.args[0]; if (watchKey === key) { @@ -155,6 +164,10 @@ export class Storage { } } }); + } else { + watch(() => this.#initStore!.value[watchKey], (modified, old) => { + fn(modified) + }, { deep: true }) } } @@ -174,7 +187,7 @@ export class Storage { if (!this.isLocalStorageEnabled()) return; try { - const prefixedKey = `${this.getLocalStoragePrefix()}${key}`; + const prefixedKey = `${this.options.stores.local?.prefix}${key}`; localStorage.setItem(prefixedKey, encodeValue(value)); } catch (e) { if (!this.options.ignoreExceptions) throw e; @@ -188,7 +201,7 @@ export class Storage { return; } - const prefixedKey = `${this.getLocalStoragePrefix()}${key}`; + const prefixedKey = `${this.options.stores.local?.prefix}${key}`; return decodeValue(localStorage.getItem(prefixedKey)); } @@ -198,22 +211,14 @@ export class Storage { return; } - const prefixedKey = `${this.getLocalStoragePrefix()}${key}`; + const prefixedKey = `${this.options.stores.local?.prefix}${key}`; localStorage.removeItem(prefixedKey); } - getLocalStoragePrefix(): string { - if (!this.options.localStorage) { - throw new Error('Cannot get prefix; localStorage is off'); - } - - return this.options.localStorage.prefix; - } - isLocalStorageEnabled(): boolean { const isNotServer = !process.server; - const isConfigEnabled = !!this.options.localStorage; + const isConfigEnabled = this.options.stores.local?.enabled; const localTest = "test"; if (isNotServer && isConfigEnabled) { @@ -243,7 +248,7 @@ export class Storage { if (!this.isSessionStorageEnabled()) return; try { - const prefixedKey = `${this.getSessionStoragePrefix()}${key}`; + const prefixedKey = `${this.options.stores!.session!.prefix}${key}`; sessionStorage.setItem(prefixedKey, encodeValue(value)); } catch (e) { if (!this.options.ignoreExceptions) throw e; @@ -257,9 +262,9 @@ export class Storage { return } - const $key = this.getSessionStoragePrefix() + key + const prefixedKey = this.options.stores!.session!.prefix + key - const value = sessionStorage.getItem($key) + const value = sessionStorage.getItem(prefixedKey) return decodeValue(value) } @@ -269,22 +274,15 @@ export class Storage { return } - const $key = this.getSessionStoragePrefix() + key - - sessionStorage.removeItem($key) - } - - getSessionStoragePrefix(): string { - if (!this.options.sessionStorage) { - throw new Error('Cannot get prefix; sessionStorage is off'); - } + const prefixedKey = this.options.stores!.session!.prefix + key - return this.options.sessionStorage.prefix; + sessionStorage.removeItem(prefixedKey) } isSessionStorageEnabled(): boolean { const isNotServer = !process.server; - const isConfigEnabled = !!this.options.sessionStorage; + // @ts-ignore + const isConfigEnabled = this.options.stores!.session?.enabled; const testKey = "test"; if (isNotServer && isConfigEnabled) { @@ -303,25 +301,25 @@ export class Storage { } // ------------------------------------ - // Cookies + // Cookie Storage // ------------------------------------ - setCookie(key: string, value: V, options: ModuleOptions['cookie'] = {}) { + setCookie(key: string, value: V, options: CookieSerializeOptions = {}) { if (!this.isCookiesEnabled()) { return; } - const prefix = options.prefix ?? this.options.cookie.prefix; - const $key = `${prefix}${key}`; + const prefix = this.options.stores!.cookie?.prefix; + const prefixedKey = `${prefix}${key}`; const $value = encodeValue(value); - const $options = { ...this.options.cookie.options, ...options }; + const $options = { ...this.options.stores.cookie?.options, ...options }; // Unset null, undefined if (isUnset(value)) { $options.maxAge = -1; } - const cookieString = serialize($key, $value, $options); + const cookieString = serialize(prefixedKey, $value, $options); if (process.client) { document.cookie = cookieString; @@ -345,20 +343,20 @@ export class Storage { return; } - const $key = this.options.cookie.prefix + key; + const prefixedKey = this.options.stores.cookie?.prefix + key; const cookies = this.getCookies(); - return decodeValue(cookies![$key] ? decodeURIComponent(cookies![$key] as string) : undefined) + return decodeValue(cookies![prefixedKey] ? decodeURIComponent(cookies![prefixedKey] as string) : undefined) } - removeCookie(key: string, options?: ModuleOptions['cookie']): void { + removeCookie(key: string, options?: CookieSerializeOptions): void { this.setCookie(key, undefined, options); } isCookiesEnabled(): boolean { const isNotClient = process.server; - const isConfigEnabled = !!this.options.cookie; - + const isConfigEnabled = this.options.stores.cookie?.enabled; + if (isConfigEnabled) { if (isNotClient || window.navigator.cookieEnabled) return true; console.warn('[AUTH] Cookies are enabled in config, but the browser does not support it.'); diff --git a/src/runtime/inc/request-handler.ts b/src/runtime/inc/request-handler.ts index 02bb87b..3acde1b 100644 --- a/src/runtime/inc/request-handler.ts +++ b/src/runtime/inc/request-handler.ts @@ -41,7 +41,7 @@ export class RequestHandler { // Refresh token has expired. There is no way to refresh. Force reset. if (refreshTokenExpired) { - this.scheme.reset!(); + this.scheme.reset?.(); throw new ExpiredAuthSessionError(); } @@ -49,7 +49,7 @@ export class RequestHandler { if (tokenExpired) { // Refresh token is not available. Force reset. if (!isRefreshable) { - this.scheme.reset!(); + this.scheme.reset?.(); throw new ExpiredAuthSessionError(); } @@ -59,7 +59,7 @@ export class RequestHandler { .then(() => true) .catch(() => { // Tokens couldn't be refreshed. Force reset. - this.scheme.reset!(); + this.scheme.reset?.(); throw new ExpiredAuthSessionError(); }); } diff --git a/src/runtime/providers/google.ts b/src/runtime/providers/google.ts index 65077ae..deed8c0 100644 --- a/src/runtime/providers/google.ts +++ b/src/runtime/providers/google.ts @@ -9,8 +9,9 @@ export function google(nuxt: Nuxt, strategy: ProviderPartialOptions = { }, endpoints: { csrf: false, - login: { - url: '/api/auth/login', - method: 'post', - }, - logout: { - url: '/api/auth/logout', - method: 'post', - }, - user: { - url: '/api/auth/user', - method: 'get', - }, + }, + token: { + type: false, + property: '', + maxAge: false, + global: false, + required: false }, user: { property: false, @@ -49,15 +38,11 @@ const DEFAULTS: SchemePartialOptions = { }, }; -export class CookieScheme extends BaseScheme { - requestHandler: RequestHandler; +export class CookieScheme extends LocalScheme implements TokenableScheme { checkStatus: boolean = false; - constructor($auth: Auth, options: SchemePartialOptions, ...defaults: SchemePartialOptions[]) { - super($auth, options as OptionsT, ...(defaults as OptionsT[]), DEFAULTS as OptionsT); - - // Initialize Request Interceptor - this.requestHandler = new RequestHandler(this, this.$auth.ctx.$http); + constructor($auth: Auth, options: SchemePartialOptions) { + super($auth, options as OptionsT, DEFAULTS as OptionsT); } async mounted(): Promise | void> { @@ -65,10 +50,11 @@ export class CookieScheme extends BaseSche this.$auth.ctx.$http.setHeader('referer', this.$auth.ctx.ssrContext!.event.node.req.headers.host!); } - this.checkStatus = true; + if (this.options.token?.type) { + return super.mounted() + } - // Initialize request interceptor - this.initializeRequestInterceptor(); + this.checkStatus = true; return this.$auth.fetchUserOnce(); } @@ -76,6 +62,10 @@ export class CookieScheme extends BaseSche check(): SchemeCheck { const response = { valid: false }; + if (!super.check().valid && this.options.token?.type) { + return response + } + if (!this.checkStatus) { response.valid = true return response @@ -101,6 +91,10 @@ export class CookieScheme extends BaseSche await this.$auth.request(this.options.endpoints.csrf); } + if (this.options.token?.type) { + return super.login(endpoint, { reset: false }) + } + if (!this.options.endpoints.login) { return; } @@ -108,11 +102,6 @@ export class CookieScheme extends BaseSche // Make login request const response = await this.$auth.request(endpoint, this.options.endpoints.login); - // Initialize request interceptor if not initialized - if (!this.requestHandler.interceptor) { - this.initializeRequestInterceptor(); - } - // Fetch user if `autoFetch` is enabled if (this.options.user.autoFetch) { if (this.checkStatus) { @@ -162,32 +151,15 @@ export class CookieScheme extends BaseSche }); } - async logout(endpoint: HTTPRequest = {}): Promise { - // Only connect to logout endpoint if it's configured - if (this.options.endpoints.logout) { - await this.$auth.requestWith(endpoint, this.options.endpoints.logout).catch((err: any) => console.error(err)); - } - - // But reset regardless - this.$auth.redirect('logout'); - return this.$auth.reset(); - } - - reset({ resetInterceptor = true } = {}): void { + reset(): void { if (this.options.cookie.name) { - this.$auth.$storage.setCookie(this.options.cookie.name, null, { - prefix: '', - }); + this.$auth.$storage.setCookie(this.options.cookie.name, null); } - this.$auth.setUser(false); - - if (resetInterceptor) { - this.requestHandler.reset(); + if (this.options.token?.type) { + return super.reset() } - } - initializeRequestInterceptor(): void { - this.requestHandler.initializeRequestInterceptor(); + this.$auth.setUser(false); } } diff --git a/src/runtime/schemes/oauth2.ts b/src/runtime/schemes/oauth2.ts index 5451890..aeafdc3 100644 --- a/src/runtime/schemes/oauth2.ts +++ b/src/runtime/schemes/oauth2.ts @@ -5,7 +5,7 @@ import { getProp, normalizePath, randomString, removeTokenPrefix, parseQuery } f import { RefreshController, RequestHandler, ExpiredAuthSessionError, Token, RefreshToken } from '../inc'; import { joinURL, withQuery } from 'ufo'; import { BaseScheme } from './base'; -import { useRoute, useRuntimeConfig } from '#imports'; +import { useRuntimeConfig } from '#imports'; import requrl from 'requrl'; export interface Oauth2SchemeEndpoints extends EndpointsOption { @@ -18,7 +18,7 @@ export interface Oauth2SchemeEndpoints extends EndpointsOption { export interface Oauth2SchemeOptions extends SchemeOptions, TokenableSchemeOptions, RefreshableSchemeOptions { endpoints: Oauth2SchemeEndpoints; user: UserOptions; - responseMode: 'query.jwt' | 'fragment.jwt' | 'form_post.jwt' | 'jwt'; + responseMode: 'query.jwt' | 'fragment.jwt' | 'form_post.jwt' | 'jwt' | ''; responseType: 'code' | 'token' | 'id_token' | 'none' | string; grantType: 'implicit' | 'authorization_code' | 'client_credentials' | 'password' | 'refresh_token' | 'urn:ietf:params:oauth:grant-type:device_code'; accessType: 'online' | 'offline'; @@ -28,7 +28,7 @@ export interface Oauth2SchemeOptions extends SchemeOptions, TokenableSchemeOptio clientSecretTransport: 'body' | 'aurthorization_header'; scope: string | string[]; state: string; - codeChallengeMethod: 'implicit' | 'S256' | 'plain'; + codeChallengeMethod: 'implicit' | 'S256' | 'plain' | ''; acrValues: string; audience: string; autoLogout: boolean; @@ -295,7 +295,7 @@ export class Oauth2Scheme { - const route = useRoute(); + const route = this.$auth.ctx.$router.currentRoute.value // Handle callback only for specified route if (this.$auth.options.redirect && normalizePath(route.path) !== normalizePath(this.$auth.options.redirect.callback)) { diff --git a/src/runtime/schemes/openIDConnect.ts b/src/runtime/schemes/openIDConnect.ts index 5412dd2..f05a9ab 100644 --- a/src/runtime/schemes/openIDConnect.ts +++ b/src/runtime/schemes/openIDConnect.ts @@ -4,7 +4,6 @@ import { Oauth2Scheme, type Oauth2SchemeEndpoints, type Oauth2SchemeOptions } fr import { normalizePath, getProp, parseQuery } from '../../utils'; import { IdToken, ConfigurationDocument } from '../inc'; import { type IdTokenableSchemeOptions } from '../../types'; -import { useRoute } from '#imports'; import { withQuery, type QueryObject, type QueryValue } from 'ufo'; export interface OpenIDConnectSchemeEndpoints extends Oauth2SchemeEndpoints { @@ -146,7 +145,7 @@ export class OpenIDConnectScheme { + if (hasOwn(useRoute().meta, 'auth') && !routeMeta(useRoute(), 'auth', false)) { + auth.redirect(loggedIn ? 'home' : 'logout'); + } + }) + } +}) \ No newline at end of file diff --git a/src/types/index.d.ts b/src/types/index.d.ts index ed5a2ac..cb95505 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,7 +1,7 @@ import type { ModuleOptions } from './options'; import type { Auth } from '../runtime'; -import type { Store, Pinia, StoreGeneric } from 'pinia'; import * as NuxtSchema from '@nuxt/schema'; +import type { CookieSerializeOptions } from 'cookie-es'; export * from './openIDConnectConfigurationDocument'; export * from './provider'; @@ -11,46 +11,7 @@ export * from './scheme'; export * from './strategy'; export * from './utils'; export * from './options'; - -export type AuthStoreDefinition = Store; - /** - * Sets the key/value pair for the auth module's auth state. - * - * @param payload - object containing the key and value - */ - SET(payload: any): void; -}> - -export type StoreMethod = 'cookie' | 'session' | 'local'; - -export interface StoreIncludeOptions { - cookie?: boolean; - session?: boolean; - local?: boolean; -} - -export interface UserInfo { - [key: string]: unknown; -} - -export type AuthState = { - [key: string]: unknown; - // user object - user?: UserInfo; - // indicates whether the user is logged in - loggedIn?: boolean; - // indicates the strategy of authentication used - strategy?: string; - // indicates if the authentication system is busy performing tasks, may not be defined initially - busy?: boolean; -} +export * from './store' declare module '#app' { interface NuxtApp { diff --git a/src/types/options.d.ts b/src/types/options.d.ts index aa4e8e3..6a1dea8 100644 --- a/src/types/options.d.ts +++ b/src/types/options.d.ts @@ -1,14 +1,13 @@ import type { Strategy } from './strategy'; import type { NuxtPlugin } from '@nuxt/schema'; import type { AuthState } from './index'; +import type { CookieSerializeOptions } from 'cookie-es'; export interface ModuleOptions { globalMiddleware?: boolean; enableMiddleware?: boolean; plugins?: (NuxtPlugin | string)[]; - strategies?: { - [strategy: string]: Strategy | false; - }; + strategies?: Record; ignoreExceptions: boolean; resetOnError: boolean | ((...args: any[]) => boolean); defaultStrategy: string | undefined; @@ -16,29 +15,34 @@ export interface ModuleOptions { rewriteRedirects: boolean; fullPathRedirect: boolean; redirectStrategy?: 'query' | 'storage'; - routerStrategy?: string; scopeKey: string; + stores: Partial<{ + state: { + namespace?: string + }; + pinia: { + enabled: boolean; + namespace?: string; + }; + cookie: { + enabled: boolean; + prefix?: string; + options?: CookieSerializeOptions; + }; + local: { + enabled: boolean; + prefix?: string; + }; + session: { + enabled: boolean; + prefix?: string; + }; + }>, redirect: { login: string; logout: string; callback: string; home: string; }; - pinia: { - namespace: string; - }; - cookie: { - prefix?: string; - options?: { - path?: string; - expires?: Date; - maxAge?: number; - domain?: string; - secure?: boolean; - sameSite?: 'strict' | 'lax' | 'none'; - }; - }; - localStorage: { prefix: string; } | false; - sessionStorage: { prefix: string; } | false; initialState?: AuthState; } diff --git a/src/types/provider.d.ts b/src/types/provider.d.ts index d209f33..1f5ee43 100644 --- a/src/types/provider.d.ts +++ b/src/types/provider.d.ts @@ -1,6 +1,8 @@ import type { SchemeOptions } from './scheme'; import type { PartialExcept } from './utils'; +export type ProviderNames = 'laravel/sanctum' | 'laravel/jwt' | 'laravel/passport' | 'google' | 'github' | 'facebook' | 'discord' | 'auth0' | N | ((...args: any[]) => any) + export interface ProviderOptions { scheme: string; clientSecret: string | number; diff --git a/src/types/scheme.d.ts b/src/types/scheme.d.ts index 825c8fd..5970143 100644 --- a/src/types/scheme.d.ts +++ b/src/types/scheme.d.ts @@ -3,12 +3,9 @@ import type { Auth } from '../runtime/core'; import type { Token, IdToken, RefreshToken, RefreshController, RequestHandler } from '../runtime/inc'; import type { PartialExcept } from './utils'; -export interface UserOptions { - property: string | false; - autoFetch: boolean; -} +export type SchemeNames = 'local' | 'cookie' | 'laravelJWT' | 'openIDConnect' | 'refresh' | 'oauth2' | 'auth0' | N -export interface CookieUserOptions { +export interface UserOptions { property: string | false; autoFetch: boolean; } @@ -38,7 +35,7 @@ export interface Scheme { name?: string; $auth: Auth; mounted?(...args: any[]): Promise | void>; - check?(checkStatus: boolean): SchemeCheck; + check?(checkStatus?: boolean): SchemeCheck; login(...args: any[]): Promise | void>; fetchUser(endpoint?: HTTPRequest): Promise | void>; setUserToken?( diff --git a/src/types/store.d.ts b/src/types/store.d.ts new file mode 100644 index 0000000..dc1705b --- /dev/null +++ b/src/types/store.d.ts @@ -0,0 +1,42 @@ +import type { Store, Pinia, StoreGeneric } from 'pinia'; +import type { CookieSerializeOptions } from 'cookie-es'; + +export type AuthStoreDefinition = Store; + /** + * Sets the key/value pair for the auth module's auth state. + * + * @param payload - object containing the key and value + */ + SET(payload: any): void; +}> + +export type StoreMethod = 'cookie' | 'session' | 'local'; + +export interface StoreIncludeOptions { + cookie?: boolean | CookieSerializeOptions; + session?: boolean; + local?: boolean; +} + +export interface UserInfo { + [key: string]: unknown; +} + +export type AuthState = { + [key: string]: unknown; + // user object + user?: UserInfo; + // indicates whether the user is logged in + loggedIn?: boolean; + // indicates the strategy of authentication used + strategy?: string; + // indicates if the authentication system is busy performing tasks, may not be defined initially + busy?: boolean; +} \ No newline at end of file diff --git a/src/types/strategy.d.ts b/src/types/strategy.d.ts index f2f6592..d03740b 100644 --- a/src/types/strategy.d.ts +++ b/src/types/strategy.d.ts @@ -1,11 +1,16 @@ -import type { SchemeOptions } from './scheme'; -import type { ProviderPartialOptions, ProviderOptions } from './provider'; +import type { SchemePartialOptions, RefreshableSchemeOptions, SchemeOptions, SchemeNames } from './scheme'; +import type { CookieSchemeOptions, Oauth2SchemeOptions, OpenIDConnectSchemeOptions } from '../runtime/schemes'; +import type { ProviderPartialOptions, ProviderOptions, ProviderNames } from './provider'; -export interface Strategy extends SchemeOptions { - provider?: string | ((...args: any[]) => any); - scheme?: string; +export type Strategy = S & Strategies; + +export interface AuthSchemeOptions extends RefreshableSchemeOptions, Oauth2SchemeOptions, CookieSchemeOptions, OpenIDConnectSchemeOptions {} + +export interface Strategies extends SchemePartialOptions { + provider?: ProviderNames | ((...args: any[]) => any); + scheme?: SchemeNames; enabled?: boolean; - [option: string]: any; + [key: string]: any; } export type StrategyOptions = ProviderPartialOptions; diff --git a/src/utils/provider.ts b/src/utils/provider.ts index 642661d..39078b1 100644 --- a/src/utils/provider.ts +++ b/src/utils/provider.ts @@ -1,5 +1,5 @@ import type { Oauth2SchemeOptions, RefreshSchemeOptions, LocalSchemeOptions, CookieSchemeOptions } from '../runtime'; -import type { StrategyOptions, HTTPRequest } from '../types'; +import type { StrategyOptions, HTTPRequest, TokenableSchemeOptions } from '../types'; import type { Nuxt } from '@nuxt/schema'; import { addServerHandler, addTemplate } from '@nuxt/kit'; import { join } from 'pathe'; @@ -80,7 +80,7 @@ export function initializePasswordGrantFlow>(strategy: SOptions): void { +export function assignAbsoluteEndpoints>(strategy: SOptions): void { const { url, endpoints } = strategy; if (endpoints) {