├── .npmrc
├── playground
├── tsconfig.json
├── server
│ └── tsconfig.json
├── app.vue
├── package.json
├── nuxt.config.ts
└── pages
│ ├── forgot-password.vue
│ ├── two-factor-authentication.vue
│ ├── login.vue
│ ├── solve-2fa.vue
│ ├── register.vue
│ └── dashboard.vue
├── .vscode
└── settings.json
├── src
├── runtime
│ ├── server
│ │ └── tsconfig.json
│ ├── plugins
│ │ ├── userInit.ts
│ │ └── fortifyApi.ts
│ ├── composables
│ │ ├── useApi.ts
│ │ ├── useFortifyIntendedRedirect.ts
│ │ ├── useTokenStorage.ts
│ │ ├── useFortifyUser.ts
│ │ └── useFortifyFeatures.ts
│ ├── middleware
│ │ ├── nuxt-fortify.guest.ts
│ │ └── nuxt-fortify.auth.ts
│ └── types
│ │ └── options.ts
└── module.ts
├── test
├── fixtures
│ └── basic
│ │ ├── package.json
│ │ ├── app.vue
│ │ └── nuxt.config.ts
└── basic.test.ts
├── .github
├── FUNDING.yml
├── PULL_REQUEST_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── feature-request.md
│ └── bug_report.md
└── workflows
│ └── validation.yml
├── tsconfig.json
├── .editorconfig
├── eslint.config.mjs
├── .gitignore
├── LICENSE
├── package.json
├── CHANGELOG.md
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | strict-peer-dependencies=false
3 |
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.nuxt/tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.experimental.useFlatConfig": true
3 | }
4 |
--------------------------------------------------------------------------------
/playground/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../.nuxt/tsconfig.server.json"
3 | }
4 |
--------------------------------------------------------------------------------
/src/runtime/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../.nuxt/tsconfig.server.json",
3 | }
4 |
--------------------------------------------------------------------------------
/playground/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test/fixtures/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "basic",
4 | "type": "module"
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/basic/app.vue:
--------------------------------------------------------------------------------
1 |
2 | basic
3 |
4 |
5 |
7 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [dev-charles15531]
2 | patreon: DevCharles
3 | custom: ['https://www.buymeacoffee.com/devcharles15531']
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.nuxt/tsconfig.json",
3 | "exclude": [
4 | "dist",
5 | "node_modules",
6 | "playground",
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/test/fixtures/basic/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | import MyModule from '../../../src/module'
2 |
3 | export default defineNuxtConfig({
4 | modules: [
5 | MyModule,
6 | ],
7 | })
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_size = 2
5 | indent_style = space
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "my-module-playground",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "nuxi dev",
7 | "build": "nuxi build",
8 | "generate": "nuxi generate"
9 | },
10 | "dependencies": {
11 | "nuxt": "^3.12.3"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/runtime/plugins/userInit.ts:
--------------------------------------------------------------------------------
1 | import { useFortifyUser } from '../composables/useFortifyUser'
2 | import { defineNuxtPlugin } from '#app'
3 |
4 | export default defineNuxtPlugin(async () => {
5 | const { user, refreshUser } = useFortifyUser()
6 |
7 | if (!user.value) {
8 | await refreshUser()
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/src/runtime/composables/useApi.ts:
--------------------------------------------------------------------------------
1 | import type { $Fetch } from 'ofetch'
2 | import { useNuxtApp } from '#app'
3 |
4 | /**
5 | * Returns a reference to the $fortifyApi property of the nuxt runtime app.
6 | *
7 | * This function is used to fetch data from the Fortify API.
8 | *
9 | * @returns {$Fetch} A reference to the $fortifyApi property of the nuxt runtime app.
10 | */
11 | export const useApi = (): $Fetch => {
12 | const api = useNuxtApp().$fortifyApi
13 |
14 | return api as $Fetch
15 | }
16 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
3 |
4 | // Run `npx @eslint/config-inspector` to inspect the resolved config interactively
5 | export default createConfigForNuxt({
6 | features: {
7 | // Rules for module authors
8 | tooling: true,
9 | // Rules for formatting
10 | stylistic: true,
11 | },
12 | dirs: {
13 | src: [
14 | './playground',
15 | ],
16 | },
17 | })
18 | .append(
19 | // your custom flat config here...
20 | )
21 |
--------------------------------------------------------------------------------
/test/basic.test.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'node:url'
2 | import { describe, it, expect } from 'vitest'
3 | import { setup, $fetch } from '@nuxt/test-utils/e2e'
4 |
5 | describe('ssr', async () => {
6 | await setup({
7 | rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
8 | })
9 |
10 | it('renders the index page', async () => {
11 | // Get response to a server-rendered page with `$fetch`.
12 | const html = await $fetch('/')
13 | expect(html).toContain('
basic
')
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/runtime/middleware/nuxt-fortify.guest.ts:
--------------------------------------------------------------------------------
1 | import type { BaseModuleOptions } from '../types/options'
2 | import { useFortifyUser } from '../composables/useFortifyUser'
3 | import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from '#app'
4 |
5 | export default defineNuxtRouteMiddleware(async (to) => {
6 | const config = useRuntimeConfig().public.nuxtFortify as BaseModuleOptions
7 |
8 | const { user } = useFortifyUser()
9 |
10 | if (!user.value) {
11 | return
12 | }
13 |
14 | if (to.path !== config.authHome) return navigateTo(config.authHome, { replace: true })
15 | })
16 |
--------------------------------------------------------------------------------
/src/runtime/composables/useFortifyIntendedRedirect.ts:
--------------------------------------------------------------------------------
1 | import { type Ref } from 'vue'
2 | import { useState } from '#imports'
3 |
4 | /**
5 | * Returns a reference to the intended route stored in the state.
6 | *
7 | * @template RouteLocationNormalized - The type of the route location.
8 | * @returns {Ref} - A reference to the intended route.
9 | */
10 | export function useFortifyIntendedRedirect(): Ref {
11 | // Get the intended route from the state.
12 | // If the intended route is not set, it returns null.
13 | const intendedRoute = useState(
14 | 'nuxt-fortify-intended-redirect',
15 | () => null,
16 | )
17 |
18 | return intendedRoute
19 | }
20 |
--------------------------------------------------------------------------------
/src/runtime/composables/useTokenStorage.ts:
--------------------------------------------------------------------------------
1 | import { type Ref } from 'vue'
2 | import type { BaseModuleOptions } from '../types/options'
3 | import { useRuntimeConfig, useState } from '#imports'
4 |
5 | /**
6 | * Returns a reference to the token stored in the state.
7 | *
8 | * @returns {Ref} - A reference to the token.
9 | */
10 | export function useTokenStorage(): Ref {
11 | const config = useRuntimeConfig().public.nuxtFortify as BaseModuleOptions
12 |
13 | // Get the token from the state using the token storage key.
14 | // If the token is not set, it returns null.
15 | const cookieToken = useState(config.tokenStorageKey, () => null)
16 |
17 | return cookieToken
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules
3 |
4 | # Logs
5 | *.log*
6 |
7 | # Temp directories
8 | .temp
9 | .tmp
10 | .cache
11 |
12 | # Yarn
13 | **/.yarn/cache
14 | **/.yarn/*state*
15 |
16 | # Generated dirs
17 | dist
18 |
19 | # Nuxt
20 | .nuxt
21 | .output
22 | .data
23 | .vercel_build_output
24 | .build-*
25 | .netlify
26 |
27 | # Env
28 | .env
29 |
30 | # Testing
31 | reports
32 | coverage
33 | *.lcov
34 | .nyc_output
35 |
36 | # VSCode
37 | .vscode/*
38 | !.vscode/settings.json
39 | !.vscode/tasks.json
40 | !.vscode/launch.json
41 | !.vscode/extensions.json
42 | !.vscode/*.code-snippets
43 |
44 | # Intellij idea
45 | *.iml
46 | .idea
47 |
48 | # OSX
49 | .DS_Store
50 | .AppleDouble
51 | .LSOverride
52 | .AppleDB
53 | .AppleDesktop
54 | Network Trash Folder
55 | Temporary Items
56 | .apdisk
57 |
--------------------------------------------------------------------------------
/src/runtime/middleware/nuxt-fortify.auth.ts:
--------------------------------------------------------------------------------
1 | import type { BaseModuleOptions } from '../types/options'
2 | import { useFortifyIntendedRedirect } from '../composables/useFortifyIntendedRedirect'
3 | import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from '#app'
4 | import { useFortifyUser } from '#imports'
5 |
6 | export default defineNuxtRouteMiddleware((to, from) => {
7 | const config = useRuntimeConfig().public.nuxtFortify as BaseModuleOptions
8 | const { user } = useFortifyUser()
9 |
10 | if (user.value) {
11 | return
12 | }
13 |
14 | // save current route to be able to redirect to it after login
15 | if (config.intendedRedirect) {
16 | const intendedRoute = useFortifyIntendedRedirect()
17 | intendedRoute.value = from.fullPath
18 | }
19 |
20 | if (to.path !== config.loginRoute) {
21 | return navigateTo(config.loginRoute, { replace: true })
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Charles Paul
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Pull Request Template
2 |
3 | ## 📋 Description
4 |
5 | Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change.
6 |
7 | Fixes # (issue)
8 |
9 | ## ✅ Type of change
10 |
11 | Please delete options that are not relevant.
12 |
13 | - [ ] Bug fix (non-breaking change which fixes an issue)
14 | - [ ] New feature (non-breaking change which adds functionality)
15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
16 | - [ ] Documentation update
17 |
18 | ## ⚠️ Checklist:
19 |
20 | - [ ] My code follows the style guidelines of this project
21 | - [ ] I have performed a self-review of my code
22 | - [ ] I have commented my code, particularly in hard-to-understand areas
23 | - [ ] I have made corresponding changes to the documentation
24 | - [ ] My changes generate no new warnings
25 | - [ ] Any dependent changes have been merged and published in downstream modules
26 |
27 | ## 📸 Screenshots (if appropriate):
28 |
29 | ## 📝 Additional context
30 |
31 | Add any other context about the pull request here.
32 |
--------------------------------------------------------------------------------
/playground/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtConfig({
2 | modules: ['../src/module'],
3 | nuxtFortify: {
4 | baseUrl: 'http://localhost',
5 | authMode: 'cookie',
6 | authHome: '/dashboard',
7 | features: {
8 | registration: true,
9 | resetPasswords: true,
10 | emailVerification: true,
11 | twoFactorAuthentication: true,
12 | updatePasswords: true,
13 | },
14 | endpoints: {
15 | csrf: '/sanctum/csrf-cookie',
16 | login: '/api/login',
17 | logout: '/api/logout',
18 | user: '/api/user',
19 | tfa: {
20 | enable: '/api/user/two-factor-authentication',
21 | disable: '/api/user/two-factor-authentication',
22 | code: '/api/user/two-factor-qr-code',
23 | recoveryCode: '/api/user/two-factor-recovery-codes',
24 | challenge: '/api/two-factor-challenge',
25 | confirm: '/api/user/confirmed-two-factor-authentication',
26 | },
27 | register: '/api/register',
28 | resetPassword: '/api/forgot-password',
29 | updatePassword: '/api/reset-password',
30 | confirmPassword: '/api/user/confirm-password',
31 | resendEmailVerificationLink: '/api/email/verification-notification',
32 | },
33 | },
34 | devtools: { enabled: true },
35 | compatibilityDate: '2024-07-14',
36 | })
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nuxt-fortify",
3 | "version": "1.1.2",
4 | "description": "Use Laravel fortify and sanctum with Nuxt",
5 | "repository": "https://github.com/dev-charles15531/nuxt-fortify",
6 | "license": "MIT",
7 | "author": "Charles Paul ",
8 | "type": "module",
9 | "exports": {
10 | ".": {
11 | "types": "./dist/types.d.ts",
12 | "import": "./dist/module.mjs",
13 | "require": "./dist/module.cjs"
14 | }
15 | },
16 | "main": "./dist/module.cjs",
17 | "types": "./dist/types.d.ts",
18 | "files": [
19 | "dist"
20 | ],
21 | "scripts": {
22 | "prepack": "nuxt-module-build build",
23 | "dev": "nuxi dev playground",
24 | "dev:build": "nuxi build playground",
25 | "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
26 | "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
27 | "lint": "eslint .",
28 | "test": "vitest run",
29 | "test:watch": "vitest watch",
30 | "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
31 | },
32 | "dependencies": {
33 | "@nuxt/kit": "^3.12.3",
34 | "defu": "^6.1.4"
35 | },
36 | "devDependencies": {
37 | "@nuxt/devtools": "^1.3.9",
38 | "@nuxt/eslint-config": "^0.3.13",
39 | "@nuxt/module-builder": "^0.8.1",
40 | "@nuxt/schema": "^3.12.3",
41 | "@nuxt/test-utils": "^3.13.1",
42 | "@types/node": "^20.14.9",
43 | "changelogen": "^0.5.5",
44 | "eslint": "^9.6.0",
45 | "nuxt": "^3.12.3",
46 | "typescript": "latest",
47 | "vitest": "^1.6.0",
48 | "vue-tsc": "^2.0.24"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: '[Feature] Short description'
5 | labels: enhancement
6 | assignees: dev-charles1531
7 | ---
8 |
9 | ## 📝 Feature Request
10 |
11 | ### 🚩 Problem Statement
12 |
13 | Is your feature request related to a problem? Please describe in detail what the problem is. Example: "I'm always frustrated when [...]"
14 |
15 | ### 💡 Proposed Solution
16 |
17 | Describe the solution you'd like to see. A clear and concise description of what you want to happen.
18 |
19 | ### 🔄 Alternatives Considered
20 |
21 | List any alternative solutions or features you've considered. A clear and concise description of these alternatives.
22 |
23 | ### 📷 Additional Context
24 |
25 | Add any other context or screenshots about the feature request here. This could include examples from other projects, references to relevant documentation, or anything else that can help understand the request.
26 |
27 | ### ✅ Acceptance Criteria
28 |
29 | Define what the acceptance criteria would be for this feature request. What should be checked or tested to ensure the feature is working as expected?
30 |
31 | - [ ] Criterion 1
32 | - [ ] Criterion 2
33 | - [ ] Criterion 3
34 |
35 | ### 📅 Implementation Plan
36 |
37 | If possible, outline an implementation plan. What steps should be taken to implement this feature?
38 |
39 | ### 🤝 Contributing
40 |
41 | If you are interested in contributing this feature, let us know! We would be happy to provide guidance and support.
42 |
43 | - [ ] I would like to work on this feature
44 | - [ ] I need help to implement this feature
45 |
46 | ### 💬 Additional Comments
47 |
48 | Add any final comments or notes here.
49 |
--------------------------------------------------------------------------------
/playground/pages/forgot-password.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Forgot password Page
6 |
7 |
8 |
9 |
10 |
11 | Enter your email to get reset link
12 |
13 |
14 |
15 | {{ error.resetPassword._data.message }}
16 |
17 |
21 |
22 | {{ successMssg }}
23 |
24 |
25 |
26 |
27 |
34 |
35 | back to login
36 |
37 |
38 |
39 |
40 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
77 |
--------------------------------------------------------------------------------
/playground/pages/two-factor-authentication.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 2FA Page
6 |
7 |
8 |
9 |
10 |
11 |
12 | Enter your 2FA code
13 |
14 |
15 |
16 | {{ error.solveTwoFactorAuthenticationChallenge._data.message }}
17 |
18 |
19 |
20 |
27 |
33 |
34 | Back to login
35 |
36 |
37 |
38 |
39 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
78 |
--------------------------------------------------------------------------------
/.github/workflows/validation.yml:
--------------------------------------------------------------------------------
1 | # Pipeline to run code quality checks (eslint, prettier, typecheck)
2 | name: Nuxt 3 [Validate]
3 |
4 | env:
5 | node_version: 20.x
6 | package_manager: npm
7 | install_command: npm ci
8 | script_command: npm run
9 |
10 | on:
11 | workflow_dispatch: # manual trigger
12 | pull_request:
13 | branches: [main]
14 |
15 | concurrency:
16 | group: module-validation
17 | cancel-in-progress: false
18 |
19 | jobs:
20 | validate:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 |
26 | - name: Setup Node.js
27 | uses: actions/setup-node@v3
28 | with:
29 | node-version: ${{ env.node_version }}
30 | cache: ${{ env.package_manager }}
31 |
32 | - name: Create packages cache
33 | uses: actions/cache@v3
34 | with:
35 | path: |
36 | dist
37 | .nuxt
38 | key: ${{ runner.os }}-nuxt-build-${{ hashFiles('dist') }}
39 | restore-keys: |
40 | ${{ runner.os }}-nuxt-build-
41 |
42 | - name: Install dependencies
43 | run: ${{ env.install_command }}
44 |
45 | - name: Run Prettier
46 | run: |
47 | ${{ env.script_command }} fmt:check
48 |
49 | - name: Run ESLint
50 | run: |
51 | ${{ env.script_command }} lint
52 |
53 | - name: Run TypeCheck
54 | run: |
55 | ${{ env.script_command }} test:types
56 |
57 | - name: Run tests
58 | run: |
59 | ${{ env.script_command }} test
60 |
61 | - name: Run build
62 | run: |
63 | ${{ env.script_command }} prepack
64 |
--------------------------------------------------------------------------------
/playground/pages/login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Login Page
6 |
7 |
8 |
9 |
10 |
11 | Enter your login details
12 |
13 |
14 |
15 | {{ error.login._data.message }}
16 |
17 |
18 |
19 |
26 |
32 |
33 |
34 | Forgot password
35 |
36 |
37 |
38 |
39 | Solve 2FA
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 | Don't have an account? Register
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
78 |
79 |
117 |
--------------------------------------------------------------------------------
/src/runtime/composables/useFortifyUser.ts:
--------------------------------------------------------------------------------
1 | import type { BaseModuleOptions } from '../types/options'
2 | import { useTokenStorage } from './useTokenStorage'
3 | import { useCookie, useRequestHeaders, useRequestURL, useRuntimeConfig, useState } from '#imports'
4 |
5 | /**
6 | * Fortify currently authenticated user composable.
7 | * @returns Reference to the user state as T or null | Fetch user function.
8 | */
9 | export function useFortifyUser() {
10 | const user = useState('nuxt-fortify-user', () => null)
11 | const config = useRuntimeConfig().public.nuxtFortify as BaseModuleOptions
12 |
13 | /**
14 | * Refresh the user state, fetches it from the API.
15 | *
16 | */
17 | const refreshUser = async () => {
18 | const token = useTokenStorage().value ?? useCookie(config.tokenStorageKey).value
19 |
20 | user.value = await fetchUser(config, token as string) as T
21 | }
22 |
23 | /**
24 | * Fetches the user details.
25 | *
26 | * @param {BaseModuleOptions} moduleConfig - The module configuration.
27 | * @param {string} authToken - The authentication token (API TOKEN).
28 | * @returns - A Promise that resolves with the user details or null if not authenticated.
29 | */
30 | async function fetchUser(
31 | moduleConfig: BaseModuleOptions,
32 | authToken: string,
33 | ) {
34 | const requestOrigin = moduleConfig.origin ?? useRequestURL().origin
35 | const cookie = useCookie(moduleConfig.cookieKey, { readonly: true })
36 |
37 | let headers: Record = {
38 | Accept: 'application/json',
39 | Referer: requestOrigin,
40 | Origin: requestOrigin,
41 | }
42 |
43 | if (moduleConfig.authMode === 'token') {
44 | headers.Authorization = `Bearer ${authToken}`
45 | }
46 | else if (moduleConfig.authMode === 'cookie') {
47 | headers[moduleConfig.cookieHeader] = cookie.value as string
48 |
49 | const clientCookies = useRequestHeaders(['cookie'])
50 | if (import.meta.server) {
51 | headers = {
52 | ...headers,
53 | ...(clientCookies.cookie && clientCookies),
54 | }
55 | }
56 | }
57 |
58 | try {
59 | const isCredentialsSupported = 'credentials' in Request.prototype
60 |
61 | const response = await $fetch(moduleConfig.endpoints.user, {
62 | baseURL: moduleConfig.baseUrl,
63 | method: 'POST',
64 | credentials: config.authMode == 'cookie' ? isCredentialsSupported ? 'include' : undefined : undefined,
65 | headers,
66 | })
67 |
68 | if (response) {
69 | return response
70 | }
71 | }
72 | catch (error) {
73 | console.log(error)
74 | }
75 |
76 | return null
77 | }
78 |
79 | return { user, refreshUser, fetchUser }
80 | }
81 |
--------------------------------------------------------------------------------
/playground/pages/solve-2fa.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Solve 2FA Page
6 |
7 |
8 |
9 |
10 |
11 | Enter your 2FA details
12 |
13 |
14 |
15 | NOTE: When on cookie auth mode, this page will always return "code invalid". You must be authenticated first before solving 2FA
16 |
17 |
18 |
19 | {{ error.solveTwoFactorAuthenticationChallenge._data.message }}
20 |
21 |
22 |
26 |
27 | {{ successMssg }}
28 |
29 |
30 |
31 |
35 |
36 | {{ errorMssg }}
37 |
38 |
39 |
40 |
41 |
47 |
53 |
54 |
55 | Back to login
56 |
57 |
58 |
59 |
60 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
104 |
--------------------------------------------------------------------------------
/playground/pages/register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Register Page
6 |
7 |
8 |
9 |
10 |
11 | Enter your details to register
12 |
13 |
14 |
15 | {{ error.register._data.message }}
16 |
17 |
21 |
22 | {{ successMssg }}
23 |
24 |
25 |
26 |
27 |
34 |
41 |
47 |
53 |
54 |
55 |
56 |
62 |
63 |
64 |
65 | Already have an account? Login
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
115 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 |
4 | ## v1.1.0
5 |
6 |
7 | ### 🚀 Enhancements
8 |
9 | - **login:** Added login feature to the useFortifyFeatures composable ([ed2bddf](https://github.com/dev-charles15531/nuxt-fortify/commit/ed2bddf))
10 | - **2FA:** Added Two Factor Authentication feature to the useFortifyFeatures composable ([62cbe14](https://github.com/dev-charles15531/nuxt-fortify/commit/62cbe14))
11 | - **register:** Added register feature ([34737ee](https://github.com/dev-charles15531/nuxt-fortify/commit/34737ee))
12 | - **resend email verification:** Added the reset email verification feature. ([537e038](https://github.com/dev-charles15531/nuxt-fortify/commit/537e038))
13 | - **forgot password:** Added the forgot password feature ([5355a55](https://github.com/dev-charles15531/nuxt-fortify/commit/5355a55))
14 | - **confirm password:** Added the confirm password feature. ([d87942c](https://github.com/dev-charles15531/nuxt-fortify/commit/d87942c))
15 | - Added guest and auth middleware ([2b4f14a](https://github.com/dev-charles15531/nuxt-fortify/commit/2b4f14a))
16 | - **middleware:** Added auth and guest middleware ([0902cb0](https://github.com/dev-charles15531/nuxt-fortify/commit/0902cb0))
17 | - **logout:** Added logout feature ([67e5fac](https://github.com/dev-charles15531/nuxt-fortify/commit/67e5fac))
18 | - **confirm2fa:** Added the confirm 2FA feature ([48ec686](https://github.com/dev-charles15531/nuxt-fortify/commit/48ec686))
19 |
20 | ### 🩹 Fixes
21 |
22 | - Fixed ESLint error ([d588aaa](https://github.com/dev-charles15531/nuxt-fortify/commit/d588aaa))
23 | - Linting error in playground ([82671dc](https://github.com/dev-charles15531/nuxt-fortify/commit/82671dc))
24 | - Cookie header bug in useFortifyUser.ts ([201286e](https://github.com/dev-charles15531/nuxt-fortify/commit/201286e))
25 | - Minor bug fix ([2c1857e](https://github.com/dev-charles15531/nuxt-fortify/commit/2c1857e))
26 |
27 | ### 🏡 Chore
28 |
29 | - Added response interceptor to initialize auth user after login ([4914cc3](https://github.com/dev-charles15531/nuxt-fortify/commit/4914cc3))
30 | - Added fetch user scope for cookie mode ([cb774d6](https://github.com/dev-charles15531/nuxt-fortify/commit/cb774d6))
31 | - Added DeepPartial type to the BaseModuleOption type ([8fbee7f](https://github.com/dev-charles15531/nuxt-fortify/commit/8fbee7f))
32 | - Code refractor and playground building ([ad904e3](https://github.com/dev-charles15531/nuxt-fortify/commit/ad904e3))
33 | - Playground build and some code refractoring ([2a7842d](https://github.com/dev-charles15531/nuxt-fortify/commit/2a7842d))
34 | - Playground for 2FA after login and code refractoring ([9428b60](https://github.com/dev-charles15531/nuxt-fortify/commit/9428b60))
35 | - Added midleware for 2FA route ([1156ad0](https://github.com/dev-charles15531/nuxt-fortify/commit/1156ad0))
36 | - Added workflows, github templates and update README.md ([6fabc6b](https://github.com/dev-charles15531/nuxt-fortify/commit/6fabc6b))
37 |
38 | ### ❤️ Contributors
39 |
40 | - Dev-charles15531 ([@dev-charles15531](http://github.com/dev-charles15531))
41 |
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | Here is a refined and detailed version of the bug report issue template to ensure clarity and completeness when reporting bugs:
2 |
3 | ```markdown
4 | ---
5 | name: Bug report
6 | about: Create a report for a bug or incorrect behavior of the project
7 | title: '[Bug] Short description'
8 | labels: bug
9 | assignees: dev-charles15531
10 | ---
11 |
12 | ## 🐛 Bug Report
13 |
14 | ### 📋 Description
15 |
16 | A clear and concise description of what the bug is.
17 |
18 | ### 🛠️ To Reproduce
19 |
20 | Steps to reproduce the behavior:
21 |
22 | 1. Go to '...'
23 | 2. Click on '....'
24 | 3. See error
25 |
26 | ### ✅ Expected Behavior
27 |
28 | A clear and concise description of what you expected to happen.
29 |
30 | ### 📸 Screenshots
31 |
32 | If applicable, add screenshots to help explain your problem.
33 |
34 | ### ℹ️ Module Information
35 |
36 | - **Version**:
37 | - **Complete Configuration of `nuxtFortify` from your `nuxt.config.ts`**:
38 |
39 | ```typescript
40 | export default defineNuxtConfig({
41 | modules: ['nuxt-fortify'],
42 |
43 | nuxtFortify: {
44 | baseUrl: 'http://localhost:3000/api',
45 | origin: 'http://localhost:3000',
46 | authMode: 'cookie',
47 | authHome: '/dashboard',
48 | endpoints: {
49 | csrf: '/sanctum/csrf-cookie',
50 | user: '/user',
51 | // other endpoints...
52 | },
53 | features: {
54 | registration: true,
55 | resetPasswords: true,
56 | twoFactorAuthentication: true,
57 | // other features...
58 | }
59 | // other configurations...
60 | }
61 | },
62 | });
63 | ```
64 |
65 | ### 🌐 Nuxt Environment
66 |
67 | - **Version**: YOUR_NUXT_VERSION
68 | - **SSR Enabled**: (yes / no)
69 | - **Environment**: (local / production)
70 |
71 | ### 🖥️ Laravel Environment
72 |
73 | - **Version**: YOUR_LARAVEL_VERSION
74 | - **Sanctum installed via Breeze**: (yes / no)
75 | - **Session Domain from your `config/session.php`**: [e.g. `domain.test`]
76 | - **List of Stateful Domains from your `config/sanctum.php`**:
77 |
78 | ```php
79 | return [
80 | 'stateful' => explode(
81 | ',',
82 | env(
83 | 'SANCTUM_STATEFUL_DOMAINS',
84 | sprintf('%s','localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1')
85 | )
86 | ),
87 | ];
88 | ```
89 |
90 | or
91 |
92 | ```env
93 | SANCTUM_STATEFUL_DOMAINS=localhost,localhost:3000
94 | ```
95 |
96 | - **CORS Settings from your `config/cors.php`**:
97 |
98 | ```php
99 | return [
100 | 'paths' => ['*'],
101 | 'allowed_methods' => ['*'],
102 | 'allowed_origins' => [
103 | env('FRONTEND_URL', 'http://localhost:3000'),
104 | ],
105 | 'allowed_origins_patterns' => [],
106 | 'allowed_headers' => ['*'],
107 | 'exposed_headers' => [],
108 | 'max_age' => 0,
109 | 'supports_credentials' => true,
110 | ];
111 | ```
112 |
113 | ### 📜 Additional Context
114 |
115 | Add any other context about the problem here. For instance, you can attach the details about the request/response of the application or logs from the backend to make this problem easier to understand.
116 |
--------------------------------------------------------------------------------
/src/module.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defineNuxtModule,
3 | addPlugin,
4 | addImportsDir,
5 | createResolver,
6 | useLogger,
7 | addRouteMiddleware,
8 | } from '@nuxt/kit'
9 | import { defu } from 'defu'
10 | import type { BaseModuleOptions } from './runtime/types/options'
11 |
12 | type DeepPartial = {
13 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P];
14 | }
15 |
16 | export type ModuleOptions = DeepPartial
17 |
18 | export default defineNuxtModule ({
19 | meta: {
20 | name: 'nuxt-fortify',
21 | configKey: 'nuxtFortify',
22 | },
23 | // Default configuration options of the Nuxt module
24 | defaults: {
25 | baseUrl: 'http://localhost:3000/api',
26 | authMode: 'cookie',
27 | loginRoute: '/login',
28 | authHome: '/home',
29 | cookieKey: 'XSRF-TOKEN',
30 | cookieHeader: 'X-XSRF-TOKEN',
31 | tokenStorageKey: 'API-TOKEN',
32 | endpoints: {
33 | csrf: '/sanctum/csrf-cookie',
34 | login: '/login',
35 | logout: '/logout',
36 | user: '/user',
37 | tfa: {
38 | enable: '/user/two-factor-authentication',
39 | disable: '/user/two-factor-authentication',
40 | code: '/user/two-factor-qr-code',
41 | confirm: '/user/confirmed-two-factor-authentication',
42 | recoveryCode: '/user/two-factor-recovery-codes',
43 | challenge: '/two-factor-challenge',
44 | },
45 | register: '/register',
46 | resetPassword: '/forgot-password',
47 | updatePassword: '/reset-password',
48 | confirmPassword: '/user/confirm-password',
49 | resendEmailVerificationLink: '/email/verification-notification',
50 | },
51 | intendedRedirect: true,
52 | features: {
53 | registration: true,
54 | resetPasswords: true,
55 | emailVerification: true,
56 | },
57 | tfaRoute: '/two-factor-authentication',
58 | logLevel: 1,
59 | origin: 'http://localhost:3000',
60 | },
61 | setup(_options, _nuxt) {
62 | const resolver = createResolver(import.meta.url)
63 |
64 | const runtimeDir = resolver.resolve('./runtime')
65 | _nuxt.options.build.transpile.push(runtimeDir)
66 |
67 | const nuxtFortifyConfig = defu(
68 | _nuxt.options.runtimeConfig.public.nuxtFortify,
69 | _options,
70 | )
71 | _nuxt.options.runtimeConfig.public.nuxtFortify = nuxtFortifyConfig
72 |
73 | const logger = useLogger('nuxt-fortify', {
74 | level: nuxtFortifyConfig.logLevel,
75 | })
76 |
77 | logger.start('Initializing Nuxt Fortify module...')
78 |
79 | // Add plugins
80 | addPlugin(resolver.resolve('./runtime/plugins/userInit'))
81 | addPlugin(resolver.resolve('./runtime/plugins/fortifyApi'))
82 |
83 | // Add composables
84 | addImportsDir(resolver.resolve('runtime/composables'))
85 |
86 | // Add middlewares
87 | addRouteMiddleware({
88 | name: 'fortify:auth',
89 | path: resolver.resolve('./runtime/middleware/nuxt-fortify.auth'),
90 | })
91 | addRouteMiddleware({
92 | name: 'fortify:guest',
93 | path: resolver.resolve('./runtime/middleware/nuxt-fortify.guest'),
94 | })
95 |
96 | // middleware for 2FA route
97 | if (nuxtFortifyConfig.authMode === 'cookie') {
98 | addRouteMiddleware({
99 | name: 'fortify:2fa',
100 | path: resolver.resolve('./runtime/middleware/nuxt-fortify.guest'),
101 | })
102 | }
103 | else if (nuxtFortifyConfig.authMode === 'token') {
104 | addRouteMiddleware({
105 | name: 'fortify:2fa',
106 | path: resolver.resolve('./runtime/middleware/nuxt-fortify.auth'),
107 | })
108 | }
109 | },
110 | })
111 |
--------------------------------------------------------------------------------
/src/runtime/types/options.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Options to be passed to the module.
3 | */
4 | export interface BaseModuleOptions {
5 | /**
6 | * The base URL of the Laravel API.
7 | */
8 | baseUrl: string
9 |
10 | /**
11 | * The mode to use for authentication.
12 | */
13 | authMode: 'cookie' | 'token'
14 |
15 | /**
16 | * The route to redirect to when the user is not authenticated.
17 | */
18 | loginRoute: string
19 |
20 | /**
21 | * The route to redirect to after successful user authentication.
22 | */
23 | authHome?: string
24 |
25 | /**
26 | * The name of the cookie that contains the CSRF token.
27 | */
28 | cookieKey: string
29 |
30 | /**
31 | * The name of the cookie header that contains the CSRF token.
32 | */
33 | cookieHeader: string
34 |
35 | /**
36 | * The key to store the token in the storage.
37 | */
38 | tokenStorageKey: string
39 |
40 | /**
41 | * The endpoints to use for API requests.
42 | */
43 | endpoints: ApiEndpoints
44 |
45 | /**
46 | * Whether to redirect to the intended route after successful authentication.
47 | */
48 | intendedRedirect: boolean
49 |
50 | /**
51 | * The features to enable.
52 | */
53 | features?: FortifyFeatures
54 |
55 | /**
56 | * The route to redirect to for Two Factor Authentication.
57 | */
58 | tfaRoute: string
59 |
60 | /**
61 | * The log level to use for the module.
62 | */
63 | logLevel: 0 | 1 | 2 | 3 | 4 | 5
64 |
65 | /**
66 | * The origin to use for CORS requests.
67 | */
68 | origin?: string
69 | }
70 |
71 | export interface ApiEndpoints {
72 | /**
73 | * The endpoint to request a new CSRF token.
74 | * @default '/sanctum/csrf-cookie'
75 | */
76 | csrf: string
77 |
78 | /**
79 | * The endpoint to send user credentials to authenticate.
80 | * @default '/login'
81 | */
82 | login: string
83 |
84 | /**
85 | * The endpoint to destroy current user session.
86 | * @default '/logout'
87 | */
88 | logout: string
89 |
90 | /**
91 | * The endpoint to fetch current user data.
92 | * @default '/user'
93 | */
94 | user: string
95 |
96 | /**
97 | * 2FA endpoints.
98 | */
99 | tfa?: {
100 | /**
101 | * The endpoint to enable 2FA.
102 | * @default '/user/two-factor-authentication'
103 | */
104 | enable: string
105 | /**
106 | * The endpoint to initialize 2FA QR code.
107 | * @default '/user/two-factor-qr-code'
108 | */
109 | code: string
110 | /**
111 | * The endpoint to confirm 2FA.
112 | * @default '/user/confirmed-two-factor-authentication'
113 | */
114 | confirm: string
115 | /**
116 | * The endpoint to retrieve 2FA recovery codes.
117 | * @default '/user/two-factor-recovery-codes'
118 | */
119 | recoveryCode: string
120 | /**
121 | * The endpoint to solve 2FA challenge.
122 | * @default '/two-factor-challenge'
123 | */
124 | challenge: string
125 | /**
126 | * The endpoint to disable 2FA.
127 | * @default '/user/two-factor-authenticatione'
128 | */
129 | disable: string
130 | }
131 |
132 | /**
133 | * The endpoint to send user credentials for registration.
134 | * @default '/register'
135 | */
136 | register?: string
137 |
138 | /**
139 | * The endpoint to send user an email containing reset password link.
140 | * @default '/forgot-password'
141 | */
142 | resetPassword?: string
143 |
144 | /**
145 | * The endpoint to send user credentials for a password reset.
146 | * @default '/reset-password'
147 | */
148 | updatePassword?: string
149 |
150 | /**
151 | * The endpoint to send user an email containing verification link.
152 | * @default '/email/verification-notification'
153 | */
154 | resendEmailVerificationLink?: string
155 |
156 | /**
157 | * The endpoint for user password confirmation.
158 | * @default '/user/confirm-password'
159 | */
160 | confirmPassword?: string
161 | }
162 |
163 | export interface FortifyFeatures {
164 | /**
165 | * Whether to enable registration feature.
166 | * @default true
167 | */
168 | registration?: boolean
169 |
170 | /**
171 | * Whether to enable reset password feature.
172 | * @default true
173 | */
174 | resetPasswords?: boolean
175 |
176 | /**
177 | * Whether to enable email verification feature.
178 | * @default true
179 | */
180 | emailVerification?: boolean
181 |
182 | /**
183 | * Whether to enable user profile update feature.
184 | * @default false
185 | */
186 | updateProfileInformation?: boolean
187 |
188 | /**
189 | * Whether to enable user password update feature.
190 | * @default false
191 | */
192 | updatePasswords?: boolean
193 |
194 | /**
195 | * Whether to enable two factor authentication feature.
196 | */
197 | twoFactorAuthentication?: boolean
198 | }
199 |
--------------------------------------------------------------------------------
/playground/pages/dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Dashboard Page
6 |
7 |
8 |
9 |
12 |
13 |
14 | User Details
15 |
16 |
17 |
21 |
26 | {{ `${key}: ` }}
27 |
28 | {{ `${value}` }}
29 |
30 |
31 |
32 |
33 |
34 |
38 |
39 | {{ successMssg }}
40 |
41 |
42 |
43 |
47 |
48 | {{ errorMssg }}
49 |
50 |
51 |
52 |
58 |
59 |
63 |
64 | -
68 | {{ code }}
69 |
70 |
71 |
72 |
73 |
77 |
Confirm 2FA code
78 |
84 |
90 |
91 |
92 |
96 |
Confirm Password
97 |
103 |
109 |
110 |
111 |
112 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
299 |
300 |
324 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🎉 Nuxt Laravel Fortify and Sanctum Module
2 |
3 | [![npm version][npm-version-src]][npm-version-href]
4 | [![npm downloads][npm-downloads-src]][npm-downloads-href]
5 | [![License][license-src]][license-href]
6 | [![Nuxt][nuxt-src]][nuxt-href]
7 |
8 | This Nuxt module seamlessly integrates Nuxt with Laravel Fortify and Sanctum in an SSR-friendly way, offering a rich set of authentication features. With this module, you can leverage Laravel Fortify's capabilities and perform both API Token and SPA cookie-based authentication.
9 |
10 | ## 🚀 Features
11 |
12 | - **Registration** 📋
13 | - **Reset Passwords** 🔄
14 | - **Email Verification** 📧
15 | - **Update Profile Information** ✏️
16 | - **Update Passwords** 🔐
17 | - **Two-Factor Authentication** 🔒
18 |
19 | ## 🛠️ Installation and Configuration
20 |
21 | 💡**Notice:** You need to install and setup
22 | [Laravel Fortify](https://laravel.com/docs/11.x/fortify), [Laravel Sanctum](https://laravel.com/docs/11.x/sanctum), and [fortify-sanctum](https://github.com/dev-charles15531/fortify-sanctum) package in your backend Laravel application. The [fortify-sanctum](https://github.com/dev-charles15531/fortify-sanctum) package easily integrates Laravel Fortify's authentication features with Laravel Sanctum
23 |
24 |
25 |
26 | Add `nuxt-fortify` module to your nuxt project
27 | ```
28 | npx nuxi@latest module add nuxt-fortify
29 | ```
30 |
31 |
32 | ## 💻 Nuxt Configuration
33 |
34 | Add the module to your Nuxt project by installing it and configuring it in `nuxt.config.js`.
35 |
36 | ```javascript
37 | // nuxt.config.js
38 | export default {
39 | modules: [
40 | 'nuxt-fortify',
41 | ],
42 | nuxtFortify: {
43 | baseUrl: 'http://localhost:3000/api',
44 | origin: 'http://localhost:3000',
45 | authMode: 'cookie',
46 | authHome: '/dashboard',
47 | endpoints: {
48 | csrf: '/sanctum/csrf-cookie',
49 | user: '/user',
50 | // other endpoints...
51 | },
52 | features: {
53 | registration: true,
54 | resetPasswords: true,
55 | twoFactorAuthentication: true,
56 | // other features...
57 | }
58 | // other configurations...
59 | }
60 | }
61 | ```
62 |
63 | ## 📜 Configs
64 |
65 | | Key | Data Type | Default Value | Required |
66 | |------------------------------------|------------|-----------------------------------------|----------|
67 | | `baseUrl` | `string` | `http://localhost:3000/api` | Yes |
68 | | `authMode` | `string` | `cookie` | Yes |
69 | | `loginRoute` | `endpoint` | `/login` | No |
70 | | `authHome` | `endpoint` | `/home` | No |
71 | | `cookieKey` | `string` | `XSRF-TOKEN` | No |
72 | | `cookieHeader` | `string` | `X-XSRF-TOKEN` | No |
73 | | `tokenStorageKey` | `string` | `API-TOKEN` | No |
74 | | `endpoints.csrf` | `endpoint` | `/sanctum/csrf-cookie` | No |
75 | | `endpoints.login` | `endpoint` | `/login` | No |
76 | | `endpoints.logout` | `endpoint` | `/logout` | No |
77 | | `endpoints.user` | `endpoint` | `/user` | No |
78 | | `endpoints.tfa.enable` | `endpoint` | `/user/two-factor-authentication` | No |
79 | | `endpoints.tfa.disable` | `endpoint` | `/user/two-factor-authentication` | No |
80 | | `endpoints.tfa.code` | `endpoint` | `/user/two-factor-qr-code` | No |
81 | | `endpoints.tfa.confirm` | `endpoint` | `/user/confirmed-two-factor-authentication` | No |
82 | | `endpoints.tfa.recoveryCode` | `endpoint` | `/user/two-factor-recovery-codes` | No |
83 | | `endpoints.tfa.challenge` | `endpoint` | `/two-factor-challenge` | No |
84 | | `endpoints.register` | `endpoint` | `/register` | No |
85 | | `endpoints.resetPassword` | `endpoint` | `/forgot-password` | No |
86 | | `endpoints.updatePassword` | `endpoint` | `/reset-password` | No |
87 | | `endpoints.confirmPassword` | `endpoint` | `/user/confirm-password` | No |
88 | | `endpoints.resendEmailVerificationLink` | `endpoint` | `/email/verification-notification` | No |
89 | | `intendedRedirect` | `boolean` | `true` | No |
90 | | `features.registration` | `boolean` | `true` | No |
91 | | `features.resetPasswords` | `boolean` | `true` | No |
92 | | `features.emailVerification` | `boolean` | `true` | No |
93 | | `features.updateProfileInformation` | `boolean` | `true` | No |
94 | | `features.updatePasswords` | `boolean` | `true` | No |
95 | | `features.twoFactorAuthentication` | `boolean` | `true` | No |
96 | | `tfaRoute` | `endpoint` | `/two-factor-authentication` | No |
97 | | `logLevel` | `number` | `1` | No |
98 | | `origin` | `string` | `http://localhost:3000` | Yes |
99 |
100 | ### 🌐 Endpoints Configuration
101 |
102 | | Endpoint Key | Path | Request Method |
103 | |------------------------------------|--------------------------|-----------------------------|
104 | | `csrf` | `/sanctum/csrf-cookie` | `POST` |
105 | | `login` | `/login` | `POST` |
106 | | `logout` | `/logout` | `POST` |
107 | | `user` | `/user` | `POST` |
108 | | `tfa.enable` | `/user/two-factor-authentication` | `POST` |
109 | | `tfa.disable` | `/user/two-factor-authentication` | `DELETE` |
110 | | `tfa.code` | `/user/two-factor-qr-code` | `GET` |
111 | | `tfa.confirm` | `/user/confirmed-two-factor-authentication` | `POST` |
112 | | `tfa.recoveryCode` | `/user/two-factor-recovery-codes` | `GET` |
113 | | `tfa.challenge` | `/two-factor-challenge` | `POST` |
114 | | `register` | `/register` | `POST` |
115 | | `resetPassword` | `/forgot-password` | `POST` |
116 | | `updatePassword` | `/reset-password` | `POST` |
117 | | `confirmPassword` | `/user/confirm-password` | `POST` |
118 | | `resendEmailVerificationLink` | `/email/verification-notification` | `POST` |
119 |
120 | By following these steps and configurations, you'll have a fully integrated Nuxt application with Laravel Fortify and Sanctum, delivering a robust authentication solution. 🚀
121 |
122 | ## 🤝 Contributing
123 |
124 | We welcome contributions to enhance this module. Here are the steps to contribute:
125 |
126 | 1. **Fork the Repository**: Create a fork of this repository on GitHub.
127 |
128 | 2. **Clone Your Fork**: Clone your forked repository to your local machine.
129 | ```bash
130 | git clone https://github.com/dev-charles15531/nuxt-forify.git
131 | cd nuxt-fortify
132 | ```
133 |
134 | 3. **Create a Branch**: Create a new branch for your feature or bug fix.
135 | ```bash
136 | git checkout -b feature-or-bugfix-name
137 | ```
138 |
139 | 4. **Make Changes**: Implement your feature or bug fix. Ensure your code follows the project's coding standards and passes all tests.
140 |
141 | 5. **Commit Changes**: Commit your changes with a clear and concise commit message.
142 | ```bash
143 | git add .
144 | git commit -m "Description of the feature or fix"
145 | ```
146 |
147 | 6. **Push to Your Fork**: Push your changes to your forked repository.
148 | ```bash
149 | git push origin feature-or-bugfix-name
150 | ```
151 |
152 | 7. **Open a Pull Request**: Open a pull request to the main repository. Provide a clear description of your changes and the problem or feature they address.
153 |
154 | ### 📝 Guidelines
155 |
156 | - Follow the coding style used in the project.
157 | - Write clear, concise commit messages.
158 | - Ensure your code passes all tests and does not introduce new issues.
159 | - Update documentation if your changes affect how the module is used or configured.
160 |
161 | ### 📧 Contact
162 |
163 | If you have any questions or need help, feel free to open an issue or contact the maintainer of this repository.
164 |
165 | Thank you for contributing! Your efforts are greatly appreciated. 🙌
166 |
167 |
168 | [npm-version-src]: https://img.shields.io/npm/v/nuxt-fortify/latest.svg?style=flat&colorA=020420&colorB=00DC82
169 | [npm-version-href]: https://npmjs.com/package/nuxt-fortify
170 |
171 | [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-fortify.svg?style=flat&colorA=020420&colorB=00DC82
172 | [npm-downloads-href]: https://npmjs.com/package/nuxt-fortify
173 |
174 | [license-src]: https://img.shields.io/npm/l/nuxt-fortify.svg?style=flat&colorA=020420&colorB=00DC82
175 | [license-href]: https://npmjs.com/package/nuxt-fortify
176 |
177 | [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js
178 | [nuxt-href]: https://nuxt.com
--------------------------------------------------------------------------------
/src/runtime/plugins/fortifyApi.ts:
--------------------------------------------------------------------------------
1 | import { createConsola, type ConsolaInstance } from 'consola'
2 | import type { FetchOptions, FetchResponse } from 'ofetch'
3 | import { useFortifyIntendedRedirect } from '../composables/useFortifyIntendedRedirect'
4 | import { useFortifyUser } from '../composables/useFortifyUser'
5 | import { useTokenStorage } from '../composables/useTokenStorage'
6 | import type { BaseModuleOptions } from '../types/options'
7 | import {
8 | defineNuxtPlugin,
9 | useRuntimeConfig,
10 | useCookie,
11 | navigateTo,
12 | useRoute,
13 | useRequestURL,
14 | useRequestHeaders,
15 | } from '#app'
16 |
17 | /**
18 | * Creates the default fetch options for the Fortify API.
19 | *
20 | * @param {BaseModuleOptions} config The module configuration.
21 | * @returns {FetchOptions} The default fetch options.
22 | */
23 | function buildFetchOptions(config: BaseModuleOptions): FetchOptions {
24 | /**
25 | * Check if the browser supports the "credentials" option in the Fetch API.
26 | */
27 | const isCredentialsSupported = 'credentials' in Request.prototype
28 |
29 | /**
30 | * The default fetch options.
31 | */
32 | const options: FetchOptions = {
33 | baseURL: config.baseUrl,
34 | redirect: 'manual',
35 | }
36 |
37 | /**
38 | * If the auth mode is set to "cookie", set the credentials mode to "include" if the browser supports it.
39 | */
40 | if (config.authMode === 'cookie') {
41 | options.credentials = isCredentialsSupported ? 'include' : undefined
42 | }
43 |
44 | return options
45 | }
46 |
47 | /**
48 | * Calls the CSRF cookie endpoint to fetch the CSRF token.
49 | *
50 | * @param {BaseModuleOptions} config - The module configuration.
51 | * @param {ConsolaInstance} logger - The logger instance.
52 | * @returns {Promise} - A promise that resolves when the CSRF cookie is called.
53 | */
54 | async function callCsrfCookie(
55 | config: BaseModuleOptions,
56 | logger: ConsolaInstance,
57 | ): Promise {
58 | await $fetch(config.endpoints.csrf, {
59 | baseURL: config.baseUrl,
60 | credentials: 'include',
61 | })
62 |
63 | logger.debug('CSRF cookie has been called')
64 | }
65 |
66 | /**
67 | * Initializes the CSRF header for API requests.
68 | *
69 | * @param {Headers} headers - The headers object.
70 | * @param {BaseModuleOptions} config - The module configuration.
71 | * @param {ConsolaInstance} logger - The logger instance.
72 | * @returns {Promise} - The modified headers with the CSRF token.
73 | */
74 | async function initializeCsrfHeader(
75 | headers: Headers,
76 | config: BaseModuleOptions,
77 | logger: ConsolaInstance,
78 | ): Promise {
79 | let csrfToken = useCookie(config.cookieKey, { readonly: true })
80 |
81 | // If the CSRF token is not present, call the CSRF cookie endpoint to fetch it
82 | if (!csrfToken.value) {
83 | await callCsrfCookie(config, logger)
84 |
85 | csrfToken = useCookie(config.cookieKey, { readonly: true })
86 | }
87 |
88 | // If the CSRF token is still not present, log a warning and return the original headers
89 | if (!csrfToken.value) {
90 | logger.warn(
91 | `${config.cookieKey} cookie is missing, unable to set ${config.cookieHeader} header`,
92 | )
93 |
94 | return headers
95 | }
96 |
97 | logger.debug(`Added API ${config.cookieHeader} header.`)
98 |
99 | // Return the modified headers with the CSRF token
100 | return {
101 | ...headers,
102 | ...(csrfToken.value && {
103 | [config.cookieHeader]: csrfToken.value,
104 | }),
105 | }
106 | }
107 |
108 | /**
109 | * Initializes the token for API requests.
110 | *
111 | * @param {Headers} headers - The headers object.
112 | * @param {BaseModuleOptions} config - The module configuration.
113 | * @param {ConsolaInstance} logger - The logger instance.
114 | * @returns {Promise} - The modified headers with the token.
115 | */
116 | async function initializeToken(
117 | headers: Headers,
118 | config: BaseModuleOptions,
119 | logger: ConsolaInstance,
120 | ): Promise {
121 | const cookieToken = useTokenStorage()
122 | const token = useCookie(config.tokenStorageKey, { secure: true }).value || cookieToken.value
123 |
124 | if (!token) {
125 | logger.warn(`Token not found`)
126 |
127 | return headers
128 | }
129 |
130 | // Return the modified headers with the Authorization header set to the token
131 | return {
132 | ...headers,
133 | Authorization: `Bearer ${token}`,
134 | }
135 | }
136 |
137 | /**
138 | * Builds the request headers based on the configuration, options, and user details.
139 | *
140 | * @param {BaseModuleOptions} config - The module configuration.
141 | * @param {FetchOptions} options - The fetch options.
142 | * @param {ConsolaInstance} logger - The logger instance.
143 | * @returns {Promise} - The built request headers.
144 | */
145 | async function buildRequestHeaders(
146 | config: BaseModuleOptions,
147 | options: FetchOptions,
148 | logger: ConsolaInstance,
149 | ): Promise {
150 | const authMode = config.authMode
151 | const origin = config.origin ?? useRequestURL().origin
152 |
153 | // Default headers with common values
154 | let defaultHeaders: HeadersInit = {
155 | ...options.headers,
156 | Accept: 'application/json',
157 | Referer: origin,
158 | Origin: origin,
159 | }
160 |
161 | if (authMode === 'token') {
162 | // Initialize headers with token-based authentication
163 | return {
164 | ...defaultHeaders,
165 | ...(await initializeToken(defaultHeaders as Headers, config, logger)),
166 | }
167 | }
168 | else if (authMode === 'cookie') {
169 | const clientCookies = useRequestHeaders(['cookie'])
170 |
171 | if (import.meta.server) {
172 | defaultHeaders = {
173 | ...options.headers,
174 | Accept: 'application/json',
175 | Referer: origin,
176 | Origin: origin,
177 | ...(clientCookies.cookie && clientCookies),
178 | }
179 | }
180 | // Initialize headers with CSRF token
181 | return {
182 | ...(await initializeCsrfHeader(
183 | defaultHeaders as Headers,
184 | config,
185 | logger,
186 | )),
187 | }
188 | }
189 |
190 | // Return the default headers if not using secure methods
191 | return defaultHeaders
192 | }
193 |
194 | /**
195 | * Function to return user data after receiving auth response from the server.
196 | *
197 | * @param {BaseModuleOptions} config - The module configuration.
198 | * @param {FetchResponse<{token?: string}>} response - The response from the server.
199 | * @returns {Promise} - The authenticated user's data.
200 | */
201 | async function postAuth(config: BaseModuleOptions, response: FetchResponse<{ token?: string }>) {
202 | let token = ''
203 |
204 | // Set the auth token if auth mode is token
205 | if (config.authMode === 'token') {
206 | if (Object.keys(response._data!).includes('token')) {
207 | token = response._data!.token as string
208 |
209 | const cookieToken = useTokenStorage()
210 | const storedToken = useCookie(config.tokenStorageKey, { secure: true })
211 | storedToken.value = token
212 | cookieToken.value = token
213 | }
214 | else {
215 | token = useCookie(config.tokenStorageKey, { secure: true }).value as string
216 | }
217 | }
218 |
219 | // Initialize authenticated user
220 | const { fetchUser } = useFortifyUser()
221 | const data = await fetchUser(config, token as string)
222 | return data
223 | }
224 |
225 | export default defineNuxtPlugin((_nuxtApp) => {
226 | const config = useRuntimeConfig().public.nuxtFortify as BaseModuleOptions
227 | const { user } = useFortifyUser()
228 | const logger = createConsola({ level: config.logLevel }).withTag(
229 | 'nuxt-fortify',
230 | )
231 |
232 | const $customFetch = $fetch.create({
233 | ...buildFetchOptions(config),
234 |
235 | async onRequest({ options }) {
236 | options.headers = {
237 | ...(await buildRequestHeaders(config, options, logger)),
238 | }
239 | },
240 |
241 | async onResponse({ response }) {
242 | if (response.ok) {
243 | if (import.meta.client) {
244 | const responseUrl = new URL(response.url)
245 | const formattedResponseUrl = `${responseUrl.protocol}//${responseUrl.hostname}${responseUrl.pathname}`
246 |
247 | const configLoginUrl = new URL(config.baseUrl + config.endpoints.login)
248 | const formattedConfiLogingUrl = `${configLoginUrl.protocol}//${configLoginUrl.hostname}${configLoginUrl.pathname}`
249 |
250 | const config2FAChallengeUrl = new URL(config.baseUrl + config.endpoints.tfa?.challenge)
251 | const formattedConfig2FAChallengeUrl = `${config2FAChallengeUrl.protocol}//${config2FAChallengeUrl.hostname}${config2FAChallengeUrl.pathname}`
252 |
253 | if (formattedResponseUrl === formattedConfiLogingUrl) {
254 | if (Object.keys(response._data).includes('two_factor')) {
255 | if (response._data.two_factor == false) {
256 | user.value = await postAuth(config, response)
257 | }
258 | else {
259 | if (config.authMode == 'token') {
260 | user.value = await postAuth(config, response)
261 | }
262 | }
263 | }
264 | }
265 | else if (formattedResponseUrl === formattedConfig2FAChallengeUrl) {
266 | user.value = await postAuth(config, response)
267 | }
268 | }
269 | }
270 | },
271 |
272 | async onResponseError({ response }) {
273 | if (response.status === 419) {
274 | logger.warn('CSRF token mismatch, check your API configuration')
275 | return
276 | }
277 |
278 | if (response.status === 401) {
279 | logger.warn('User session is not set or access token expired')
280 |
281 | if (config.authMode === 'token') {
282 | const oldToken = useCookie(config.tokenStorageKey, { secure: true })
283 | oldToken.value = null
284 | }
285 | if (user.value !== null) {
286 | user.value = null
287 | }
288 |
289 | if (import.meta.client && config.loginRoute) {
290 | const route = useRoute()
291 |
292 | // save current route to be able to redirect to it after login
293 | if (config.intendedRedirect) {
294 | const intendedRoute = useFortifyIntendedRedirect()
295 | intendedRoute.value = route.fullPath
296 | }
297 |
298 | await _nuxtApp.runWithContext(() => {
299 | if (route.path !== config.loginRoute) navigateTo(config.loginRoute)
300 | })
301 | }
302 | }
303 | },
304 | })
305 |
306 | // Expose to useNuxtApp().$fortifyApi
307 | return {
308 | provide: {
309 | fortifyApi: $customFetch,
310 | },
311 | }
312 | })
313 |
--------------------------------------------------------------------------------
/src/runtime/composables/useFortifyFeatures.ts:
--------------------------------------------------------------------------------
1 | import { type Ref, computed, reactive } from 'vue'
2 | import type { FetchResponse } from 'ofetch'
3 | import type { BaseModuleOptions } from '../types/options'
4 | import { useFortifyUser } from './useFortifyUser'
5 | import { useApi } from './useApi'
6 | import { useFortifyIntendedRedirect } from './useFortifyIntendedRedirect'
7 | import {
8 | navigateTo,
9 | useCookie,
10 | useNuxtApp,
11 | useRoute,
12 | useRuntimeConfig,
13 | useTokenStorage,
14 | } from '#imports'
15 |
16 | interface FortifyError {
17 | login: FetchResponse | null
18 | enableTwoFactorAuthentication: FetchResponse | null
19 | getTwoFactorAuthenticationQRCode: FetchResponse | null
20 | confirmTwoFactorAuthentication: FetchResponse | null
21 | showTwoFactorAuthenticationRecoveryCodes: FetchResponse | null
22 | solveTwoFactorAuthenticationChallenge: FetchResponse | null
23 | register: FetchResponse | null
24 | resendEmailVerification: FetchResponse | null
25 | resetPassword: FetchResponse | null
26 | updatePassword: FetchResponse | null
27 | confirmPassword: FetchResponse | null
28 | }
29 |
30 | export interface FortifyFeatures {
31 | isAuth: Ref
32 | error: FortifyError
33 | login: (credentials: Credentials) => Promise
34 | logout: () => Promise
35 | enableTwoFactorAuthentication: () => Promise
36 | getTwoFactorAuthenticationQRCode: () => Promise
37 | confirmTwoFactorAuthentication: (
38 | twoFactorCredentials: TwoFactorCredentials
39 | ) => Promise
40 | showTwoFactorAuthenticationRecoveryCodes: () => Promise>
41 | solveTwoFactorAuthenticationChallenge: (
42 | twoFactorCredentials: TwoFactorCredentials
43 | ) => Promise
44 | disableTwoFactorAuthentication: () => Promise
45 | register: (registrationCredentials: RegistrationCredentials) => Promise
46 | resendEmailVerification: () => Promise
47 | resetPassword: (email: string) => Promise
48 | updatePassword: (credentials: ResetPasswordCredentials) => Promise
49 | confirmPassword: (password: string) => Promise
50 | }
51 |
52 | // Define the login credentials type
53 | interface Credentials {
54 | username: string
55 | password: string
56 | }
57 |
58 | // Define the 2fa credentials type
59 | interface HasCode {
60 | code: number
61 | }
62 | interface HasRecoveryCode {
63 | recovery_code: string
64 | }
65 | type TwoFactorCredentials = HasCode | HasRecoveryCode
66 |
67 | // Define the registeration credentials type
68 | interface RegistrationCredentials {
69 | name: string
70 | password: string
71 | password_confirmation: string
72 | }
73 |
74 | // Define the reset password credentials type
75 | interface ResetPasswordCredentials {
76 | email: string
77 | password: string
78 | password_confirmation: string
79 | token: string
80 | }
81 |
82 | /**
83 | * This function initializes and returns the fortify features.
84 | *
85 | * @returns features.
86 | */
87 | export function useFortifyFeatures(): FortifyFeatures {
88 | const config = useRuntimeConfig().public.nuxtFortify as BaseModuleOptions
89 | const nuxtApp = useNuxtApp()
90 | const api = useApi()
91 | const { user } = useFortifyUser()
92 |
93 | const error: FortifyError = reactive({
94 | login: null,
95 | enableTwoFactorAuthentication: null,
96 | getTwoFactorAuthenticationQRCode: null,
97 | confirmTwoFactorAuthentication: null,
98 | showTwoFactorAuthenticationRecoveryCodes: null,
99 | solveTwoFactorAuthenticationChallenge: null,
100 | register: null,
101 | resendEmailVerification: null,
102 | resetPassword: null,
103 | updatePassword: null,
104 | confirmPassword: null,
105 | })
106 |
107 | // determine if the user is authenticated
108 | const isAuth = computed(() => {
109 | return user.value !== null
110 | })
111 |
112 | /**
113 | * Logs in the user with the provided credentials.
114 | *
115 | * @param credentials - The user credentials.
116 | */
117 | const login = async (credentials: Credentials) => {
118 | error.login = null
119 | const currentRoute = useRoute()
120 |
121 | if (isAuth.value === true) {
122 | if (config.authHome === undefined) {
123 | throw new Error('User is already authenticated')
124 | }
125 | else if (currentRoute.path === config.authHome) {
126 | return
127 | }
128 |
129 | await nuxtApp.runWithContext(() => navigateTo(config.authHome))
130 | return
131 | }
132 |
133 | await api(config.endpoints.login, {
134 | method: 'POST',
135 | body: credentials,
136 | })
137 | .then(async (response) => {
138 | if (response.two_factor) {
139 | // redirect to 2fa page if configured
140 | if (config.tfaRoute) {
141 | return await nuxtApp.runWithContext(() =>
142 | navigateTo(config.tfaRoute),
143 | )
144 | }
145 | else {
146 | console.log('Module tfaRoute is not configured.')
147 | throw new Error('Module tfaRoute is not configured.')
148 | }
149 | }
150 |
151 | if (config.authHome) {
152 | const intendedRoute
153 | = useFortifyIntendedRedirect()
154 | if (config.intendedRedirect && intendedRoute.value) {
155 | return await nuxtApp.runWithContext(() =>
156 | navigateTo(intendedRoute.value),
157 | )
158 | }
159 |
160 | return await nuxtApp.runWithContext(() =>
161 | navigateTo(config.authHome),
162 | )
163 | }
164 | else {
165 | console.log('auth home is not configured')
166 | throw new Error('Module tfaRoute is not configured.')
167 | }
168 | })
169 | .catch(({ response }) => {
170 | console.log(response)
171 | error.login = response
172 | throw new Error('Unable to login.')
173 | })
174 | }
175 |
176 | /**
177 | * Logout authenticated user
178 | *
179 | */
180 | const logout = async () => {
181 | const nuxtApp = useNuxtApp()
182 |
183 | await api(config.endpoints.logout, { method: 'POST' })
184 |
185 | if (config.authMode === 'token') {
186 | // clear token cookie and storage
187 | const cookieToken = useCookie(config.tokenStorageKey, { secure: true })
188 | const storedToken = useTokenStorage()
189 | storedToken.value = null
190 | cookieToken.value = null
191 | }
192 | else if (config.authMode === 'cookie') {
193 | // clear cookie
194 | const cookie = useCookie(config.cookieKey)
195 | cookie.value = null
196 | }
197 |
198 | // auth user state to null
199 | user.value = null
200 |
201 | await nuxtApp.runWithContext(() =>
202 | navigateTo(config.loginRoute),
203 | )
204 |
205 | return
206 | }
207 |
208 | /**
209 | * Enable 2FA for the user
210 | *
211 | * Sends a POST request to enable 2FA for the user
212 | * @see https://laravel.com/docs/11.x/fortify#enabling-two-factor-authentication
213 | *
214 | * @return {Promise}
215 | */
216 | const enableTwoFactorAuthentication = async (): Promise => {
217 | error.enableTwoFactorAuthentication = null
218 |
219 | // Check if the 2FA feature is enabled in the config
220 | if (!config.features?.twoFactorAuthentication) {
221 | throw new Error('2FA feature not enabled. Please enable it from config')
222 | }
223 |
224 | // Check if the 2fa enable endpoint is set in the config
225 | if (!config.endpoints.tfa?.enable) {
226 | throw new Error(
227 | '2FA enable endpoint not set. Please set this endpoint from config',
228 | )
229 | }
230 |
231 | await api(config.endpoints.tfa?.enable, { method: 'POST' }).catch(({ response }) => {
232 | console.log(response)
233 | error.enableTwoFactorAuthentication = response
234 | throw new Error('Caouldn\'t enable 2FA')
235 | })
236 | }
237 |
238 | /**
239 | * Fetches the QR code for two factor authentication setup.
240 | *
241 | * Sends a GET request to the server to fetch the QR code for two factor authentication.
242 | * @see https://laravel.com/docs/11.x/fortify#enabling-two-factor-authentication
243 | *
244 | * @return {Promise} The QR code svg data as a string.
245 | */
246 | const getTwoFactorAuthenticationQRCode = async (): Promise => {
247 | error.getTwoFactorAuthenticationQRCode = null
248 |
249 | // Check if the 2FA feature is enabled in the config
250 | if (!config.features?.twoFactorAuthentication) {
251 | throw new Error('2FA feature not enabled. Please enable it from config')
252 | }
253 |
254 | // Check if the 2fa setup QR code endpoint is set in the config
255 | if (!config.endpoints.tfa?.code) {
256 | throw new Error(
257 | '2FA code endpoint not set. Please set this endpoint from config',
258 | )
259 | }
260 |
261 | let response = null
262 | await api(config.endpoints.tfa?.code, { method: 'GET' }).then((res) => {
263 | response = res
264 |
265 | return
266 | }).catch(({ response }) => {
267 | console.log(response)
268 | error.getTwoFactorAuthenticationQRCode = response
269 |
270 | throw new Error('Unable to retrive 2FA QR code.')
271 | })
272 |
273 | return response
274 | }
275 |
276 | /**
277 | * Fetches the QR code for two factor authentication setup.
278 | *
279 | * Sends a GET request to the server to fetch the QR code for two factor authentication.
280 | * @see https://laravel.com/docs/11.x/fortify#enabling-two-factor-authentication
281 | *
282 | * @return {Promise} The QR code svg data as a string.
283 | */
284 | const confirmTwoFactorAuthentication = async (
285 | twoFactorCredentials: TwoFactorCredentials,
286 | ): Promise => {
287 | error.confirmTwoFactorAuthentication = null
288 |
289 | // Check if the 2FA feature is enabled in the config
290 | if (!config.features?.twoFactorAuthentication) {
291 | throw new Error('2FA feature not enabled. Please enable it from config')
292 | }
293 |
294 | // Check if the 2fa confirm endpoint is set in the config
295 | if (!config.endpoints.tfa?.confirm) {
296 | throw new Error(
297 | '2FA confirm endpoint not set. Please set this endpoint from config',
298 | )
299 | }
300 |
301 | await api(config.endpoints.tfa?.confirm, {
302 | method: 'post',
303 | body: twoFactorCredentials,
304 | }).catch(({ response }) => {
305 | console.log(response)
306 | error.confirmTwoFactorAuthentication = response
307 | throw new Error('Failed to confirm two factor authentication')
308 | })
309 | }
310 |
311 | /**
312 | * Shows the two factor authentication recovery codes.
313 | *
314 | * Fetches the two factor authentication recovery codes from the server.
315 | * @see https://laravel.com/docs/11.x/fortify#displaying-the-recovery-codes
316 | *
317 | * @return {Promise} - A Promise that resolves when the recovery codes are fetched.
318 | */
319 | const showTwoFactorAuthenticationRecoveryCodes = async (): Promise> => {
320 | error.showTwoFactorAuthenticationRecoveryCodes = null
321 |
322 | // Check if the 2FA feature is enabled in the config
323 | if (!config.features?.twoFactorAuthentication) {
324 | throw new Error('2FA feature not enabled. Please enable it from config')
325 | }
326 |
327 | // Check if the 2fa recodery code endpoint is set in the config
328 | if (!config.endpoints.tfa?.recoveryCode) {
329 | throw new Error(
330 | '2FA recovery code endpoint not set. Please set this endpoint from config',
331 | )
332 | }
333 |
334 | let response = null
335 | await api(config.endpoints.tfa?.recoveryCode, { method: 'GET' }).then((res) => {
336 | response = res
337 | return
338 | }).catch(({ response }) => {
339 | console.log(response)
340 | error.showTwoFactorAuthenticationRecoveryCodes = response
341 |
342 | throw new Error('Failed to retrieve two factor authentication recovery codes')
343 | })
344 |
345 | return response
346 | }
347 |
348 | /**
349 | * Solves the two factor authentication challenge.
350 | *
351 | * Sends a POST request to the server to solve the two factor authentication challenge.
352 | * @see https://laravel.com/docs/11.x/fortify#authenticating-with-two-factor-authentication
353 | *
354 | * @param {TwoFactorCredentials} twoFactorCredentials - The two factor authentication credentials.
355 | * @return {Promise} - A Promise that resolves when the challenge is solved.
356 | */
357 | const solveTwoFactorAuthenticationChallenge = async (
358 | twoFactorCredentials: TwoFactorCredentials,
359 | ): Promise => {
360 | error.solveTwoFactorAuthenticationChallenge = null
361 |
362 | // Check if the 2FA feature is enabled in the config
363 | if (!config.features?.twoFactorAuthentication) {
364 | throw new Error('2FA feature not enabled. Please enable it from config')
365 | }
366 |
367 | // Check if the 2fa challenge endpoint is set in the config
368 | if (!config.endpoints.tfa?.challenge) {
369 | throw new Error(
370 | '2FA challenge endpoint not set. Please set this endpoint from config',
371 | )
372 | }
373 |
374 | await api(config.endpoints.tfa?.challenge, {
375 | method: 'POST',
376 | body: twoFactorCredentials,
377 | }).catch(({ response }) => {
378 | console.log(response)
379 | error.solveTwoFactorAuthenticationChallenge = response
380 | throw new Error('Unable to solve 2FA challenge')
381 | })
382 | }
383 |
384 | /**
385 | * Disables two-factor authentication.
386 | *
387 | * Sends a DELETE request to the server to disable two-factor authentication.
388 | * @see https://laravel.com/docs/11.x/fortify#disabling-two-factor-authentication
389 | *
390 | * @return {Promise} - A Promise that resolves when two-factor authentication is disabled.
391 | */
392 | const disableTwoFactorAuthentication = async (): Promise => {
393 | // Check if the 2FA feature is enabled in the config
394 | if (!config.features?.twoFactorAuthentication) {
395 | throw new Error('2FA feature not enabled. Please enable it from config')
396 | }
397 |
398 | // Check if the 2fa disable endpoint is set in the config
399 | if (!config.endpoints.tfa?.disable) {
400 | throw new Error(
401 | '2FA disable endpoint not set. Please set this endpoint from config',
402 | )
403 | }
404 |
405 | try {
406 | await api(config.endpoints.tfa?.disable, { method: 'DELETE' })
407 | }
408 | catch (error) {
409 | console.log(error)
410 | throw new Error('couldn\'t disable 2FA feature.')
411 | }
412 | }
413 |
414 | /**
415 | * Registers a new user.
416 | *
417 | * Sends a POST request to the server to register a new user.
418 | *
419 | * @param {RegistrationCredentials} registrationCredentials - The registration credentials.
420 | * @throws {Error} If the registration feature is not enabled in the config, or
421 | * if the registration endpoint is not set in the config.
422 | * @see https://laravel.com/docs/11.x/fortify#registration
423 | * @return {Promise} - A Promise that resolves when the user is registered.
424 | */
425 | const register = async (
426 | registrationCredentials: RegistrationCredentials,
427 | ): Promise => {
428 | error.register = null
429 |
430 | // Check if the registration feature is enabled in the config
431 | if (!config.features?.registration) {
432 | throw new Error(
433 | 'Registration feature not enabled. Please enable it from config',
434 | )
435 | }
436 |
437 | // Check if the register endpoint is set in the config
438 | if (!config.endpoints?.register) {
439 | throw new Error(
440 | 'Register endpoint not set. Please set this endpoint from config',
441 | )
442 | }
443 |
444 | // Send a POST request to the server to register a new user
445 | await api(config.endpoints.register, {
446 | method: 'POST',
447 | body: registrationCredentials,
448 | }).catch(({ response }) => {
449 | console.log(response)
450 | error.register = response
451 |
452 | throw new Error('couldn\'t complete registration.')
453 | })
454 | }
455 |
456 | /**
457 | * Resends the email verification link to the user.
458 | *
459 | * @throws {Error} If the email verification feature is not enabled in the config, or
460 | * if the resend email verification link endpoint is not set in the config.
461 | * @see https://laravel.com/docs/11.x/fortify#resending-email-verification-links
462 | * @return {Promise} A Promise that resolves when the request is successfully sent.
463 | */
464 | const resendEmailVerification = async (): Promise => {
465 | error.resendEmailVerification = null
466 |
467 | // Check if the email verification feature is enabled in the config
468 | if (!config.features?.emailVerification) {
469 | throw new Error(
470 | 'Email verification feature not enabled. Please enable it from config',
471 | )
472 | }
473 |
474 | // Check if the "resend email verification link" endpoint is set in the config
475 | if (!config.endpoints?.resendEmailVerificationLink) {
476 | throw new Error(
477 | 'The resend email verification link endpoint not set. Please set this endpoint from config',
478 | )
479 | }
480 |
481 | await api(
482 | config.endpoints.resendEmailVerificationLink,
483 | {
484 | method: 'POST',
485 | },
486 | ).catch(({ response }) => {
487 | console.log(response)
488 | error.resendEmailVerification = response
489 |
490 | throw new Error('Unable to resend email verification link.')
491 | })
492 | }
493 |
494 | /**
495 | * Sends a POST request to the server to initiate the password reset process.
496 | *
497 | * @param {string} email - The email address of the user whose password is to be reset.
498 | * @throws {Error} If the reset password feature is not enabled in the config, or
499 | * if the reset password endpoint is not set in the config.
500 | * @see https://laravel.com/docs/11.x/fortify#requesting-a-password-reset-link
501 | * @return {Promise} A Promise that resolves when the request is successfully sent.
502 | */
503 | const resetPassword = async (email: string): Promise => {
504 | error.resetPassword = null
505 |
506 | // Check if the reset password feature is enabled in the config
507 | if (!config.features?.resetPasswords) {
508 | throw new Error(
509 | 'Reset password feature not enabled. Please enable it from config',
510 | )
511 | }
512 |
513 | // Check if the reset password endpoint is set in the config
514 | if (!config.endpoints?.resetPassword) {
515 | throw new Error(
516 | 'Reset password endpoint not set. Please set this endpoint from config',
517 | )
518 | }
519 |
520 | await api(config.endpoints.resetPassword, {
521 | method: 'POST',
522 | body: { email: email },
523 | }).catch(({ response }) => {
524 | console.log(response)
525 | error.resetPassword = response
526 |
527 | throw new Error('Unable to process reset password.')
528 | })
529 | }
530 |
531 | /**
532 | * Sends a POST request to the server to update the user's password.
533 | *
534 | * @param {ResetPasswordCredentials} resetPasswordCredentials - The new password and its confirmation,
535 | * as well as a token that contains the value of request()->route('token').
536 | * @see https://laravel.com/docs/11.x/fortify#resetting-the-password
537 | * @throws {Error} If the update password feature is not enabled in the config, or
538 | * if the update password endpoint is not set in the config.
539 | * @return {Promise} A Promise that resolves when the request is successfully sent.
540 | */
541 | const updatePassword = async (
542 | resetPasswordCredentials: ResetPasswordCredentials,
543 | ): Promise => {
544 | error.updatePassword = null
545 |
546 | // Check if the update password feature is enabled in the config
547 | if (!config.features?.updatePasswords) {
548 | throw new Error(
549 | 'Update password feature not enabled. Please enable it from config',
550 | )
551 | }
552 |
553 | // Check if the update password endpoint is set in the config
554 | if (!config.endpoints?.updatePassword) {
555 | throw new Error(
556 | 'Update password endpoint not set. Please set this endpoint from config',
557 | )
558 | }
559 |
560 | await api(config.endpoints.updatePassword, {
561 | method: 'POST',
562 | body: resetPasswordCredentials,
563 | }).catch(({ response }) => {
564 | console.log(response)
565 | error.updatePassword = response
566 |
567 | throw new Error('Unable to process password update.')
568 | })
569 | }
570 |
571 | /**
572 | * Confirms the user's password.
573 | *
574 | * @param password - The user's password to confirm.
575 | * @throws {Error} If the confirm password endpoint is not set in the config.
576 | * @see https://laravel.com/docs/11.x/fortify#password-confirmation
577 | * @return {Promise} A Promise that resolves when the request is successfully sent.
578 | */
579 | const confirmPassword = async (password: string): Promise => {
580 | error.confirmPassword = null
581 |
582 | // Check if the confirm password endpoint is set in the config
583 | if (!config.endpoints?.confirmPassword) {
584 | throw new Error(
585 | 'Confirm password endpoint not set. Please set this endpoint from config',
586 | )
587 | }
588 |
589 | await api(config.endpoints.confirmPassword as RequestInfo, {
590 | method: 'POST',
591 | body: { password: password },
592 | }).catch(({ response }) => {
593 | console.log(response)
594 | error.confirmPassword = response
595 |
596 | throw new Error('Unable to process password confirmation.')
597 | })
598 | }
599 |
600 | return {
601 | isAuth,
602 | error,
603 | login,
604 | logout,
605 | enableTwoFactorAuthentication,
606 | getTwoFactorAuthenticationQRCode,
607 | confirmTwoFactorAuthentication,
608 | showTwoFactorAuthenticationRecoveryCodes,
609 | solveTwoFactorAuthenticationChallenge,
610 | disableTwoFactorAuthentication,
611 | register,
612 | resendEmailVerification,
613 | resetPassword,
614 | updatePassword,
615 | confirmPassword,
616 | }
617 | }
618 |
--------------------------------------------------------------------------------