├── .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 | 6 | -------------------------------------------------------------------------------- /test/fixtures/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "basic", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/basic/app.vue: -------------------------------------------------------------------------------- 1 | 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 | 51 | 52 | 77 | -------------------------------------------------------------------------------- /playground/pages/two-factor-authentication.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 71 | 72 | 104 | -------------------------------------------------------------------------------- /playground/pages/register.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | --------------------------------------------------------------------------------