├── .editorconfig ├── .gitattributes ├── .gitignore ├── .npmrc ├── .nuxtrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .vitepress │ └── config.ts ├── configuration │ ├── email.md │ ├── oauth.md │ ├── redirection.md │ ├── registration.md │ └── tokens.md ├── get-started │ ├── adapters.md │ ├── data-integration.md │ └── introduction.md ├── index.md ├── public │ ├── logo.png │ └── logo_original.png ├── upgrades │ └── v3.md └── usage │ ├── composables.md │ ├── hooks.md │ ├── middleware.md │ └── utils.md ├── eslint.config.js ├── package.json ├── playground ├── .gitignore ├── .npmrc ├── app.vue ├── index.d.ts ├── nuxt.config.ts ├── package.json ├── pages │ ├── auth │ │ ├── callback.vue │ │ ├── email-verify.vue │ │ ├── login.vue │ │ ├── password-reset.vue │ │ └── register.vue │ ├── home.vue │ └── index.vue ├── plugins │ └── auth.ts ├── prisma │ └── schema.prisma └── server │ └── plugins │ ├── hooks.ts │ ├── prisma.ts │ └── unstorage.ts ├── playwright.config.ts ├── pnpm-lock.yaml ├── src ├── module.ts ├── runtime │ ├── composables │ │ ├── useAuth.ts │ │ ├── useAuthSession.ts │ │ └── useAuthToken.ts │ ├── middleware │ │ ├── auth.ts │ │ ├── common.ts │ │ └── guest.ts │ ├── plugins │ │ ├── flow.ts │ │ └── provider.ts │ ├── server │ │ ├── api │ │ │ ├── avatar.get.ts │ │ │ ├── email │ │ │ │ ├── request.post.ts │ │ │ │ └── verify.get.ts │ │ │ ├── login │ │ │ │ ├── [provider].get.ts │ │ │ │ ├── [provider] │ │ │ │ │ └── callback.get.ts │ │ │ │ └── index.post.ts │ │ │ ├── logout.post.ts │ │ │ ├── me.get.ts │ │ │ ├── password │ │ │ │ ├── change.put.ts │ │ │ │ ├── request.post.ts │ │ │ │ └── reset.put.ts │ │ │ ├── register.post.ts │ │ │ └── sessions │ │ │ │ ├── [id].delete.ts │ │ │ │ ├── index.delete.ts │ │ │ │ ├── index.get.ts │ │ │ │ └── refresh.post.ts │ │ └── utils │ │ │ ├── adapter │ │ │ ├── prisma.ts │ │ │ ├── unstorage.ts │ │ │ └── utils.ts │ │ │ ├── avatar.ts │ │ │ ├── bcrypt.ts │ │ │ ├── config.ts │ │ │ ├── context.ts │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ ├── mail.ts │ │ │ ├── mustache.ts │ │ │ ├── token │ │ │ ├── accessToken.ts │ │ │ ├── emailVerify.ts │ │ │ ├── fingerprint.ts │ │ │ ├── jwt.ts │ │ │ ├── passwordReset.ts │ │ │ └── refreshToken.ts │ │ │ └── user.ts │ ├── templates │ │ ├── email_verification.html │ │ └── password_reset.html │ └── types │ │ ├── adapter.d.ts │ │ ├── common.d.ts │ │ └── config.d.ts ├── setup_backend.ts ├── setup_frontend.ts └── utils.ts ├── tests ├── basic.spec.ts ├── logout.teardown.ts ├── register.setup.ts └── utils │ └── index.ts └── tsconfig.json /.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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 | .vercel_build_output 23 | .build-* 24 | .env 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 | 58 | bg-dev-nuxt-auth-0.0.0-development.tgz 59 | package 60 | playground/prisma/migrations 61 | /test-results/ 62 | /playwright-report/ 63 | /blob-report/ 64 | /playwright/.cache/ 65 | playwright-report/ 66 | test-results/ 67 | docs/.vitepress/dist 68 | docs/.vitepress/cache -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | shamefully-hoist=true 3 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | imports.autoImport=false 2 | typescript.includeWorkspace=true -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v3.0.3 4 | 5 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v3.0.2...v3.0.3) 6 | 7 | ### 📖 Documentation 8 | 9 | - Correct link of the used bcrypt pkg ([48b76a3](https://github.com/becem-gharbi/nuxt-auth/commit/48b76a3)) 10 | 11 | ### 🌊 Types 12 | 13 | - Import `NitroApp` from `nitropack/types` ([77d7020](https://github.com/becem-gharbi/nuxt-auth/commit/77d7020)) 14 | 15 | ### 🏡 Chore 16 | 17 | - Add `ufo` as peer dependency ([61eb176](https://github.com/becem-gharbi/nuxt-auth/commit/61eb176)) 18 | - Add `unstorage` as peer dependency ([3ec2e3b](https://github.com/becem-gharbi/nuxt-auth/commit/3ec2e3b)) 19 | - **lint:** Fix ([9b7443b](https://github.com/becem-gharbi/nuxt-auth/commit/9b7443b)) 20 | 21 | ### ❤️ Contributors 22 | 23 | - Becem-gharbi 24 | - Becem 25 | 26 | ## v3.0.2 27 | 28 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v3.0.0...v3.0.2) 29 | 30 | ### 🔥 Performance 31 | 32 | - Migrate from `bcryptjs` to `bcrypt-edge` ([8e12b80](https://github.com/becem-gharbi/nuxt-auth/commit/8e12b80)) 33 | 34 | ### 🩹 Fixes 35 | 36 | - Avoid params sanitization on login redirection ([72113e7](https://github.com/becem-gharbi/nuxt-auth/commit/72113e7)) 37 | - Automatically revoke expired sessions ([6be46b1](https://github.com/becem-gharbi/nuxt-auth/commit/6be46b1)) 38 | 39 | ### 🏡 Chore 40 | 41 | - **release:** V3.0.1 ([5e510e7](https://github.com/becem-gharbi/nuxt-auth/commit/5e510e7)) 42 | - **package.json:** Update packageManager ([c1d164c](https://github.com/becem-gharbi/nuxt-auth/commit/c1d164c)) 43 | - **playground:** Use memory storage with unstorage ([f8458e3](https://github.com/becem-gharbi/nuxt-auth/commit/f8458e3)) 44 | 45 | ### ❤️ Contributors 46 | 47 | - Becem-gharbi 48 | 49 | ## v3.0.1 50 | 51 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v3.0.0...v3.0.1) 52 | 53 | ### 🩹 Fixes 54 | 55 | - Avoid params sanitization on login redirection ([72113e7](https://github.com/becem-gharbi/nuxt-auth/commit/72113e7)) 56 | - Automatically revoke expired sessions ([6be46b1](https://github.com/becem-gharbi/nuxt-auth/commit/6be46b1)) 57 | 58 | ### ❤️ Contributors 59 | 60 | - Becem-gharbi 61 | 62 | ## v3.0.0 63 | 64 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v3.0.4-rc...v3.0.0) 65 | 66 | ### 🚀 Enhancements 67 | 68 | - **oauth:** Allow adding custom query params on authorization request ([#60](https://github.com/becem-gharbi/nuxt-auth/pull/60)) 69 | - Allow compatibility with `nuxt` v4 ([8a73aa1](https://github.com/becem-gharbi/nuxt-auth/commit/8a73aa1)) 70 | 71 | ### 🩹 Fixes 72 | 73 | - Correctly import from `nitropack` ([eb8965d](https://github.com/becem-gharbi/nuxt-auth/commit/eb8965d)) 74 | 75 | ### 💅 Refactors 76 | 77 | - No significant change ([9c93a5b](https://github.com/becem-gharbi/nuxt-auth/commit/9c93a5b)) 78 | 79 | ### 🏡 Chore 80 | 81 | - Remove `rc` suffix ([04b6964](https://github.com/becem-gharbi/nuxt-auth/commit/04b6964)) 82 | 83 | ### ❤️ Contributors 84 | 85 | - Becem-gharbi 86 | - Becem 87 | 88 | ## v3.0.4-rc 89 | 90 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v3.0.3-rc...v3.0.4-rc) 91 | 92 | ### 🏡 Chore 93 | 94 | - Refresh lockfile ([8a98cd1](https://github.com/becem-gharbi/nuxt-auth/commit/8a98cd1)) 95 | 96 | ### ❤️ Contributors 97 | 98 | - Becem-gharbi 99 | 100 | ## v3.0.3-rc 101 | 102 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v3.0.2-rc...v3.0.3-rc) 103 | 104 | ### 💅 Refactors 105 | 106 | - **useAuthSession:** Avoid access to private runtimeConfig ([1b8a318](https://github.com/becem-gharbi/nuxt-auth/commit/1b8a318)) 107 | - No significant change ([a27d601](https://github.com/becem-gharbi/nuxt-auth/commit/a27d601)) 108 | 109 | ### 📖 Documentation 110 | 111 | - Migrate to `vitepress` ([#56](https://github.com/becem-gharbi/nuxt-auth/pull/56)) 112 | - Fix typo ([2180eec](https://github.com/becem-gharbi/nuxt-auth/commit/2180eec)) 113 | - Fix selection of feature item ([06dbe3e](https://github.com/becem-gharbi/nuxt-auth/commit/06dbe3e)) 114 | 115 | ### ❤️ Contributors 116 | 117 | - Becem-gharbi 118 | - Becem 119 | 120 | ## v3.0.2-rc 121 | 122 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v3.0.1-rc...v3.0.2-rc) 123 | 124 | ### 🩹 Fixes 125 | 126 | - Solve potential infinite redirections on Vercel ([#54](https://github.com/becem-gharbi/nuxt-auth/pull/54)) 127 | - Prioritize `guest` middleware over global `auth` middleware ([bde3091](https://github.com/becem-gharbi/nuxt-auth/commit/bde3091)) 128 | 129 | ### 💅 Refactors 130 | 131 | - **unstorage:** Rename `token` to `session` ([9d8f649](https://github.com/becem-gharbi/nuxt-auth/commit/9d8f649)) 132 | - **config:** Make `registration.defaultRole` optional ([fb9013d](https://github.com/becem-gharbi/nuxt-auth/commit/fb9013d)) 133 | 134 | ### 📖 Documentation 135 | 136 | - Update README ([5528a33](https://github.com/becem-gharbi/nuxt-auth/commit/5528a33)) 137 | 138 | ### ❤️ Contributors 139 | 140 | - Becem-gharbi 141 | - Becem 142 | 143 | ## v3.0.1-rc 144 | 145 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v3.0.0-beta.0...v3.0.1-rc) 146 | 147 | ### 🏡 Chore 148 | 149 | - **release:** V3.0.0-rc ([5875141](https://github.com/becem-gharbi/nuxt-auth/commit/5875141)) 150 | 151 | ### ❤️ Contributors 152 | 153 | - Becem-gharbi 154 | 155 | ## v3.0.0-rc 156 | 157 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v3.0.0-beta.0...v3.0.0-rc) 158 | 159 | ## vv3.0.0-beta.5 160 | 161 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/70e293339fa43dcedde5dd235cd1b5dbea80fc22...vv3.0.0-beta.5) 162 | 163 | ### 🩹 Fixes 164 | 165 | - Allow overwriting adapter Source ([#47](https://github.com/becem-gharbi/nuxt-auth/pull/47)) 166 | 167 | ### 💅 Refactors 168 | 169 | - Rename adapter Options to Source ([5780b45](https://github.com/becem-gharbi/nuxt-auth/commit/5780b45)) 170 | - ⚠️ Change event.context definition ([#45](https://github.com/becem-gharbi/nuxt-auth/pull/45)) 171 | - ⚠️ Rename `#auth` to `#auth_utils` ([d9d1bcc](https://github.com/becem-gharbi/nuxt-auth/commit/d9d1bcc)) 172 | - ⚠️ Change path of session endpoints ([#48](https://github.com/becem-gharbi/nuxt-auth/pull/48)) 173 | - Add max length validation for email & name ([ff5b4ad](https://github.com/becem-gharbi/nuxt-auth/commit/ff5b4ad)) 174 | 175 | ### 🌊 Types 176 | 177 | - Fix utils types & refactor ([b8412df](https://github.com/becem-gharbi/nuxt-auth/commit/b8412df)) 178 | - Rename `#build/types/auth_adapter` to `#auth_adapter` ([#46](https://github.com/becem-gharbi/nuxt-auth/pull/46)) 179 | 180 | #### ⚠️ Breaking Changes 181 | 182 | - ⚠️ Change event.context definition ([#45](https://github.com/becem-gharbi/nuxt-auth/pull/45)) 183 | - ⚠️ Rename `#auth` to `#auth_utils` ([d9d1bcc](https://github.com/becem-gharbi/nuxt-auth/commit/d9d1bcc)) 184 | - ⚠️ Change path of session endpoints ([#48](https://github.com/becem-gharbi/nuxt-auth/pull/48)) 185 | 186 | ### ❤️ Contributors 187 | 188 | - Becem-gharbi 189 | - Becem 190 | 191 | ## v3.0.0-beta.4 192 | 193 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/d3967fe2aa32156e3dfd2dad21414fecd835ac8d...vv3.0.0-beta.4) 194 | 195 | ### 🚀 Enhancements 196 | 197 | - Add Email action timeout ([#43](https://github.com/becem-gharbi/nuxt-auth/pull/43)) 198 | 199 | ### 🔥 Performance 200 | 201 | - Skip refresh token verification when undefined ([5bf7521](https://github.com/becem-gharbi/nuxt-auth/commit/5bf7521)) 202 | 203 | ### 🩹 Fixes 204 | 205 | - Reset `requestedPasswordReset` to false on login ([1deca27](https://github.com/becem-gharbi/nuxt-auth/commit/1deca27)) 206 | - Allow overwriting ID type ([#44](https://github.com/becem-gharbi/nuxt-auth/pull/44)) 207 | 208 | ### 💅 Refactors 209 | 210 | - Code review ([b5739ad](https://github.com/becem-gharbi/nuxt-auth/commit/b5739ad)) 211 | - Rename filename of `#auth` type definition ([8b636a3](https://github.com/becem-gharbi/nuxt-auth/commit/8b636a3)) 212 | 213 | ### 📖 Documentation 214 | 215 | - Update JSDOC of composables ([#42](https://github.com/becem-gharbi/nuxt-auth/pull/42)) 216 | 217 | ### 🏡 Chore 218 | 219 | - Change setup files location ([a3c2514](https://github.com/becem-gharbi/nuxt-auth/commit/a3c2514)) 220 | 221 | ### ✅ Tests 222 | 223 | - Update basic ([17a8f35](https://github.com/becem-gharbi/nuxt-auth/commit/17a8f35)) 224 | 225 | ### ❤️ Contributors 226 | 227 | - Becem 228 | 229 | ## v3.0.0-beta.3 230 | 231 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/vv3.0.0-beta.2...vv3.0.0-beta.3) 232 | 233 | ### 🚀 Enhancements 234 | 235 | - Add `auth:fetchError` hook ([ab89ac9](https://github.com/becem-gharbi/nuxt-auth/commit/ab89ac9)) 236 | - Feat: add `emailValidationRegex` for email validation on registration ([#37](https://github.com/becem-gharbi/nuxt-auth/pull/37)) 237 | - Add `prisma` adapter ([#38](https://github.com/becem-gharbi/nuxt-auth/pull/38)) 238 | - Add `unstorage` adapter ([#39](https://github.com/becem-gharbi/nuxt-auth/pull/39)) 239 | - Allow augmenting adapter types e.g User ([916ab82](https://github.com/becem-gharbi/nuxt-auth/commit/916ab82)) 240 | 241 | ### 🩹 Fixes 242 | 243 | - Disallow token refresh when account not verified ([5672407](https://github.com/becem-gharbi/nuxt-auth/commit/5672407)) 244 | 245 | ### 💅 Refactors 246 | 247 | - Ensure auth refresh flow runs at the end ([#36](https://github.com/becem-gharbi/nuxt-auth/pull/36)) 248 | 249 | ### 🌊 Types 250 | 251 | - Define types of route middlewares `auth` and `guest` ([#35](https://github.com/becem-gharbi/nuxt-auth/pull/35)) 252 | - Add known oauth options for `google` and `github` ([06b9f82](https://github.com/becem-gharbi/nuxt-auth/commit/06b9f82)) 253 | - Resolve `provider` from `User` ([68a357f](https://github.com/becem-gharbi/nuxt-auth/commit/68a357f)) 254 | 255 | ### 🏡 Chore 256 | 257 | - ⚠️ Do not convert `createdAt` `updatedAt` to Date on user state ([82fc63c](https://github.com/becem-gharbi/nuxt-auth/commit/82fc63c)) 258 | - **playground:** Add adapter selection ([580b821](https://github.com/becem-gharbi/nuxt-auth/commit/580b821)) 259 | - **playground:** Avoid editing runtime config ([b48e24c](https://github.com/becem-gharbi/nuxt-auth/commit/b48e24c)) 260 | 261 | #### ⚠️ Breaking Changes 262 | 263 | - ⚠️ Do not convert `createdAt` `updatedAt` to Date on user state ([82fc63c](https://github.com/becem-gharbi/nuxt-auth/commit/82fc63c)) 264 | 265 | ### ❤️ Contributors 266 | 267 | - Becem 268 | 269 | ## v3.0.0-beta.2 270 | 271 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/ce6e049f69c74db2251dc4040f44c4ab4914bafe...vv3.0.0-beta.2) 272 | 273 | ### 🚀 Enhancements 274 | 275 | - Add `provider` to access token payload ([#34](https://github.com/becem-gharbi/nuxt-auth/pull/34)) 276 | 277 | ### 🔥 Performance 278 | 279 | - ⚠️ Avoid registration of server handlers when respective configuration missing ([#33](https://github.com/becem-gharbi/nuxt-auth/pull/33)) 280 | 281 | ### 🩹 Fixes 282 | 283 | - Convert param id to number if possible ([9a88165](https://github.com/becem-gharbi/nuxt-auth/commit/9a88165)) 284 | - Assign role `default` on registration with credentials ([65a9813](https://github.com/becem-gharbi/nuxt-auth/commit/65a9813)) 285 | 286 | ### 💅 Refactors 287 | 288 | - ⚠️ Change server error messages ([#32](https://github.com/becem-gharbi/nuxt-auth/pull/32)) 289 | 290 | ### 🌊 Types 291 | 292 | - Set `accessToken.customClaims` values to `unknown` ([242be21](https://github.com/becem-gharbi/nuxt-auth/commit/242be21)) 293 | 294 | ### 🏡 Chore 295 | 296 | - **playground:** Pass prisma client to event context ([56e8604](https://github.com/becem-gharbi/nuxt-auth/commit/56e8604)) 297 | - Sync changelog ([e474411](https://github.com/becem-gharbi/nuxt-auth/commit/e474411)) 298 | 299 | ### ✅ Tests 300 | 301 | - Add render user avatar test ([ad503b1](https://github.com/becem-gharbi/nuxt-auth/commit/ad503b1)) 302 | - Add request password reset test ([454ec0e](https://github.com/becem-gharbi/nuxt-auth/commit/454ec0e)) 303 | 304 | #### ⚠️ Breaking Changes 305 | 306 | - ⚠️ Avoid registration of server handlers when respective configuration missing ([#33](https://github.com/becem-gharbi/nuxt-auth/pull/33)) 307 | - ⚠️ Change server error messages ([#32](https://github.com/becem-gharbi/nuxt-auth/pull/32)) 308 | 309 | ### ❤️ Contributors 310 | 311 | - Becem-gharbi 312 | - Becem 313 | 314 | ## v3.0.0-beta.1 315 | 316 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v3.0.0-beta...v3) 317 | 318 | ### 🚀 Enhancements 319 | 320 | - ⚠️ Allow usage of custom data layer ([#30](https://github.com/becem-gharbi/nuxt-auth/pull/30)) 321 | 322 | ### 🩹 Fixes 323 | 324 | - Avoid delete of non-existant refresh token ([#31](https://github.com/becem-gharbi/nuxt-auth/pull/31)) 325 | 326 | ### 💅 Refactors 327 | 328 | - Change findUser to findUserById and findUserByEmail ([7ce97d6](https://github.com/becem-gharbi/nuxt-auth/commit/7ce97d6)) 329 | 330 | ### 🏡 Chore 331 | 332 | - Resolve import of nitro utils ([8f98519](https://github.com/becem-gharbi/nuxt-auth/commit/8f98519)) 333 | - **playground:** Change email provider to hook ([028697f](https://github.com/becem-gharbi/nuxt-auth/commit/028697f)) 334 | 335 | #### ⚠️ Breaking Changes 336 | 337 | - ⚠️ Allow usage of custom data layer ([#30](https://github.com/becem-gharbi/nuxt-auth/pull/30)) 338 | 339 | ### ❤️ Contributors 340 | 341 | - Becem 342 | 343 | ## v3.0.0-beta 344 | 345 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.6.0...v3.0.0-beta) 346 | 347 | ### 📖 Documentation 348 | 349 | - Update 4.email.md ([aecf78f](https://github.com/becem-gharbi/nuxt-auth/commit/aecf78f)) 350 | 351 | ### 🏡 Chore 352 | 353 | - **lint:** Migrate to `@nuxt/eslint-config` ([67a2dcb](https://github.com/becem-gharbi/nuxt-auth/commit/67a2dcb)) 354 | - **lint:** Fix issues ([687a7b4](https://github.com/becem-gharbi/nuxt-auth/commit/687a7b4)) 355 | - Hide node deprecation warnings on build ([3c7d27a](https://github.com/becem-gharbi/nuxt-auth/commit/3c7d27a)) 356 | - ⚠️ Remove `useAuthFetch` ([#24](https://github.com/becem-gharbi/nuxt-auth/pull/24)) 357 | - ⚠️ Remove internal prisma instantiation ([#25](https://github.com/becem-gharbi/nuxt-auth/pull/25)) 358 | - ⚠️ Remove Custom email provider ([#26](https://github.com/becem-gharbi/nuxt-auth/pull/26)) 359 | - **playground:** Remove deprecated config options ([38c59b3](https://github.com/becem-gharbi/nuxt-auth/commit/38c59b3)) 360 | - ⚠️ Remove purge of expired sessions ([#27](https://github.com/becem-gharbi/nuxt-auth/pull/27)) 361 | - ⚠️ Rename `registration.enable` to `registration.enabled` ([#28](https://github.com/becem-gharbi/nuxt-auth/pull/28)) 362 | - ⚠️ Only except `.html` custom email templates ([#29](https://github.com/becem-gharbi/nuxt-auth/pull/29)) 363 | - Change password reset and email verification token's secrets ([9125a3f](https://github.com/becem-gharbi/nuxt-auth/commit/9125a3f)) 364 | - Resolve `@typescript-eslint/ban-ts-comment` overrides ([bf0ab12](https://github.com/becem-gharbi/nuxt-auth/commit/bf0ab12)) 365 | 366 | ### ✅ Tests 367 | 368 | - Add basic tests ([98c3e3a](https://github.com/becem-gharbi/nuxt-auth/commit/98c3e3a)) 369 | 370 | #### ⚠️ Breaking Changes 371 | 372 | - ⚠️ Remove `useAuthFetch` ([#24](https://github.com/becem-gharbi/nuxt-auth/pull/24)) 373 | - ⚠️ Remove internal prisma instantiation ([#25](https://github.com/becem-gharbi/nuxt-auth/pull/25)) 374 | - ⚠️ Remove Custom email provider ([#26](https://github.com/becem-gharbi/nuxt-auth/pull/26)) 375 | - ⚠️ Remove purge of expired sessions ([#27](https://github.com/becem-gharbi/nuxt-auth/pull/27)) 376 | - ⚠️ Rename `registration.enable` to `registration.enabled` ([#28](https://github.com/becem-gharbi/nuxt-auth/pull/28)) 377 | - ⚠️ Only except `.html` custom email templates ([#29](https://github.com/becem-gharbi/nuxt-auth/pull/29)) 378 | 379 | ### ❤️ Contributors 380 | 381 | - Becem-gharbi 382 | - Becem 383 | 384 | ## v2.6.0 385 | 386 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.5.1...v2.6.0) 387 | 388 | ### 🚀 Enhancements 389 | 390 | - Support adding custom email templates via relative path ([#20](https://github.com/becem-gharbi/nuxt-auth/pull/20)) 391 | 392 | ### 🔥 Performance 393 | 394 | - Replace `uuid` with `crypto.randomUUID` ([b045232](https://github.com/becem-gharbi/nuxt-auth/commit/b045232)) 395 | 396 | ### 🩹 Fixes 397 | 398 | - **registration:** Inform user when account not verified ([#21](https://github.com/becem-gharbi/nuxt-auth/pull/21)) 399 | 400 | ### 💅 Refactors 401 | 402 | - No significant change ([ac16309](https://github.com/becem-gharbi/nuxt-auth/commit/ac16309)) 403 | 404 | ### 📖 Documentation 405 | 406 | - Mention new starter ([a8a4a47](https://github.com/becem-gharbi/nuxt-auth/commit/a8a4a47)) 407 | - Remove Nuxt version specification ([96fbae0](https://github.com/becem-gharbi/nuxt-auth/commit/96fbae0)) 408 | 409 | ### ❤️ Contributors 410 | 411 | - Becem-gharbi 412 | - Becem 413 | 414 | ## v2.5.1 415 | 416 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.5.0...v2.5.1) 417 | 418 | ### 🔥 Performance 419 | 420 | - Replace `mustache` with lightweight internal utility ([50041c3](https://github.com/becem-gharbi/nuxt-auth/commit/50041c3)) 421 | - Minify default email templates ([6f3f7e2](https://github.com/becem-gharbi/nuxt-auth/commit/6f3f7e2)) 422 | 423 | ### 🏡 Chore 424 | 425 | - **playground:** Use sqlite instead of mongo db ([79b72c0](https://github.com/becem-gharbi/nuxt-auth/commit/79b72c0)) 426 | 427 | ### ❤️ Contributors 428 | 429 | - Becem-gharbi ([@becem-gharbi](http://github.com/becem-gharbi)) 430 | 431 | ## v2.5.0 432 | 433 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.4.8...v2.5.0) 434 | 435 | ### 🚀 Enhancements 436 | 437 | - Support custom prisma client instantiation ([#17](https://github.com/becem-gharbi/nuxt-auth/pull/17)) 438 | - Add Hook email provider ([#18](https://github.com/becem-gharbi/nuxt-auth/pull/18)) 439 | 440 | ### 🏡 Chore 441 | 442 | - Revert pkg version ([05f109b](https://github.com/becem-gharbi/nuxt-auth/commit/05f109b)) 443 | 444 | ### ❤️ Contributors 445 | 446 | - Becem-gharbi 447 | - Becem 448 | 449 | ## v2.4.8 450 | 451 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.4.7...v2.4.8) 452 | 453 | ### 💅 Refactors 454 | 455 | - No significant change ([d802de6](https://github.com/becem-gharbi/nuxt-auth/commit/d802de6)) 456 | - Better concurrent refresh handling ([184a62e](https://github.com/becem-gharbi/nuxt-auth/commit/184a62e)) 457 | - Replace `process` with `import.meta` ([2070c8f](https://github.com/becem-gharbi/nuxt-auth/commit/2070c8f)) 458 | - Remove `try...catch` of flow plugin ([92ab51e](https://github.com/becem-gharbi/nuxt-auth/commit/92ab51e)) 459 | - Better code readibility ([ade5fd4](https://github.com/becem-gharbi/nuxt-auth/commit/ade5fd4)) 460 | - Format import statements ([ffb4428](https://github.com/becem-gharbi/nuxt-auth/commit/ffb4428)) 461 | - More refactoring ([bef2094](https://github.com/becem-gharbi/nuxt-auth/commit/bef2094)) 462 | 463 | ### 🌊 Types 464 | 465 | - Solve typecheck issues ([23b7d4f](https://github.com/becem-gharbi/nuxt-auth/commit/23b7d4f)) 466 | 467 | ### 🏡 Chore 468 | 469 | - **playground:** Remove user image ([9ef9134](https://github.com/becem-gharbi/nuxt-auth/commit/9ef9134)) 470 | - Transpile `mustache` ([a7738b2](https://github.com/becem-gharbi/nuxt-auth/commit/a7738b2)) 471 | 472 | ### ❤️ Contributors 473 | 474 | - Becem-gharbi 475 | 476 | ## v2.4.7 477 | 478 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.4.6...v2.4.7) 479 | 480 | ### 🔥 Performance 481 | 482 | - Avoid extra refresh on oauth login fail ([271bc9b](https://github.com/becem-gharbi/nuxt-auth/commit/271bc9b)) 483 | 484 | ### 🌊 Types 485 | 486 | - Solve typecheck issues ([b36d6f2](https://github.com/becem-gharbi/nuxt-auth/commit/b36d6f2)) 487 | 488 | ### 🏡 Chore 489 | 490 | - Disable auto import ([4047b7e](https://github.com/becem-gharbi/nuxt-auth/commit/4047b7e)) 491 | 492 | ### ❤️ Contributors 493 | 494 | - Becem-gharbi 495 | 496 | ## v2.4.6 497 | 498 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.4.5...v2.4.6) 499 | 500 | ### 🩹 Fixes 501 | 502 | - Auto-revoke active session if refresh fails ([67cd505](https://github.com/becem-gharbi/nuxt-auth/commit/67cd505)) 503 | 504 | ### 💅 Refactors 505 | 506 | - Only avoid auto-logout when page not found ([390de02](https://github.com/becem-gharbi/nuxt-auth/commit/390de02)) 507 | - No significant change ([57f20a9](https://github.com/becem-gharbi/nuxt-auth/commit/57f20a9)) 508 | 509 | ### 📖 Documentation 510 | 511 | - Update introduction ([fe7b271](https://github.com/becem-gharbi/nuxt-auth/commit/fe7b271)) 512 | 513 | ### ❤️ Contributors 514 | 515 | - Becem-gharbi 516 | 517 | ## v2.4.5 518 | 519 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.4.3...v2.4.5) 520 | 521 | ### 🩹 Fixes 522 | 523 | - Avoid auto-logout on SSR error ([921662e](https://github.com/becem-gharbi/nuxt-auth/commit/921662e)) 524 | 525 | ### 💅 Refactors 526 | 527 | - Remove extra token check ([0391746](https://github.com/becem-gharbi/nuxt-auth/commit/0391746)) 528 | - No significant change ([309f48d](https://github.com/becem-gharbi/nuxt-auth/commit/309f48d)) 529 | 530 | ### 📖 Documentation 531 | 532 | - Change font family ([e8d8103](https://github.com/becem-gharbi/nuxt-auth/commit/e8d8103)) 533 | 534 | ### 🏡 Chore 535 | 536 | - **package.json:** Set homepage property ([1f98370](https://github.com/becem-gharbi/nuxt-auth/commit/1f98370)) 537 | 538 | ### ❤️ Contributors 539 | 540 | - Becem-gharbi 541 | 542 | ## v2.4.3 543 | 544 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.4.2...v2.4.3) 545 | 546 | ### 🔥 Performance 547 | 548 | - Generate default user avatar internally ([984fa4d](https://github.com/becem-gharbi/nuxt-auth/commit/984fa4d)) 549 | 550 | ### 🩹 Fixes 551 | 552 | - Avoid `useFetch` call outside of script setup ([22466e5](https://github.com/becem-gharbi/nuxt-auth/commit/22466e5)) 553 | 554 | ### 📖 Documentation 555 | 556 | - Add more details and improve phrasing ([#16](https://github.com/becem-gharbi/nuxt-auth/pull/16)) 557 | - Ensure style concistency ([49db800](https://github.com/becem-gharbi/nuxt-auth/commit/49db800)) 558 | - No significant change ([d300746](https://github.com/becem-gharbi/nuxt-auth/commit/d300746)) 559 | 560 | ### 🌊 Types 561 | 562 | - Fix undefined configKey `auth` ([2ea4d74](https://github.com/becem-gharbi/nuxt-auth/commit/2ea4d74)) 563 | 564 | ### 🏡 Chore 565 | 566 | - **playground:** No significant change ([a919215](https://github.com/becem-gharbi/nuxt-auth/commit/a919215)) 567 | - Remove demo app ([ebfdfc3](https://github.com/becem-gharbi/nuxt-auth/commit/ebfdfc3)) 568 | 569 | ### ❤️ Contributors 570 | 571 | - Becem-gharbi 572 | - Behon Baker 573 | 574 | ## v2.4.2 575 | 576 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.3.8...v2.4.2) 577 | 578 | ### 🏡 Chore 579 | 580 | - Bump version to 2.4 ([0dd9f8f](https://github.com/becem-gharbi/nuxt-auth/commit/0dd9f8f)) 581 | 582 | ### ❤️ Contributors 583 | 584 | - Becem-gharbi 585 | 586 | ## v2.3.8 587 | 588 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.3.7...v2.3.8) 589 | 590 | ### 💅 Refactors 591 | 592 | - Add `backendEnabled` and `backendBaseUrl` config options ([da41425](https://github.com/becem-gharbi/nuxt-auth/commit/da41425)) 593 | - Always overwrite baseURL with backendBaseUrl ([90285ad](https://github.com/becem-gharbi/nuxt-auth/commit/90285ad)) 594 | - Set credentials to include for cross-site requests ([adbe673](https://github.com/becem-gharbi/nuxt-auth/commit/adbe673)) 595 | - Always provide `refreshToken.cookieName` config option ([b8d02ef](https://github.com/becem-gharbi/nuxt-auth/commit/b8d02ef)) 596 | 597 | ### 📖 Documentation 598 | 599 | - Update 1.tokens.md ([7c6c06a](https://github.com/becem-gharbi/nuxt-auth/commit/7c6c06a)) 600 | - Add frontend-only docs ([fcd8d4c](https://github.com/becem-gharbi/nuxt-auth/commit/fcd8d4c)) 601 | 602 | ### 🌊 Types 603 | 604 | - Remove extra assertions ([03723f0](https://github.com/becem-gharbi/nuxt-auth/commit/03723f0)) 605 | - Exclude `backendBaseUrl` option if backend is enabled ([88eed61](https://github.com/becem-gharbi/nuxt-auth/commit/88eed61)) 606 | - Solve typecheck issues ([d3d0eac](https://github.com/becem-gharbi/nuxt-auth/commit/d3d0eac)) 607 | - Minor refactoring ([6ce190a](https://github.com/becem-gharbi/nuxt-auth/commit/6ce190a)) 608 | 609 | ### 🏡 Chore 610 | 611 | - **demo:** Upgrade nuxt-auth to v2.3.7 ([e9a776e](https://github.com/becem-gharbi/nuxt-auth/commit/e9a776e)) 612 | - **playground:** Allow cross site requests ([d87d08e](https://github.com/becem-gharbi/nuxt-auth/commit/d87d08e)) 613 | - **demo:** Upgrade deps ([c17e8ed](https://github.com/becem-gharbi/nuxt-auth/commit/c17e8ed)) 614 | - **docs:** Upgrade non-major dependencies ([3f80d07](https://github.com/becem-gharbi/nuxt-auth/commit/3f80d07)) 615 | 616 | ### ❤️ Contributors 617 | 618 | - Becem-gharbi 619 | - Becem 620 | 621 | ## v2.3.7 622 | 623 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.3.6...v2.3.7) 624 | 625 | ### 🩹 Fixes 626 | 627 | - **refresh:** Wait until previous refresh call is completed ([07afcf6](https://github.com/becem-gharbi/nuxt-auth/commit/07afcf6)) 628 | 629 | ### 💅 Refactors 630 | 631 | - **refresh:** Only pass cookies on SSR ([a73b9df](https://github.com/becem-gharbi/nuxt-auth/commit/a73b9df)) 632 | 633 | ### ❤️ Contributors 634 | 635 | - Becem-gharbi 636 | 637 | ## v2.3.6 638 | 639 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.3.5...v2.3.6) 640 | 641 | ### 🩹 Fixes 642 | 643 | - **_onLogout:** Clear user state after redirection ([30d544d](https://github.com/becem-gharbi/nuxt-auth/commit/30d544d)) 644 | 645 | ### 💅 Refactors 646 | 647 | - **plugin:** Implement new method for initialization check ([cfe6ebb](https://github.com/becem-gharbi/nuxt-auth/commit/cfe6ebb)) 648 | - **middleware:** Replace user with access token to check logged in status ([a1b8432](https://github.com/becem-gharbi/nuxt-auth/commit/a1b8432)) 649 | - No significant change ([c1d1c52](https://github.com/becem-gharbi/nuxt-auth/commit/c1d1c52)) 650 | - **_loggedIn:** Use computed value instead of get/set methods ([d8dc8a2](https://github.com/becem-gharbi/nuxt-auth/commit/d8dc8a2)) 651 | - **useAuthSession:** Rename _loggedIn to _loggedInFlag ([387c097](https://github.com/becem-gharbi/nuxt-auth/commit/387c097)) 652 | - **_refresh:** Remove extra _loggedInFlag set ([96addfb](https://github.com/becem-gharbi/nuxt-auth/commit/96addfb)) 653 | - Reload the page on logout ([97acee8](https://github.com/becem-gharbi/nuxt-auth/commit/97acee8)) 654 | - Use navigateTo instead of location.replace ([a61db44](https://github.com/becem-gharbi/nuxt-auth/commit/a61db44)) 655 | 656 | ### 🏡 Chore 657 | 658 | - **demo:** Upgrade nuxt-auth to v2.3.5 ([12fe4d6](https://github.com/becem-gharbi/nuxt-auth/commit/12fe4d6)) 659 | 660 | ### ❤️ Contributors 661 | 662 | - Becem-gharbi 663 | 664 | ## v2.3.5 665 | 666 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.3.4...v2.3.5) 667 | 668 | ### 💅 Refactors 669 | 670 | - Specify auto-imported composables ([00847bd](https://github.com/becem-gharbi/nuxt-auth/commit/00847bd)) 671 | - Add `expires_in` to login & refresh response ([7f5a57e](https://github.com/becem-gharbi/nuxt-auth/commit/7f5a57e)) 672 | - No significant change ([4f1f565](https://github.com/becem-gharbi/nuxt-auth/commit/4f1f565)) 673 | - Create `useAuthToken` to handle access token storage ([cbd4508](https://github.com/becem-gharbi/nuxt-auth/commit/cbd4508)) 674 | - Change access token storage from cookie to memory ([13c2b2b](https://github.com/becem-gharbi/nuxt-auth/commit/13c2b2b)) 675 | - No significant change ([9632308](https://github.com/becem-gharbi/nuxt-auth/commit/9632308)) 676 | - Remove unused `accessTokenCookieName` config option ([e99fc73](https://github.com/becem-gharbi/nuxt-auth/commit/e99fc73)) 677 | 678 | ### 📖 Documentation 679 | 680 | - **tokens:** Remove `accessTokenCookieName` config option ([9a29219](https://github.com/becem-gharbi/nuxt-auth/commit/9a29219)) 681 | 682 | ### 🏡 Chore 683 | 684 | - **playground:** Remove accessTokenCookieName ([b41732c](https://github.com/becem-gharbi/nuxt-auth/commit/b41732c)) 685 | 686 | ### ❤️ Contributors 687 | 688 | - Becem-gharbi 689 | 690 | ## v2.3.4 691 | 692 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.3.3...v2.3.4) 693 | 694 | ### 🌊 Types 695 | 696 | - **getAllSessions:** Fix return type ([1fa06ed](https://github.com/becem-gharbi/nuxt-auth/commit/1fa06ed)) 697 | 698 | ### 🏡 Chore 699 | 700 | - **demo:** Upgrade deps ([a799763](https://github.com/becem-gharbi/nuxt-auth/commit/a799763)) 701 | 702 | ### ❤️ Contributors 703 | 704 | - Becem-gharbi 705 | 706 | ## v2.3.3 707 | 708 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.3.2...v2.3.3) 709 | 710 | ### 🩹 Fixes 711 | 712 | - **getAllSessions:** Fix undefined `ua` property ([d04aa37](https://github.com/becem-gharbi/nuxt-auth/commit/d04aa37)) 713 | 714 | ### 🏡 Chore 715 | 716 | - **demo:** Upgrade deps ([702cec6](https://github.com/becem-gharbi/nuxt-auth/commit/702cec6)) 717 | 718 | ### ❤️ Contributors 719 | 720 | - Becem-gharbi 721 | 722 | ## v2.3.2 723 | 724 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.3.1...v2.3.2) 725 | 726 | ### 🩹 Fixes 727 | 728 | - Sync login on multiple tabs ([28c7c32](https://github.com/becem-gharbi/nuxt-auth/commit/28c7c32)) 729 | - Delete refresh token cookie on server-side refresh fail ([7ae64bc](https://github.com/becem-gharbi/nuxt-auth/commit/7ae64bc)) 730 | 731 | ### 💅 Refactors 732 | 733 | - Verify user state on `_login` `_logout` handlers ([fa1b37f](https://github.com/becem-gharbi/nuxt-auth/commit/fa1b37f)) 734 | - **getAllSessions:** Move formatting on server-side ([017c830](https://github.com/becem-gharbi/nuxt-auth/commit/017c830)) 735 | - **getAllSessions:** Remove userId ([d94e1fa](https://github.com/becem-gharbi/nuxt-auth/commit/d94e1fa)) 736 | - **getAllSessions:** Move current session on top ([809486e](https://github.com/becem-gharbi/nuxt-auth/commit/809486e)) 737 | 738 | ### 🏡 Chore 739 | 740 | - **demo:** Upgrade deps ([d178162](https://github.com/becem-gharbi/nuxt-auth/commit/d178162)) 741 | 742 | ### ❤️ Contributors 743 | 744 | - Becem-gharbi 745 | 746 | ## v2.3.1 747 | 748 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.3.0...v2.3.1) 749 | 750 | ### 🩹 Fixes 751 | 752 | - **revoke session:** Fix id parser ([4529197](https://github.com/becem-gharbi/nuxt-auth/commit/4529197)) 753 | 754 | ### 🏡 Chore 755 | 756 | - **demo:** Upgrade deps ([17b3e0a](https://github.com/becem-gharbi/nuxt-auth/commit/17b3e0a)) 757 | 758 | ### ❤️ Contributors 759 | 760 | - Becem-gharbi 761 | 762 | ## v2.3.0 763 | 764 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.2.1...v2.3.0) 765 | 766 | ### 🚀 Enhancements 767 | 768 | - Add support for prisma accelerate on edge runtimes ([15acf8b](https://github.com/becem-gharbi/nuxt-auth/commit/15acf8b)) 769 | 770 | ### 🔥 Performance 771 | 772 | - Limit selection on DB queries ([eb8164d](https://github.com/becem-gharbi/nuxt-auth/commit/eb8164d)) 773 | - Fix bfcache failed ([d588217](https://github.com/becem-gharbi/nuxt-auth/commit/d588217)) 774 | 775 | ### 🩹 Fixes 776 | 777 | - **session revoke:** Parse `id` to int when needed ([ba7b3ef](https://github.com/becem-gharbi/nuxt-auth/commit/ba7b3ef)) 778 | 779 | ### 💅 Refactors 780 | 781 | - Add loggedInFlagName config option ([011cf5f](https://github.com/becem-gharbi/nuxt-auth/commit/011cf5f)) 782 | 783 | ### 📖 Documentation 784 | 785 | - Update edge deployment section ([4a77bb8](https://github.com/becem-gharbi/nuxt-auth/commit/4a77bb8)) 786 | 787 | ### 🏡 Chore 788 | 789 | - **demo:** Upgrade deps ([cc19286](https://github.com/becem-gharbi/nuxt-auth/commit/cc19286)) 790 | - **playground:** Update sql schema ([bf1a170](https://github.com/becem-gharbi/nuxt-auth/commit/bf1a170)) 791 | - **demo:** Upgrade deps ([114068d](https://github.com/becem-gharbi/nuxt-auth/commit/114068d)) 792 | - **demo:** Update prisma generate command in prod ([43491fd](https://github.com/becem-gharbi/nuxt-auth/commit/43491fd)) 793 | - Set tag to latest ([d4d55ed](https://github.com/becem-gharbi/nuxt-auth/commit/d4d55ed)) 794 | 795 | ### ❤️ Contributors 796 | 797 | - Becem-gharbi 798 | 799 | ## v2.2.1 800 | 801 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.2.0...v2.2.1) 802 | 803 | ### 🩹 Fixes 804 | 805 | - Make sure provider plugin is registered first ([ba73ecf](https://github.com/becem-gharbi/nuxt-auth/commit/ba73ecf)) 806 | 807 | ### 💅 Refactors 808 | 809 | - **useAuthFetch:** Minor refactoring ([c40fcfb](https://github.com/becem-gharbi/nuxt-auth/commit/c40fcfb)) 810 | - Remove path check on auth server middleware ([45e9633](https://github.com/becem-gharbi/nuxt-auth/commit/45e9633)) 811 | - **composables:** Use named export ([095f689](https://github.com/becem-gharbi/nuxt-auth/commit/095f689)) 812 | - Create custom $fetch instance as alternative to useAuthFetch ([38507b5](https://github.com/becem-gharbi/nuxt-auth/commit/38507b5)) 813 | 814 | ### 📖 Documentation 815 | 816 | - Add useAuthFetch deprecation alert ([1a8fa89](https://github.com/becem-gharbi/nuxt-auth/commit/1a8fa89)) 817 | - Upgrade dependencies ([503cbe3](https://github.com/becem-gharbi/nuxt-auth/commit/503cbe3)) 818 | 819 | ### 🌊 Types 820 | 821 | - **useAuthFetch:** Set return type the same as $fetch ([b71bd79](https://github.com/becem-gharbi/nuxt-auth/commit/b71bd79)) 822 | - **useAuth:** Refactor and add missing return types ([28de251](https://github.com/becem-gharbi/nuxt-auth/commit/28de251)) 823 | - **useAuthSession:** Refactor and add missing types ([80b278d](https://github.com/becem-gharbi/nuxt-auth/commit/80b278d)) 824 | - Ignore specific typechecks ([123e30c](https://github.com/becem-gharbi/nuxt-auth/commit/123e30c)) 825 | - **accessToken:** Set fingerprint as null instead of empty string ([271c9f8](https://github.com/becem-gharbi/nuxt-auth/commit/271c9f8)) 826 | - **refreshToken:** Set userAgent as null instead of undefined ([caf5f65](https://github.com/becem-gharbi/nuxt-auth/commit/caf5f65)) 827 | - Set $auth.fetch type the same as $fetch ([d4cd4f1](https://github.com/becem-gharbi/nuxt-auth/commit/d4cd4f1)) 828 | - **composables:** Explicitly set return types ([3dceb53](https://github.com/becem-gharbi/nuxt-auth/commit/3dceb53)) 829 | 830 | ### 🏡 Chore 831 | 832 | - **lint:** Ignore #imports not found ([64f64b6](https://github.com/becem-gharbi/nuxt-auth/commit/64f64b6)) 833 | - **lint:** Check on release script ([44a6b69](https://github.com/becem-gharbi/nuxt-auth/commit/44a6b69)) 834 | - Rename nuxt plugins ([440e1db](https://github.com/becem-gharbi/nuxt-auth/commit/440e1db)) 835 | - **useAuthFetch:** Mark as deprecated ([5e131f5](https://github.com/becem-gharbi/nuxt-auth/commit/5e131f5)) 836 | 837 | ### ❤️ Contributors 838 | 839 | - Becem-gharbi 840 | 841 | ## v2.2.0 842 | 843 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.1.0...v2.2.0) 844 | 845 | ### 🚀 Enhancements 846 | 847 | - Add fingerprint check on access token verification ([6a9c604](https://github.com/becem-gharbi/nuxt-auth/commit/6a9c604)) 848 | 849 | ### 🔥 Performance 850 | 851 | - Avoid relying on useCookie for multi-tabs auto logout ([7865639](https://github.com/becem-gharbi/nuxt-auth/commit/7865639)) 852 | - Avoid access token check on non API requests ([e9b98e3](https://github.com/becem-gharbi/nuxt-auth/commit/e9b98e3)) 853 | 854 | ### 💅 Refactors 855 | 856 | - **useAuthSession:** Replace useCookie with js-cookie ([cc2ea24](https://github.com/becem-gharbi/nuxt-auth/commit/cc2ea24)) 857 | - **useAuth:** Remove delay on login ([ed7e39b](https://github.com/becem-gharbi/nuxt-auth/commit/ed7e39b)) 858 | - Create client-only plugin for Broadcast channel ([4d5050a](https://github.com/becem-gharbi/nuxt-auth/commit/4d5050a)) 859 | - Verify userAgent on token refresh ([5495a8f](https://github.com/becem-gharbi/nuxt-auth/commit/5495a8f)) 860 | - **refresh:** Pass user-agent to API ([cfe9bb6](https://github.com/becem-gharbi/nuxt-auth/commit/cfe9bb6)) 861 | - **fetch:** Pass user-agent to API ([827cdf7](https://github.com/becem-gharbi/nuxt-auth/commit/827cdf7)) 862 | - Add event argument to verifyAccessToken and createAccessToken ([1a2f6fc](https://github.com/becem-gharbi/nuxt-auth/commit/1a2f6fc)) 863 | - Create fingerprint server utility ([6438610](https://github.com/becem-gharbi/nuxt-auth/commit/6438610)) 864 | - Always return json on API response (or redirect) ([feeed74](https://github.com/becem-gharbi/nuxt-auth/commit/feeed74)) 865 | - **fingerprint:** Use h3 built-in hash option ([7dc51c6](https://github.com/becem-gharbi/nuxt-auth/commit/7dc51c6)) 866 | - Minor refactoring ([7fceab1](https://github.com/becem-gharbi/nuxt-auth/commit/7fceab1)) 867 | 868 | ### 🌊 Types 869 | 870 | - Update types.d.ts ([9153b39](https://github.com/becem-gharbi/nuxt-auth/commit/9153b39)) 871 | 872 | ### 🏡 Chore 873 | 874 | - **demo:** Upgrade dependencies ([42b4825](https://github.com/becem-gharbi/nuxt-auth/commit/42b4825)) 875 | - **demo:** Sync lock ([0c0f473](https://github.com/becem-gharbi/nuxt-auth/commit/0c0f473)) 876 | - **demo:** Upgrade nuxt to 3.8.2 ([7b6fb55](https://github.com/becem-gharbi/nuxt-auth/commit/7b6fb55)) 877 | - Set nuxt compatibility to 3.8.2 ([50ff905](https://github.com/becem-gharbi/nuxt-auth/commit/50ff905)) 878 | - **demo:** Set access token max age to 20 sec ([cb174c9](https://github.com/becem-gharbi/nuxt-auth/commit/cb174c9)) 879 | - **demo:** Upgrade deps ([4535cd7](https://github.com/becem-gharbi/nuxt-auth/commit/4535cd7)) 880 | - Set tag to latest ([5712684](https://github.com/becem-gharbi/nuxt-auth/commit/5712684)) 881 | 882 | ### ❤️ Contributors 883 | 884 | - Becem-gharbi 885 | - Becem 886 | 887 | ## v2.1.0 888 | 889 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.0.2...v2.1.0) 890 | 891 | ### 🚀 Enhancements 892 | 893 | - **deps:** Upgrade jose to v5 ([0ff01d1](https://github.com/becem-gharbi/nuxt-auth/commit/0ff01d1)) 894 | 895 | ### 🔥 Performance 896 | 897 | - Move default email templates to module setup ([bbf875c](https://github.com/becem-gharbi/nuxt-auth/commit/bbf875c)) 898 | - **login:** Reduce timeout to zero ([e775eb6](https://github.com/becem-gharbi/nuxt-auth/commit/e775eb6)) 899 | 900 | ### 🩹 Fixes 901 | 902 | - Auto logout when multiple tabs opened ([eae8f4f](https://github.com/becem-gharbi/nuxt-auth/commit/eae8f4f)) 903 | 904 | ### 💅 Refactors 905 | 906 | - Remove nuxt logo from default email templates ([990ef5e](https://github.com/becem-gharbi/nuxt-auth/commit/990ef5e)) 907 | - Minor refactoring ([7acadc5](https://github.com/becem-gharbi/nuxt-auth/commit/7acadc5)) 908 | - **useAuth:** Create _onlogin and _onLogout handlers ([cefe150](https://github.com/becem-gharbi/nuxt-auth/commit/cefe150)) 909 | - Avoid fetch on auto logout ([c81edda](https://github.com/becem-gharbi/nuxt-auth/commit/c81edda)) 910 | - **configOptions:** Add accessToken cookieName option ([b1c267b](https://github.com/becem-gharbi/nuxt-auth/commit/b1c267b)) 911 | - **login:** Resolve after redirection ([a7e7dec](https://github.com/becem-gharbi/nuxt-auth/commit/a7e7dec)) 912 | - Watch access token cookie on mounted ([1c853d8](https://github.com/becem-gharbi/nuxt-auth/commit/1c853d8)) 913 | 914 | ### 📖 Documentation 915 | 916 | - Change social card ([24b2929](https://github.com/becem-gharbi/nuxt-auth/commit/24b2929)) 917 | - Add cookieName to accessToken default config ([c9d097f](https://github.com/becem-gharbi/nuxt-auth/commit/c9d097f)) 918 | 919 | ### 🏡 Chore 920 | 921 | - **demo:** Upgrade dependencies ([d76e02c](https://github.com/becem-gharbi/nuxt-auth/commit/d76e02c)) 922 | - No significant change ([3a098e1](https://github.com/becem-gharbi/nuxt-auth/commit/3a098e1)) 923 | - Set tag to latest ([e032b0e](https://github.com/becem-gharbi/nuxt-auth/commit/e032b0e)) 924 | 925 | ### ❤️ Contributors 926 | 927 | - Becem-gharbi ([@becem-gharbi](http://github.com/becem-gharbi)) 928 | 929 | ## v2.0.2 930 | 931 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.1.1-rc...v2.0.2) 932 | 933 | ### 📖 Documentation 934 | 935 | - **readme:** Remove V2 from title ([c974f0c](https://github.com/becem-gharbi/nuxt-auth/commit/c974f0c)) 936 | - Add utils page ([389f6f2](https://github.com/becem-gharbi/nuxt-auth/commit/389f6f2)) 937 | 938 | ### 🌊 Types 939 | 940 | - Add types for bcrypt and jwt server utilities ([7f158c8](https://github.com/becem-gharbi/nuxt-auth/commit/7f158c8)) 941 | 942 | ### 🏡 Chore 943 | 944 | - **demo:** Upgrade dependencies ([072ae09](https://github.com/becem-gharbi/nuxt-auth/commit/072ae09)) 945 | - Add typecheck to release workflow ([f0860a9](https://github.com/becem-gharbi/nuxt-auth/commit/f0860a9)) 946 | - Remove rc suffix ([730948b](https://github.com/becem-gharbi/nuxt-auth/commit/730948b)) 947 | - Fix lint issues ([03ee52f](https://github.com/becem-gharbi/nuxt-auth/commit/03ee52f)) 948 | - **release:** V2.0.1 ([6578540](https://github.com/becem-gharbi/nuxt-auth/commit/6578540)) 949 | - Add funding btn ([cc609e1](https://github.com/becem-gharbi/nuxt-auth/commit/cc609e1)) 950 | - Expose bcrypt and jwt server utilities via #auth ([683e2e9](https://github.com/becem-gharbi/nuxt-auth/commit/683e2e9)) 951 | - Upgrade dependencies ([c720a1f](https://github.com/becem-gharbi/nuxt-auth/commit/c720a1f)) 952 | - Fix lint issues ([dfce5c8](https://github.com/becem-gharbi/nuxt-auth/commit/dfce5c8)) 953 | 954 | ### ❤️ Contributors 955 | 956 | - Becem-gharbi 957 | - Becem Gharbi 958 | 959 | ## v2.0.1 960 | 961 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.1.1-rc...v2.0.1) 962 | 963 | ### 📖 Documentation 964 | 965 | - **readme:** Remove V2 from title ([c974f0c](https://github.com/becem-gharbi/nuxt-auth/commit/c974f0c)) 966 | 967 | ### 🏡 Chore 968 | 969 | - **demo:** Upgrade dependencies ([072ae09](https://github.com/becem-gharbi/nuxt-auth/commit/072ae09)) 970 | - Add typecheck to release workflow ([f0860a9](https://github.com/becem-gharbi/nuxt-auth/commit/f0860a9)) 971 | - Remove rc suffix ([730948b](https://github.com/becem-gharbi/nuxt-auth/commit/730948b)) 972 | - Fix lint issues ([03ee52f](https://github.com/becem-gharbi/nuxt-auth/commit/03ee52f)) 973 | 974 | ### ❤️ Contributors 975 | 976 | - Becem Gharbi 977 | 978 | ## v2.1.1-rc 979 | 980 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.1.0-rc...v2.1.1-rc) 981 | 982 | ### 🔥 Performance 983 | 984 | - Remove baseURL on internal $fetch ([3ef64c9](https://github.com/becem-gharbi/nuxt-auth/commit/3ef64c9)) 985 | 986 | ### 📖 Documentation 987 | 988 | - Update 4.email.md ([fb7b2cb](https://github.com/becem-gharbi/nuxt-auth/commit/fb7b2cb)) 989 | - Remove domain should be 127.0.0.1 warning ([b0e9e3e](https://github.com/becem-gharbi/nuxt-auth/commit/b0e9e3e)) 990 | 991 | ### 🏡 Chore 992 | 993 | - **demo:** Display module version ([034a271](https://github.com/becem-gharbi/nuxt-auth/commit/034a271)) 994 | - Fix lint issues ([cfa2097](https://github.com/becem-gharbi/nuxt-auth/commit/cfa2097)) 995 | 996 | ### ❤️ Contributors 997 | 998 | - Becem Gharbi 999 | - Becem 1000 | 1001 | ## v2.1.0-rc 1002 | 1003 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.0.5-rc...v2.1.0-rc) 1004 | 1005 | ### 🚀 Enhancements 1006 | 1007 | - Add Resend email provider ([b881cbc](https://github.com/becem-gharbi/nuxt-auth/commit/b881cbc)) 1008 | 1009 | ### 🩹 Fixes 1010 | 1011 | - **handleError:** Avoid returning server errors instead log them to console ([560dffe](https://github.com/becem-gharbi/nuxt-auth/commit/560dffe)) 1012 | - **handleError:** Return all errors except for Prisma errors ([8c48120](https://github.com/becem-gharbi/nuxt-auth/commit/8c48120)) 1013 | 1014 | ### 💅 Refactors 1015 | 1016 | - Use vent.context.auth to check authorization on internal protected endpoints ([5adf8be](https://github.com/becem-gharbi/nuxt-auth/commit/5adf8be)) 1017 | - Pass id as param on revoke single session endpoint ([e441c46](https://github.com/becem-gharbi/nuxt-auth/commit/e441c46)) 1018 | 1019 | ### 📖 Documentation 1020 | 1021 | - Add development domain should be 127.0.0.1 warning ([eda21a9](https://github.com/becem-gharbi/nuxt-auth/commit/eda21a9)) 1022 | - Add Resend configuration to email section ([ae55731](https://github.com/becem-gharbi/nuxt-auth/commit/ae55731)) 1023 | 1024 | ### 🏡 Chore 1025 | 1026 | - **demo:** Upgrade dependencies ([dd722e9](https://github.com/becem-gharbi/nuxt-auth/commit/dd722e9)) 1027 | - Fix ESlint issues ([ea9a546](https://github.com/becem-gharbi/nuxt-auth/commit/ea9a546)) 1028 | - **playground:** Set access token max age to 10 sec ([d7915d7](https://github.com/becem-gharbi/nuxt-auth/commit/d7915d7)) 1029 | - Upgrade dependencies ([dd9d9ce](https://github.com/becem-gharbi/nuxt-auth/commit/dd9d9ce)) 1030 | - **playground:** Add forms ([560826a](https://github.com/becem-gharbi/nuxt-auth/commit/560826a)) 1031 | - **playground:** Update config ([b2d991e](https://github.com/becem-gharbi/nuxt-auth/commit/b2d991e)) 1032 | - **playground:** Change baseUrl host to 127.0.0.1 ([bc16f65](https://github.com/becem-gharbi/nuxt-auth/commit/bc16f65)) 1033 | - Fix ESLint issues ([df9b281](https://github.com/becem-gharbi/nuxt-auth/commit/df9b281)) 1034 | - **playground:** Switch email provider to Resend ([68d72a6](https://github.com/becem-gharbi/nuxt-auth/commit/68d72a6)) 1035 | - Set tag to latest ([a253093](https://github.com/becem-gharbi/nuxt-auth/commit/a253093)) 1036 | 1037 | ### ❤️ Contributors 1038 | 1039 | - Becem Gharbi 1040 | 1041 | ## v2.0.5-rc 1042 | 1043 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.0.4-rc...v2.0.5-rc) 1044 | 1045 | ### 🩹 Fixes 1046 | 1047 | - **passwordReset:** Allow only one password reset per password request ([0309a64](https://github.com/becem-gharbi/nuxt-auth/commit/0309a64)) 1048 | 1049 | ### 💅 Refactors 1050 | 1051 | - Remove unused server-side user utilities ([4fbdbf0](https://github.com/becem-gharbi/nuxt-auth/commit/4fbdbf0)) 1052 | 1053 | ### 📖 Documentation 1054 | 1055 | - Update composables content ([6ec56e6](https://github.com/becem-gharbi/nuxt-auth/commit/6ec56e6)) 1056 | - Update setup content ([d828593](https://github.com/becem-gharbi/nuxt-auth/commit/d828593)) 1057 | - Update setup content ([6821263](https://github.com/becem-gharbi/nuxt-auth/commit/6821263)) 1058 | 1059 | ### 🏡 Chore 1060 | 1061 | - **demo:** Upgrade dependencies ([47ba2b0](https://github.com/becem-gharbi/nuxt-auth/commit/47ba2b0)) 1062 | 1063 | ### ❤️ Contributors 1064 | 1065 | - Becem Gharbi 1066 | 1067 | ## v2.0.4-rc 1068 | 1069 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.0.3-rc...v2.0.4-rc) 1070 | 1071 | ### 💅 Refactors 1072 | 1073 | - **generateAvatar:** Select background color from predefined colors ([70cacfc](https://github.com/becem-gharbi/nuxt-auth/commit/70cacfc)) 1074 | - Remove extra vent argument ([6f54dc2](https://github.com/becem-gharbi/nuxt-auth/commit/6f54dc2)) 1075 | - **useAuthSession:** Prefix internal apis with underscore ([c15013d](https://github.com/becem-gharbi/nuxt-auth/commit/c15013d)) 1076 | - **defaults:** Set access token default maxAge to 15 min ([21d402a](https://github.com/becem-gharbi/nuxt-auth/commit/21d402a)) 1077 | 1078 | ### 📖 Documentation 1079 | 1080 | - Update docs link ([c89f7f9](https://github.com/becem-gharbi/nuxt-auth/commit/c89f7f9)) 1081 | - Update 2.middlewares.md ([7328747](https://github.com/becem-gharbi/nuxt-auth/commit/7328747)) 1082 | - Update composables content ([0eafadb](https://github.com/becem-gharbi/nuxt-auth/commit/0eafadb)) 1083 | - Fix typo ([65ba2f0](https://github.com/becem-gharbi/nuxt-auth/commit/65ba2f0)) 1084 | 1085 | ### 🌊 Types 1086 | 1087 | - Mark user state as read-only ([402db04](https://github.com/becem-gharbi/nuxt-auth/commit/402db04)) 1088 | 1089 | ### 🏡 Chore 1090 | 1091 | - **demo:** Upgrade dependencies ([3933115](https://github.com/becem-gharbi/nuxt-auth/commit/3933115)) 1092 | 1093 | ### ❤️ Contributors 1094 | 1095 | - Becem Gharbi 1096 | - Becem 1097 | 1098 | ## v2.0.3-rc 1099 | 1100 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.0.2-rc...v2.0.3-rc) 1101 | 1102 | ### 🩹 Fixes 1103 | 1104 | - Remove method from email custom config ([ef99be2](https://github.com/becem-gharbi/nuxt-auth/commit/ef99be2)) 1105 | 1106 | ### 📖 Documentation 1107 | 1108 | - **readme:** Update features ([55a3e56](https://github.com/becem-gharbi/nuxt-auth/commit/55a3e56)) 1109 | - Update docs website ([4cb80ff](https://github.com/becem-gharbi/nuxt-auth/commit/4cb80ff)) 1110 | - Add oauth redirect url setting ([d16b3a1](https://github.com/becem-gharbi/nuxt-auth/commit/d16b3a1)) 1111 | - Update docs website ([88a0006](https://github.com/becem-gharbi/nuxt-auth/commit/88a0006)) 1112 | - Update docs website ([aaa7c3e](https://github.com/becem-gharbi/nuxt-auth/commit/aaa7c3e)) 1113 | 1114 | ### ❤️ Contributors 1115 | 1116 | - Becem Gharbi 1117 | 1118 | ## v2.0.2-rc 1119 | 1120 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v2.0.1-rc...v2.0.2-rc) 1121 | 1122 | ### 💅 Refactors 1123 | 1124 | - Remove admin API ([3c749cb](https://github.com/becem-gharbi/nuxt-auth/commit/3c749cb)) 1125 | 1126 | ### 📖 Documentation 1127 | 1128 | - **readme:** Add installation section ([aa713b7](https://github.com/becem-gharbi/nuxt-auth/commit/aa713b7)) 1129 | 1130 | ### ❤️ Contributors 1131 | 1132 | - Becem Gharbi 1133 | 1134 | ## v2.0.1-rc 1135 | 1136 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.4.4...v2.0.1) 1137 | 1138 | ### 🔥 Performance 1139 | 1140 | - Register server handlers conditionally ([dd789ab](https://github.com/becem-gharbi/nuxt-auth/commit/dd789ab)) 1141 | - Use named imports ([cb1ef03](https://github.com/becem-gharbi/nuxt-auth/commit/cb1ef03)) 1142 | 1143 | ### 🩹 Fixes 1144 | 1145 | - Import prisma edge client on edge environments (support for cloudflare) ([dbf14e5](https://github.com/becem-gharbi/nuxt-auth/commit/dbf14e5)) 1146 | - Detect edge env from nitro preset ([bd2d48a](https://github.com/becem-gharbi/nuxt-auth/commit/bd2d48a)) 1147 | - Disable environment detection only on dev ([b8f48a8](https://github.com/becem-gharbi/nuxt-auth/commit/b8f48a8)) 1148 | - **handleError:** Check if error exists ([cbea95d](https://github.com/becem-gharbi/nuxt-auth/commit/cbea95d)) 1149 | - Use default import from nodemailer ([759cde1](https://github.com/becem-gharbi/nuxt-auth/commit/759cde1)) 1150 | - Assign default value to passwordValidationRegex ([eca323e](https://github.com/becem-gharbi/nuxt-auth/commit/eca323e)) 1151 | - **refresh:** Remove request body ([5af0fd4](https://github.com/becem-gharbi/nuxt-auth/commit/5af0fd4)) 1152 | - Exclude current session on delete all sessions ([a3715f2](https://github.com/becem-gharbi/nuxt-auth/commit/a3715f2)) 1153 | - Fix get accessToken on server side ([eba964f](https://github.com/becem-gharbi/nuxt-auth/commit/eba964f)) 1154 | 1155 | ### 💅 Refactors 1156 | 1157 | - Pass prisma instance via event context (support cloudflare) ([9b3a612](https://github.com/becem-gharbi/nuxt-auth/commit/9b3a612)) 1158 | - Retreive config object on event lifecycle (support for cloudflare) ([508462c](https://github.com/becem-gharbi/nuxt-auth/commit/508462c)) 1159 | - Move email default templates to event handlers ([2bb43d9](https://github.com/becem-gharbi/nuxt-auth/commit/2bb43d9)) 1160 | - Always import getConfig from #auth ([2be2471](https://github.com/becem-gharbi/nuxt-auth/commit/2be2471)) 1161 | - Replace logger with console on error handler ([66b8e18](https://github.com/becem-gharbi/nuxt-auth/commit/66b8e18)) 1162 | - Create #auth on setup scope ([bcc4360](https://github.com/becem-gharbi/nuxt-auth/commit/bcc4360)) 1163 | - Start setup with setting runtime config ([5b4cee8](https://github.com/becem-gharbi/nuxt-auth/commit/5b4cee8)) 1164 | - Use relative import between server utils ([13efb59](https://github.com/becem-gharbi/nuxt-auth/commit/13efb59)) 1165 | - **handleError:** Remove Prisma & JWT instance check ([7907466](https://github.com/becem-gharbi/nuxt-auth/commit/7907466)) 1166 | - **sendEmail:** Replace nodemailer with HTTP client ([9e68078](https://github.com/becem-gharbi/nuxt-auth/commit/9e68078)) 1167 | - Change default password reset email template ([87fd699](https://github.com/becem-gharbi/nuxt-auth/commit/87fd699)) 1168 | - Change default email verification template ([2ef7b16](https://github.com/becem-gharbi/nuxt-auth/commit/2ef7b16)) 1169 | - Remove extra credentials include from fetch calls ([21f46ab](https://github.com/becem-gharbi/nuxt-auth/commit/21f46ab)) 1170 | - Refactor useAuthSession ([55c5191](https://github.com/becem-gharbi/nuxt-auth/commit/55c5191)) 1171 | - Change fallback avatar properties ([4f8cd31](https://github.com/becem-gharbi/nuxt-auth/commit/4f8cd31)) 1172 | - Replace useUser method with user reactive state ([9d2d34d](https://github.com/becem-gharbi/nuxt-auth/commit/9d2d34d)) 1173 | - Expose auth session on event context ([05df8ea](https://github.com/becem-gharbi/nuxt-auth/commit/05df8ea)) 1174 | - Remove unused event arg from getConfig utility ([623fb5d](https://github.com/becem-gharbi/nuxt-auth/commit/623fb5d)) 1175 | 1176 | ### 📖 Documentation 1177 | 1178 | - Create docus app ([121af26](https://github.com/becem-gharbi/nuxt-auth/commit/121af26)) 1179 | - Define architecture ([3dec8dc](https://github.com/becem-gharbi/nuxt-auth/commit/3dec8dc)) 1180 | - Update README ([631b22d](https://github.com/becem-gharbi/nuxt-auth/commit/631b22d)) 1181 | - Update README ([7352172](https://github.com/becem-gharbi/nuxt-auth/commit/7352172)) 1182 | - Add docs website to README ([8cea913](https://github.com/becem-gharbi/nuxt-auth/commit/8cea913)) 1183 | 1184 | ### 🌊 Types 1185 | 1186 | - Add prisma type to event context ([426ef45](https://github.com/becem-gharbi/nuxt-auth/commit/426ef45)) 1187 | - Change auth type on event context ([6aa738b](https://github.com/becem-gharbi/nuxt-auth/commit/6aa738b)) 1188 | 1189 | ### 🏡 Chore 1190 | 1191 | - Upgrade dependencies ([d0ee118](https://github.com/becem-gharbi/nuxt-auth/commit/d0ee118)) 1192 | - Upgrade prisma to latest ([1209bd3](https://github.com/becem-gharbi/nuxt-auth/commit/1209bd3)) 1193 | - Add edge tag ([b34e444](https://github.com/becem-gharbi/nuxt-auth/commit/b34e444)) 1194 | - Add environment detection log ([521b1a3](https://github.com/becem-gharbi/nuxt-auth/commit/521b1a3)) 1195 | - Migrate from jsonwebtoken to jwt-simple ([f544c81](https://github.com/becem-gharbi/nuxt-auth/commit/f544c81)) 1196 | - Set prisma as peer-dependency ([a89fbc8](https://github.com/becem-gharbi/nuxt-auth/commit/a89fbc8)) 1197 | - Add cloudflare to edge supported presets ([bd8b3a5](https://github.com/becem-gharbi/nuxt-auth/commit/bd8b3a5)) 1198 | - Fix signRefreshToken call ([24b5cf4](https://github.com/becem-gharbi/nuxt-auth/commit/24b5cf4)) 1199 | - Create demo app ([175f8d4](https://github.com/becem-gharbi/nuxt-auth/commit/175f8d4)) 1200 | - **demo:** Prepare first deployment ([39acdfd](https://github.com/becem-gharbi/nuxt-auth/commit/39acdfd)) 1201 | - **demo:** Add auth pages ([1fa57b6](https://github.com/becem-gharbi/nuxt-auth/commit/1fa57b6)) 1202 | - **demo:** Trigger new deployment ([31160b2](https://github.com/becem-gharbi/nuxt-auth/commit/31160b2)) 1203 | - Migrate from jwt-simple to jose ([0758e11](https://github.com/becem-gharbi/nuxt-auth/commit/0758e11)) 1204 | - **demo:** Add login form ([d31cf6e](https://github.com/becem-gharbi/nuxt-auth/commit/d31cf6e)) 1205 | - **demo:** Upgrade dependencies ([e21ea5b](https://github.com/becem-gharbi/nuxt-auth/commit/e21ea5b)) 1206 | - **demo:** Add register form ([47a6534](https://github.com/becem-gharbi/nuxt-auth/commit/47a6534)) 1207 | - **demo:** Add reset password form ([7802e4d](https://github.com/becem-gharbi/nuxt-auth/commit/7802e4d)) 1208 | - **demo:** Upgrade dependencies ([80214c0](https://github.com/becem-gharbi/nuxt-auth/commit/80214c0)) 1209 | - **demo:** Upgrade dependencies ([14bc652](https://github.com/becem-gharbi/nuxt-auth/commit/14bc652)) 1210 | - **demo:** Set prisma datasourceUrl ([d66a4de](https://github.com/becem-gharbi/nuxt-auth/commit/d66a4de)) 1211 | - **demo:** Upgrade to nuxt v3.7 ([87f6cba](https://github.com/becem-gharbi/nuxt-auth/commit/87f6cba)) 1212 | - **demo:** Disable SSR ([97bdec1](https://github.com/becem-gharbi/nuxt-auth/commit/97bdec1)) 1213 | - **demo:** Enable ssr ([81d4196](https://github.com/becem-gharbi/nuxt-auth/commit/81d4196)) 1214 | - **demo:** Upgrade dependencies ([29b3d27](https://github.com/becem-gharbi/nuxt-auth/commit/29b3d27)) 1215 | - Remove logs ([dd15758](https://github.com/becem-gharbi/nuxt-auth/commit/dd15758)) 1216 | - **demo:** Upgrade dependencies ([254f326](https://github.com/becem-gharbi/nuxt-auth/commit/254f326)) 1217 | - THE MODULE IS EDGE COMPATIBLE ([4206c5c](https://github.com/becem-gharbi/nuxt-auth/commit/4206c5c)) 1218 | - Strict nuxt compatibility to >=3.7 ([498e176](https://github.com/becem-gharbi/nuxt-auth/commit/498e176)) 1219 | - Remove test api route ([c9f412e](https://github.com/becem-gharbi/nuxt-auth/commit/c9f412e)) 1220 | - Set tag to latest ([68049b9](https://github.com/becem-gharbi/nuxt-auth/commit/68049b9)) 1221 | 1222 | ### ❤️ Contributors 1223 | 1224 | - Becem Gharbi 1225 | 1226 | ## v1.4.4 1227 | 1228 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.4.2...v1.4.4) 1229 | 1230 | ### 🩹 Fixes 1231 | 1232 | - **useAuthFetch:** Remove logout when access token not found ([a96e471](https://github.com/becem-gharbi/nuxt-auth/commit/a96e471)) 1233 | - Reset user state on fetchUser fail ([83f3130](https://github.com/becem-gharbi/nuxt-auth/commit/83f3130)) 1234 | - Fix nuxt instance not available on SSR ([fa336cb](https://github.com/becem-gharbi/nuxt-auth/commit/fa336cb)) 1235 | 1236 | ### 💅 Refactors 1237 | 1238 | - Implement same session handling from nuxt-directus ([961670c](https://github.com/becem-gharbi/nuxt-auth/commit/961670c)) 1239 | 1240 | ### 📖 Documentation 1241 | 1242 | - Update Readme ([f661642](https://github.com/becem-gharbi/nuxt-auth/commit/f661642)) 1243 | 1244 | ### 🏡 Chore 1245 | 1246 | - Remove client-side session handling code ([93e69b8](https://github.com/becem-gharbi/nuxt-auth/commit/93e69b8)) 1247 | - Disable admin API by default ([fab901a](https://github.com/becem-gharbi/nuxt-auth/commit/fab901a)) 1248 | - Rename middleware common.global to common ([8732ff1](https://github.com/becem-gharbi/nuxt-auth/commit/8732ff1)) 1249 | - **release:** V1.4.3 ([a398950](https://github.com/becem-gharbi/nuxt-auth/commit/a398950)) 1250 | - Use console.error to log errors ([8bfd0ef](https://github.com/becem-gharbi/nuxt-auth/commit/8bfd0ef)) 1251 | 1252 | ### ❤️ Contributors 1253 | 1254 | - Becem Gharbi 1255 | 1256 | ## v1.4.3 1257 | 1258 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.4.2...v1.4.3) 1259 | 1260 | ### 🩹 Fixes 1261 | 1262 | - **useAuthFetch:** Remove logout when access token not found ([a96e471](https://github.com/becem-gharbi/nuxt-auth/commit/a96e471)) 1263 | 1264 | ### 💅 Refactors 1265 | 1266 | - Implement same session handling from nuxt-directus ([961670c](https://github.com/becem-gharbi/nuxt-auth/commit/961670c)) 1267 | 1268 | ### 📖 Documentation 1269 | 1270 | - Update Readme ([f661642](https://github.com/becem-gharbi/nuxt-auth/commit/f661642)) 1271 | 1272 | ### 🏡 Chore 1273 | 1274 | - Remove client-side session handling code ([93e69b8](https://github.com/becem-gharbi/nuxt-auth/commit/93e69b8)) 1275 | - Disable admin API by default ([fab901a](https://github.com/becem-gharbi/nuxt-auth/commit/fab901a)) 1276 | - Rename middleware common.global to common ([8732ff1](https://github.com/becem-gharbi/nuxt-auth/commit/8732ff1)) 1277 | 1278 | ### ❤️ Contributors 1279 | 1280 | - Becem Gharbi 1281 | 1282 | ## v1.4.2 1283 | 1284 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.4.1...v1.4.2) 1285 | 1286 | ### 💅 Refactors 1287 | 1288 | - Return instead of <{}> on REST success ([d40e227](https://github.com/becem-gharbi/nuxt-auth/commit/d40e227)) 1289 | - Use default import from @prisma/client ([28fd331](https://github.com/becem-gharbi/nuxt-auth/commit/28fd331)) 1290 | 1291 | ### 📖 Documentation 1292 | 1293 | - Replace serverless with edge ([738c1d1](https://github.com/becem-gharbi/nuxt-auth/commit/738c1d1)) 1294 | 1295 | ### 🏡 Chore 1296 | 1297 | - Upgrade dependencies ([38eb02a](https://github.com/becem-gharbi/nuxt-auth/commit/38eb02a)) 1298 | - Create dev package version ([4359929](https://github.com/becem-gharbi/nuxt-auth/commit/4359929)) 1299 | - Update package keywords ([b44afe3](https://github.com/becem-gharbi/nuxt-auth/commit/b44afe3)) 1300 | 1301 | ### ❤️ Contributors 1302 | 1303 | - Becem Gharbi 1304 | 1305 | ## v1.4.1 1306 | 1307 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.4.0...v1.4.1) 1308 | 1309 | ### 🩹 Fixes 1310 | 1311 | - **oauth:** Check name on oauth user fetch ([ed65013](https://github.com/becem-gharbi/nuxt-auth/commit/ed65013)) 1312 | 1313 | ### 💅 Refactors 1314 | 1315 | - Remove prisma validation errors from response ([6c197ec](https://github.com/becem-gharbi/nuxt-auth/commit/6c197ec)) 1316 | 1317 | ### 🏡 Chore 1318 | 1319 | - Upgrade dependencies ([296ceb0](https://github.com/becem-gharbi/nuxt-auth/commit/296ceb0)) 1320 | 1321 | ### ❤️ Contributors 1322 | 1323 | - Becem Gharbi ([@becem-gharbi](http://github.com/becem-gharbi)) 1324 | 1325 | ## v1.4.0 1326 | 1327 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.3.5...v1.4.0) 1328 | 1329 | 1330 | ### 🚀 Enhancements 1331 | 1332 | - Add uth:loggedIn hook ([0903edf](https://github.com/becem-gharbi/nuxt-auth/commit/0903edf)) 1333 | 1334 | ### 🔥 Performance 1335 | 1336 | - Disable SSR on callback page ([2cfe78c](https://github.com/becem-gharbi/nuxt-auth/commit/2cfe78c)) 1337 | 1338 | ### 🩹 Fixes 1339 | 1340 | - **middleware:** Replace redirect from.path to to.path in auth middleware ([cbac1dd](https://github.com/becem-gharbi/nuxt-auth/commit/cbac1dd)) 1341 | - **useAuth:** Import useNuxtApp ([2256a65](https://github.com/becem-gharbi/nuxt-auth/commit/2256a65)) 1342 | 1343 | ### 📖 Documentation 1344 | 1345 | - **readme:** Add hooks section ([4a63983](https://github.com/becem-gharbi/nuxt-auth/commit/4a63983)) 1346 | 1347 | ### 🏡 Chore 1348 | 1349 | - Configure Renovate ([300048e](https://github.com/becem-gharbi/nuxt-auth/commit/300048e)) 1350 | - Replace npm with pnpm ([1b39697](https://github.com/becem-gharbi/nuxt-auth/commit/1b39697)) 1351 | 1352 | ### ❤️ Contributors 1353 | 1354 | - Becem Gharbi ([@becem-gharbi](http://github.com/becem-gharbi)) 1355 | 1356 | ## v1.3.5 1357 | 1358 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.3.6...v1.3.5) 1359 | 1360 | 1361 | ### 🏡 Chore 1362 | 1363 | - **release:** V1.3.6 ([8e1b409](https://github.com/becem-gharbi/nuxt-auth/commit/8e1b409)) 1364 | 1365 | ### ❤️ Contributors 1366 | 1367 | - Becem Gharbi ([@becem-gharbi](http://github.com/becem-gharbi)) 1368 | 1369 | ## v1.3.6 1370 | 1371 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.3.6...v1.3.6) 1372 | 1373 | ## v1.3.6 1374 | 1375 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.3.3...v1.3.6) 1376 | 1377 | 1378 | ### 🩹 Fixes 1379 | 1380 | - Fix useRoute not defined ([d3263de](https://github.com/becem-gharbi/nuxt-auth/commit/d3263de)) 1381 | 1382 | ### 🏡 Chore 1383 | 1384 | - **release:** V1.3.4 ([f93ca56](https://github.com/becem-gharbi/nuxt-auth/commit/f93ca56)) 1385 | 1386 | ### ❤️ Contributors 1387 | 1388 | - Becem Gharbi ([@becem-gharbi](http://github.com/becem-gharbi)) 1389 | 1390 | ## v1.3.4 1391 | 1392 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.3.3...v1.3.4) 1393 | 1394 | 1395 | ### 🩹 Fixes 1396 | 1397 | - Fix useRoute not defined ([d3263de](https://github.com/becem-gharbi/nuxt-auth/commit/d3263de)) 1398 | 1399 | ### ❤️ Contributors 1400 | 1401 | - Becem Gharbi ([@becem-gharbi](http://github.com/becem-gharbi)) 1402 | 1403 | ## v1.3.3 1404 | 1405 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.3.2...v1.3.3) 1406 | 1407 | 1408 | ### 🩹 Fixes 1409 | 1410 | - Remove DATABASE_URL env check ([717c4b0](https://github.com/becem-gharbi/nuxt-auth/commit/717c4b0)) 1411 | 1412 | ### 📖 Documentation 1413 | 1414 | - **readme:** Add oauth redirect URI note ([31e3681](https://github.com/becem-gharbi/nuxt-auth/commit/31e3681)) 1415 | 1416 | ### 🏡 Chore 1417 | 1418 | - Upgrade dependencies ([c2ed65f](https://github.com/becem-gharbi/nuxt-auth/commit/c2ed65f)) 1419 | 1420 | ### ❤️ Contributors 1421 | 1422 | - Becem Gharbi ([@becem-gharbi](http://github.com/becem-gharbi)) 1423 | - Becem ([@becem-gharbi](http://github.com/becem-gharbi)) 1424 | 1425 | ## v1.3.2 1426 | 1427 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.3.1...v1.3.2) 1428 | 1429 | 1430 | ### 💅 Refactors 1431 | 1432 | - Allow `id` fields to be `string` ([cebe94b](https://github.com/becem-gharbi/nuxt-auth/commit/cebe94b)) 1433 | 1434 | ### 📖 Documentation 1435 | 1436 | - Update README.md ([c7f22a1](https://github.com/becem-gharbi/nuxt-auth/commit/c7f22a1)) 1437 | - Add Mongo DB setup instructions ([f7af325](https://github.com/becem-gharbi/nuxt-auth/commit/f7af325)) 1438 | - Update readme ([745907c](https://github.com/becem-gharbi/nuxt-auth/commit/745907c)) 1439 | 1440 | ### 🌊 Types 1441 | 1442 | - Resolve `id` fields from Prisma schema ([24a7bca](https://github.com/becem-gharbi/nuxt-auth/commit/24a7bca)) 1443 | 1444 | ### 🏡 Chore 1445 | 1446 | - Upgrade dependencies ([de45f86](https://github.com/becem-gharbi/nuxt-auth/commit/de45f86)) 1447 | - Define prisma schema for Mongo DB ([8a65f05](https://github.com/becem-gharbi/nuxt-auth/commit/8a65f05)) 1448 | 1449 | ### ❤️ Contributors 1450 | 1451 | - Becem Gharbi ([@becem-gharbi](http://github.com/becem-gharbi)) 1452 | - Becem ([@becem-gharbi](http://github.com/becem-gharbi)) 1453 | 1454 | ## v1.3.1 1455 | 1456 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.3.0...v1.3.1) 1457 | 1458 | 1459 | ### 🩹 Fixes 1460 | 1461 | - **logout:** Deleted cache of fetched data on logout ([e548110](https://github.com/becem-gharbi/nuxt-auth/commit/e548110)) 1462 | 1463 | ### ❤️ Contributors 1464 | 1465 | - Becem-gharbi 1466 | 1467 | ## v1.3.0 1468 | 1469 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v0.1.6...v1.3.0) 1470 | 1471 | 1472 | ### 🚀 Enhancements 1473 | 1474 | - **admin:** Add admin API enable option ([81a61e4](https://github.com/becem-gharbi/nuxt-auth/commit/81a61e4)) 1475 | - **redirect:** On login required, return to previous page instead of home ([25324ae](https://github.com/becem-gharbi/nuxt-auth/commit/25324ae)) 1476 | 1477 | ### 💅 Refactors 1478 | 1479 | - Remove unused condition check ([34b0715](https://github.com/becem-gharbi/nuxt-auth/commit/34b0715)) 1480 | - **session:** Remove accessToken cookieName config ([84aee80](https://github.com/becem-gharbi/nuxt-auth/commit/84aee80)) 1481 | 1482 | ### 📖 Documentation 1483 | 1484 | - Add JSDoc to composables ([3637b34](https://github.com/becem-gharbi/nuxt-auth/commit/3637b34)) 1485 | - **readme:** Add serverless deployment feature ([cb1c079](https://github.com/becem-gharbi/nuxt-auth/commit/cb1c079)) 1486 | - **readme:** Add all module options to setup section ([8e73c2c](https://github.com/becem-gharbi/nuxt-auth/commit/8e73c2c)) 1487 | - **readme:** Add notes ([9be490b](https://github.com/becem-gharbi/nuxt-auth/commit/9be490b)) 1488 | - **readme:** Add explicit support to SQL db only ([5871419](https://github.com/becem-gharbi/nuxt-auth/commit/5871419)) 1489 | 1490 | ### 🏡 Chore 1491 | 1492 | - Upgrade dependencies ([82c0fd5](https://github.com/becem-gharbi/nuxt-auth/commit/82c0fd5)) 1493 | - Set version to 1.0.0 ([318b644](https://github.com/becem-gharbi/nuxt-auth/commit/318b644)) 1494 | - **release:** V1.1.0 ([447245a](https://github.com/becem-gharbi/nuxt-auth/commit/447245a)) 1495 | - Set version to 1.2.0 ([4066ce4](https://github.com/becem-gharbi/nuxt-auth/commit/4066ce4)) 1496 | 1497 | ### ❤️ Contributors 1498 | 1499 | - Becem-gharbi 1500 | 1501 | ## v0.1.6 1502 | 1503 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v0.1.5...v0.1.6) 1504 | 1505 | 1506 | ### 🚀 Enhancements 1507 | 1508 | - **session:** Store access token in localStorage ([166a58c](https://github.com/becem-gharbi/nuxt-auth/commit/166a58c)) 1509 | 1510 | ### 🩹 Fixes 1511 | 1512 | - **refresh:** Set refresh token cookie after user check ([162bb66](https://github.com/becem-gharbi/nuxt-auth/commit/162bb66)) 1513 | 1514 | ### 💅 Refactors 1515 | 1516 | - Replace bcrypt with bcryptjs, fix Cloudflare build ([ac355ba](https://github.com/becem-gharbi/nuxt-auth/commit/ac355ba)) 1517 | 1518 | ### 🏡 Chore 1519 | 1520 | - Upgrade dependencies ([192410a](https://github.com/becem-gharbi/nuxt-auth/commit/192410a)) 1521 | 1522 | ### ❤️ Contributors 1523 | 1524 | - Becem-gharbi 1525 | 1526 | ## v0.1.5 1527 | 1528 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v0.1.4...v0.1.5) 1529 | 1530 | 1531 | ### 🩹 Fixes 1532 | 1533 | - Fix runtimeConfig related warnings ([b308c73](https://github.com/becem-gharbi/nuxt-auth/commit/b308c73)) 1534 | 1535 | ### ❤️ Contributors 1536 | 1537 | - Becem-gharbi 1538 | 1539 | ## v0.1.4 1540 | 1541 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v0.1.3...v0.1.4) 1542 | 1543 | 1544 | ### 🩹 Fixes 1545 | 1546 | - Check if account suspended on refresh handler ([3d1bd58](https://github.com/becem-gharbi/nuxt-auth/commit/3d1bd58)) 1547 | - Check if account suspended on oauth callback handler ([9b35972](https://github.com/becem-gharbi/nuxt-auth/commit/9b35972)) 1548 | 1549 | ### 📖 Documentation 1550 | 1551 | - Add Graphql client authorization section to README ([e940c0d](https://github.com/becem-gharbi/nuxt-auth/commit/e940c0d)) 1552 | - Display total downloads ([a18128f](https://github.com/becem-gharbi/nuxt-auth/commit/a18128f)) 1553 | 1554 | ### ❤️ Contributors 1555 | 1556 | - Becem-gharbi 1557 | 1558 | ## v0.1.3 1559 | 1560 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v0.1.2...v0.1.3) 1561 | 1562 | 1563 | ### 💅 Refactors 1564 | 1565 | - Refactor logger messages ([884a959](https://github.com/becem-gharbi/nuxt-auth/commit/884a959)) 1566 | 1567 | ### 📖 Documentation 1568 | 1569 | - Update README ([82b9a77](https://github.com/becem-gharbi/nuxt-auth/commit/82b9a77)) 1570 | 1571 | ### ❤️ Contributors 1572 | 1573 | - Becem-gharbi 1574 | 1575 | ## v0.1.2 1576 | 1577 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v0.1.1...v0.1.2) 1578 | 1579 | 1580 | ### 🩹 Fixes 1581 | 1582 | - Check error before redirect on login ([a57e56d](https://github.com/becem-gharbi/nuxt-auth/commit/a57e56d)) 1583 | 1584 | ### 💅 Refactors 1585 | 1586 | - Redirect to logout page before fetch, on logout ([3075717](https://github.com/becem-gharbi/nuxt-auth/commit/3075717)) 1587 | 1588 | ### ❤️ Contributors 1589 | 1590 | - Becem-gharbi 1591 | 1592 | ## v0.1.1 1593 | 1594 | [compare changes](https://github.com/becem-gharbi/nuxt-auth/compare/v1.3.0-rc.10...v0.1.1) 1595 | 1596 | 1597 | ### 🏡 Chore 1598 | 1599 | - Remove semantic-release & github workflow ([eefbc9f](https://github.com/becem-gharbi/nuxt-auth/commit/eefbc9f)) 1600 | - Install changelogen ([5293fac](https://github.com/becem-gharbi/nuxt-auth/commit/5293fac)) 1601 | 1602 | ### ❤️ Contributors 1603 | 1604 | - Becem-gharbi 1605 | 1606 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 becem 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Auth 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 | A fairly complete solution to handle authentication for your Nuxt project 9 | 10 | ## Features 11 | 12 | - ✔️ Email/password authentication 13 | - ✔️ Email verification & password reset flows 14 | - ✔️ Oauth login (Google, Github ...) 15 | - ✔️ Route middleware protection 16 | - ✔️ Database agnostic 17 | - ✔️ Custom backend option 18 | - ✔️ Auth operations via `useAuth` composable 19 | - ✔️ Auto refresh of access token via `useAuthFetch` composable 20 | - ✔️ Add dynamic custom claims to access token 21 | - ✔️ Customizable email templates 22 | - ✔️ User session management via `useAuthSession` composable 23 | - ✔️ Edge deployment on Vercel, Netlify, Cloudflare ... 24 | - ✔️ Ready to use [starter](https://github.com/becem-gharbi/nuxt-starter) 25 | 26 | ## Installation 27 | 28 | Add `@bg-dev/nuxt-auth` dependency to your project 29 | 30 | ```bash 31 | npx nuxi module add @bg-dev/nuxt-auth 32 | ``` 33 | 34 | ## Documentation 35 | 36 | The documentation website can be found [here](https://nuxt-auth.bg.tn). 37 | 38 | ## License 39 | 40 | [MIT License](./LICENSE) 41 | 42 | 43 | 44 | [npm-version-src]: https://img.shields.io/npm/v/@bg-dev/nuxt-auth/latest.svg?style=flat&colorA=18181B&colorB=28CF8D 45 | [npm-version-href]: https://npmjs.com/package/@bg-dev/nuxt-auth 46 | [npm-downloads-src]: https://img.shields.io/npm/dt/@bg-dev/nuxt-auth.svg?style=flat&colorA=18181B&colorB=28CF8D 47 | [npm-downloads-href]: https://npmjs.com/package/@bg-dev/nuxt-auth 48 | [license-src]: https://img.shields.io/npm/l/@bg-dev/nuxt-auth.svg?style=flat&colorA=18181B&colorB=28CF8D 49 | [license-href]: https://npmjs.com/package/@bg-dev/nuxt-auth 50 | [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js 51 | [nuxt-href]: https://nuxt.com 52 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: 'Nuxt Auth', 6 | description: 'A fairly complete solution to handle authentication for Nuxt', 7 | themeConfig: { 8 | logo: '/logo.png', 9 | 10 | search: { 11 | provider: 'local', 12 | }, 13 | // https://vitepress.dev/reference/default-theme-config 14 | nav: [], 15 | 16 | sidebar: [ 17 | { 18 | text: 'Get Started', 19 | base: '/get-started', 20 | items: [ 21 | { text: 'Introduction', link: '/introduction' }, 22 | { text: 'Data integration', link: '/data-integration' }, 23 | { text: 'Adapters', link: '/adapters' }, 24 | ], 25 | }, 26 | { 27 | text: 'Configuration', 28 | base: '/configuration', 29 | items: [ 30 | { text: 'Tokens', link: '/tokens' }, 31 | { text: 'OAuth', link: '/oauth' }, 32 | { text: 'Registration', link: '/registration' }, 33 | { text: 'Email', link: '/email' }, 34 | { text: 'Redirection', link: '/redirection' }, 35 | ], 36 | }, 37 | { 38 | text: 'Usage', 39 | base: '/usage', 40 | items: [ 41 | { text: 'Composables', link: '/composables' }, 42 | { text: 'Middleware', link: '/middleware' }, 43 | { text: 'Hooks', link: '/hooks' }, 44 | { text: 'Utils', link: '/utils' }, 45 | ], 46 | }, 47 | { 48 | text: 'Upgrades', 49 | base: '/upgrades', 50 | items: [ 51 | { text: 'V3', link: '/v3' }, 52 | ], 53 | }, 54 | ], 55 | 56 | socialLinks: [ 57 | { icon: 'github', link: 'https://github.com/becem-gharbi/nuxt-auth' }, 58 | ], 59 | }, 60 | }) 61 | -------------------------------------------------------------------------------- /docs/configuration/email.md: -------------------------------------------------------------------------------- 1 | # Email 2 | 3 | The API for sending emails depends on the provider used. The configuration can be set through the `email` property in the `auth` configuration of your `nuxt.config` file. 4 | 5 | ::: warning Important 6 | Please note that only **HTML** messages are supported at this time. 7 | ::: 8 | 9 | ## Hook 10 | 11 | This is the default provider. It allows sending emails via a Nitro hook. 12 | 13 | ```ts [nuxt.config.ts] 14 | export default defineNuxtConfig({ 15 | // ... 16 | auth: { 17 | email: { 18 | actionTimeout: 30 * 60, // How long the action (e.g password reset) is valid. 19 | from: "", // The email address to send from. 20 | provider: { 21 | name: "hook", 22 | }, 23 | }, 24 | }, 25 | // ... 26 | }); 27 | ``` 28 | 29 | Then you will need to register a handler for `auth:email` hook to send the message. 30 | 31 | ```ts [server/plugins/email.ts] 32 | export default defineNitroPlugin((nitroApp) => { 33 | nitroApp.hooks.hook("auth:email", (from, message) => { 34 | // send message 35 | }); 36 | }); 37 | ``` 38 | 39 | ## Sendgrid 40 | 41 | If you choose to use [Sendgrid](https://sendgrid.com), you will need to provide an API key. 42 | 43 | ```ts [nuxt.config.ts] 44 | export default defineNuxtConfig({ 45 | // ... 46 | auth: { 47 | email: { 48 | from: process.env.NUXT_AUTH_EMAIL_FROM, // The email address to send from. 49 | provider: { 50 | name: "sendgrid", 51 | apiKey: process.env.NUXT_AUTH_EMAIL_PROVIDER_API_KEY, 52 | }, 53 | }, 54 | }, 55 | // ... 56 | }); 57 | ``` 58 | 59 | ## Resend 60 | 61 | If you choose to use [Resend](https://resend.com/), you will need to provide an API key. 62 | 63 | ```ts [nuxt.config.ts] 64 | export default defineNuxtConfig({ 65 | // ... 66 | auth: { 67 | email: { 68 | from: process.env.NUXT_AUTH_EMAIL_FROM, // The email address to send from. 69 | provider: { 70 | name: "resend", 71 | apiKey: process.env.NUXT_AUTH_EMAIL_PROVIDER_API_KEY, 72 | }, 73 | }, 74 | }, 75 | // ... 76 | }); 77 | ``` 78 | 79 | ## Template Customization 80 | 81 | Default [templates](https://github.com/becem-gharbi/nuxt-auth/tree/main/src/runtime/templates) are provided for email verification and password reset. To customize them, `email.templates` config option is provided. Templates can be added as HTML files with paths relative to the `srcDir`. 82 | 83 | The variables below are injected with [mustache](https://github.com/janl/mustache.js) syntax: 84 | 85 | - **name** - The user's name. 86 | - **link** - for redirection. 87 | - **validityInMinutes** - equals to `email.actionTimeout`. 88 | 89 | It's recommended to use [maily.to](https://maily.to/) to build well-designed templates. 90 | 91 | ```ts [nuxt.config.ts] 92 | export default defineNuxtConfig({ 93 | // ... 94 | auth: { 95 | email: { 96 | templates: { 97 | emailVerify: "./email_verify.html", 98 | passwordReset: "./password_reset.html", 99 | }, 100 | }, 101 | }, 102 | // ... 103 | }); 104 | ``` 105 | -------------------------------------------------------------------------------- /docs/configuration/oauth.md: -------------------------------------------------------------------------------- 1 | # OAuth login 2 | 3 | Besides the local email/password login strategy, the module supports login with OAuth2 providers such as Google, and Github. 4 | 5 | ::: warning Important 6 | Please note that `email` and `name` information are required for registration, otherwise not accessible error message will be returned. 7 | ::: 8 | 9 | #### Options 10 | 11 | The module can accept multiple OAuth2 providers via `oauth` config option: 12 | 13 | ```ts [nuxt.config.ts] 14 | export default defineNuxtConfig({ 15 | // ... 16 | auth: { 17 | oauth: { 18 | "": { 19 | clientId: "", 20 | clientSecret: "", 21 | scopes: "", 22 | authorizeUrl: "", 23 | tokenUrl: "", 24 | userUrl: "", 25 | customParams: {}, 26 | }, 27 | }, 28 | }, 29 | // ... 30 | }); 31 | ``` 32 | 33 | To login with an OAuth2 provider the module implements this flow: 34 | 35 | 1. Via `authorizeUrl`: it requests an authorization code from the provider with `scope` to get user info and `state` to maintain the redirection path of the previously visited protected page. The provider handles user authentication and consent. 36 | 2. Via `tokenUrl`: it requests an access token from the OAuth2 authorization server with the authorization `code` returned earlier. 37 | 3. Via `userUrl`: it requests user info with the access token returned earlier. The `scope` should permit getting the user `name` and `email` fields. 38 | 4. The module checks if the user exists (stored in the database), if not it registers him. 39 | 5. The module issues an access token and a refresh token for this new session. Note the tokens issued by the OAuth provider are omitted, they are only needed to get user info. 40 | 41 | The redirect URI to be set on `oauth` configuration should be the following: 42 | 43 | ```bash 44 | {baseUrl}/api/auth/login/{provider}/callback 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/configuration/redirection.md: -------------------------------------------------------------------------------- 1 | # Redirection 2 | 3 | The module offers automatic redirection to predefined routes, a feature inspired by the functionality found in the [@nuxtjs/auth-next](https://auth.nuxtjs.org) Nuxt 2 module. 4 | 5 | ## Configuration 6 | 7 | The configuration can be set through the `redirect` key in the `auth` configuration of your `nuxt.config` file. 8 | 9 | ::: warning Important 10 | Please ensure that the routes specified in the configuration exist in your application. 11 | ::: 12 | 13 | ```ts [nuxt.config.ts] 14 | export default defineNuxtConfig({ 15 | // ... 16 | auth: { 17 | redirect: { 18 | // These are example routes, please replace them with your own 19 | login: "/auth/login", 20 | logout: "/auth/login", 21 | home: "/", 22 | callback: "/auth/callback", 23 | passwordReset: "/auth/reset-password", 24 | emailVerify: "/auth/verify-email", 25 | }, 26 | }, 27 | // ... 28 | }); 29 | ``` 30 | 31 | - **`login`** - The page that the user will be redirected to if login is required. 32 | - **`logout`** - The page that the user will be redirected to after logging out. 33 | - **`home`** - The page that the user will be redirected to after a successful login. 34 | - **`callback`** - The page that the user will be redirected to after a successful login with OAuth. 35 | - **`passwordReset`** - The page that the user will be redirected to after a password reset. 36 | - **`emailVerify`** - The page that the user will be redirected to after verifying their email. 37 | -------------------------------------------------------------------------------- /docs/configuration/registration.md: -------------------------------------------------------------------------------- 1 | # Registration 2 | 3 | In the OAuth case, a new user is automatically registered. 4 | 5 | In credentials case with email/password, the module provides a `register` function that is returned by [`useAuth`](/usage/composables.html#useauth) composable to register a new user requiring essential information are: `name`, `email` and `password`. Additionally, it automatically generates a default avatar based on the provided `name`. For OAuth login scenarios, the avatar is retrieved from the respective provider whenever accessible. 6 | 7 | ## Configuration 8 | 9 | The configuration can be set via the `registration` key in the `auth` configuration of your `nuxt.config` file. 10 | 11 | ```ts [nuxt.config.ts] 12 | export default defineNuxtConfig({ 13 | // ... 14 | auth: { 15 | registration: { 16 | enabled: true, // The registration can be disabled for limited user base. 17 | requireEmailVerification: true, // Allow non-verified users. 18 | passwordValidationRegex: "^.+$", // Constraint for password strength. 19 | emailValidationRegex: "^.+$", // Constraint for email. 20 | defaultRole: "user", // Role assigned to new users. 21 | }, 22 | }, 23 | // ... 24 | }); 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/configuration/tokens.md: -------------------------------------------------------------------------------- 1 | # Tokens 2 | 3 | Tokens play a crucial role in the authorization process within the module. They serve as secure identifiers that grant access to protected resources. 4 | 5 | ## Configuration 6 | 7 | - The module employs the [`HS256`](https://www.loginradius.com/blog/engineering/jwt-signing-algorithms/#hs256) algorithm, utilizing symmetric encryption. 8 | - You have the flexibility to customize encryption options via the `auth` configuration in your `nuxt.config` file. 9 | 10 | ```ts [nuxt.config.ts] 11 | export default defineNuxtConfig({ 12 | // ... 13 | auth: { 14 | accessToken: { 15 | jwtSecret: "", // Required 16 | maxAge: 15 * 60, // The access token is valid for 15 minutes 17 | }, 18 | refreshToken: { 19 | jwtSecret: "", // Required 20 | maxAge: 7 * 24 * 60 * 60, // The refresh token is valid for 7 days 21 | cookieName: "auth_refresh_token", 22 | }, 23 | }, 24 | // ... 25 | }); 26 | ``` 27 | 28 | ### **Recommendation** 29 | 30 | While you can set the values above directly in the `nuxt.config` file, it is mandatory to store sensitive information such as `jwtSecret` in environment variables. This practice ensures that your secrets remain secure and are not exposed to the public. 31 | 32 | > You can use the command below to generate a secure secret for your JWT tokens. 33 | 34 | ```bash 35 | node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" 36 | ``` 37 | 38 | ```bash [.env] 39 | NUXT_AUTH_ACCESS_TOKEN_JWT_SECRET=your_secret 40 | NUXT_AUTH_REFRESH_TOKEN_JWT_SECRET=your_secret 41 | ``` 42 | 43 | ## Custom claims 44 | 45 | Some backend services require JWT claims for authorization such as [Hasura](https://hasura.io). To add dynamic custom claims to the access token's payload, `accessToken.customClaims` is provided. For injecting **User** related fields, use the [mustache](https://github.com/janl/mustache.js) syntax. 46 | 47 | ```ts [nuxt.config.ts] 48 | export default defineNuxtConfig({ 49 | // ... 50 | auth: { 51 | accessToken: { 52 | customClaims: { 53 | "https://hasura.io/jwt/claims": { 54 | "x-hasura-allowed-roles": ["user", "admin"], 55 | "x-hasura-default-role": "{{role}}", 56 | "x-hasura-user-id": "{{id}}", 57 | }, 58 | }, 59 | }, 60 | }, 61 | // ... 62 | }); 63 | ``` 64 | -------------------------------------------------------------------------------- /docs/get-started/adapters.md: -------------------------------------------------------------------------------- 1 | # Adapters 2 | 3 | The module provides a set of ready-to-use adapters where their source code is available [here](https://github.com/becem-gharbi/nuxt-auth/tree/main/src/runtime/server/utils/adapter). Please make sure to add their respective Nitro plugin to your application. 4 | 5 | ## Prisma 6 | 7 | The module provides a ready to use adapter for [Prisma ORM](https://prisma.io/). 8 | 9 | ```ts 10 | import { PrismaClient } from "@prisma/client"; 11 | import { usePrismaAdapter, setEventContext } from "#auth_utils"; 12 | 13 | declare module "#auth_adapter" { 14 | type Source = PrismaClient; 15 | } 16 | 17 | export default defineNitroPlugin((nitroApp) => { 18 | const prisma = new PrismaClient(); 19 | const adapter = usePrismaAdapter(prisma); 20 | nitroApp.hooks.hook("request", (event) => setEventContext(event, adapter)); 21 | }); 22 | ``` 23 | 24 | ## Unstorage 25 | 26 | The module provides a ready to use adapter for [Unstorage](https://unstorage.unjs.io/). 27 | 28 | ```ts 29 | import { useUnstorageAdapter, setEventContext } from "#auth_utils"; 30 | import type { Storage } from 'unstorage' 31 | 32 | declare module "#auth_adapter" { 33 | type Source = Storage; 34 | } 35 | 36 | export default defineNitroPlugin((nitroApp) => { 37 | const storage = useStorage() 38 | const adapter = useUnstorageAdapter(storage) 39 | 40 | nitroApp.hooks.hook("request", (event) => setEventContext(event, adapter)); 41 | } 42 | }); 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/get-started/data-integration.md: -------------------------------------------------------------------------------- 1 | # Data integration 2 | 3 | The module implements a JWT-based session so authorization is stateless. Data needs to be persisted to have control over sessions and provide user-related functionalities (e.g. registration). For that, the module offers two options: Frontend-only with backend API or Full-stack with data adapter. 4 | 5 | ## Frontend-only 6 | 7 | The module provides a frontend-only option that turns off the built-in backend by excluding the server's handlers, utilities, and middleware and permits users to bring their own. The provided backend can be internal, meaning as part of the application, or external. 8 | 9 | This feature does not affect the frontend implementation meaning the same APIs and benefits (auto-redirection, auto-refresh of token) as the full-stack implementation. 10 | 11 | The specification for the backend APIs is described [here](https://app.swaggerhub.com/apis-docs/becem-gharbi/nuxt-auth). 12 | 13 | To enable this feature, these config options should be set: 14 | 15 | - `backendEnabled` set to `false`. 16 | - `backendBaseUrl` set to `/` for internal Backend. 17 | 18 | ## Full-stack 19 | 20 | The full-stack option requires a data source adapter. An adapter is an object with methods for reading and writing the required data models. You can use a [built-in adapter](/get-started/adapters) or create your own. 21 | 22 | To create a custom adapter two models are required: 23 | 24 | - `User` to store a user's data. 25 | - `Session` to store a session's data for a specific user. 26 | 27 | ```ts 28 | interface User { 29 | id: UserId; 30 | name: string; 31 | email: string; 32 | picture: string; 33 | role: string; 34 | provider: string; 35 | verified: boolean; 36 | createdAt: Date; 37 | updatedAt: Date; 38 | suspended?: boolean | null; 39 | password?: string | null; 40 | requestedPasswordReset?: boolean | null; 41 | } 42 | 43 | interface Session { 44 | id: SessionId; 45 | uid: string; 46 | userAgent: string | null; 47 | createdAt: Date; 48 | updatedAt: Date; 49 | userId: User["id"]; 50 | } 51 | ``` 52 | 53 | By default, the `id` fields are of type `string`. To change it to `number`, the `UserId` and `SessionId` types need to be overwritten: 54 | 55 | ```ts 56 | declare module "#auth_adapter" { 57 | type UserId = number; // Change User ID type to `number` 58 | type SessionId = number; // Change Session ID type to `number` 59 | } 60 | ``` 61 | 62 | To define a new adapter, the `defineAdapter` utility is provided. The expected argument is the data source, the API you use to interact with your data (e.g. Prisma Client). 63 | 64 | ```ts 65 | interface Adapter { 66 | name: string; 67 | source: Source; 68 | 69 | user: { 70 | findById: (id: User["id"]) => Promise; 71 | findByEmail: (email: User["email"]) => Promise; 72 | create: (input: UserCreateInput) => Promise; 73 | update: (id: User["id"], input: UserUpdateInput) => Promise; 74 | }; 75 | 76 | session: { 77 | findById: ( 78 | id: Session["id"], 79 | userId: User["id"] 80 | ) => Promise; 81 | findManyByUserId: (id: User["id"]) => Promise; 82 | create: (input: SessionCreateInput) => Promise; 83 | update: (id: Session["id"], input: SessionUpdateInput) => Promise; 84 | delete: (id: Session["id"], userId: User["id"]) => Promise; 85 | deleteManyByUserId: ( 86 | userId: User["id"], 87 | excludeId?: Session["id"] // The current session's id, it should not be deleted. 88 | ) => Promise; 89 | }; 90 | } 91 | ``` 92 | 93 | To integrate your adapter within your application, the `setEventContext` utility is provided. It extends `event.context` with the `auth` property that contains the access token's payload on `auth.data` and the adapter instance on `auth.adapter`. 94 | 95 | ```ts 96 | import { defineAdapter, setEventContext } from "#auth_utils"; 97 | 98 | const useAdapter = defineAdapter((source)=> {/* */}); 99 | 100 | export default defineNitroPlugin((nitroApp) => { 101 | const adapter = useAdapter() 102 | nitroApp.hooks.hook("request", (event) => setEventContext(event, adapter)); 103 | } 104 | }); 105 | ``` 106 | 107 | On your event handlers, the data source used by the adapter can be accessed on `event.context.auth.adapter.source`. Note that it needs to be manually typed: 108 | 109 | ```ts 110 | declare module "#auth_adapter" { 111 | type Source = SourceType; 112 | } 113 | ``` 114 | -------------------------------------------------------------------------------- /docs/get-started/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Nuxt auth is an unofficial module for [Nuxt](https://nuxt.com) that aims to provide a complete solution for authentication. 4 | 5 | - Complete, as no external authentication service is required. It provides registration and login with email/password or directly via OAuth2 providers. 6 | 7 | - Data layer agnostic, it works with any data source: database, ORM, backend API. 8 | 9 | - Edge compatible, thanks to [Nitro](https://github.com/unjs/nitro) and [Jose](https://github.com/panva/jose) the module can run on edge environments. 10 | 11 | - Email customization, it provides the needed flows for email verification and password reset with customizable email templates. 12 | 13 | - Ready to use starters [nuxt-starter](https://github.com/becem-gharbi/nuxt-starter), [prisma-cloudflare](https://github.com/becem-gharbi/prisma-cloudflare). 14 | 15 | ## Installation 16 | 17 | Add `@bg-dev/nuxt-auth` to your Nuxt modules: 18 | 19 | ```bash 20 | npx nuxi module add @bg-dev/nuxt-auth 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Nuxt Auth" 7 | tagline: Authentication made easy for Nuxt 8 | actions: 9 | - theme: brand 10 | text: Get Started 11 | link: /get-started/introduction 12 | - theme: alt 13 | text: Demo 14 | link: https://nuxt-starter.bg.tn 15 | 16 | features: 17 | - title: Login with credentials 18 | icon: 🔑 19 | details: Supports login and registration with email and password. 20 | - title: Login with OAuth 21 | icon: 🌐 22 | details: Supports login via OAuth2 providers (Google, GitHub...). 23 | - title: Data layer agnostic 24 | icon: 💾 25 | details: Works with any data source (database, ORM, backend API). 26 | - title: Edge compatible 27 | icon: 🚀 28 | details: Runs on Edge workers (Cloudflare, Vercel Edge...). 29 | - title: Auto redirection 30 | icon: ↩️ 31 | details: Built-in middleware to protect page routes with auto-redirection. 32 | - title: Extensible 33 | icon: 🔌 34 | details: Provides hooks to add custom logic and actions. 35 | --- 36 | 37 | 47 | -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/becem-gharbi/nuxt-auth/a74f1499453f8f89cd5bfff97fd582704c6a697f/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/logo_original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/becem-gharbi/nuxt-auth/a74f1499453f8f89cd5bfff97fd582704c6a697f/docs/public/logo_original.png -------------------------------------------------------------------------------- /docs/upgrades/v3.md: -------------------------------------------------------------------------------- 1 | # Upgrade to v3 2 | 3 | ### ✨ New features 4 | 5 | - Add `auth:error` and `auth:registration` nitro hooks - [source](https://github.com/becem-gharbi/nuxt-auth/pull/49). 6 | - Add Email action timeout - [source](https://github.com/becem-gharbi/nuxt-auth/pull/43). 7 | - Allow usage of custom data layer - [source](https://github.com/becem-gharbi/nuxt-auth/pull/30). 8 | - Add `unstorage` database adapter - [source](https://github.com/becem-gharbi/nuxt-auth/pull/39). 9 | - Add `prisma` database adapter - [source](https://github.com/becem-gharbi/nuxt-auth/pull/38). 10 | - Add `emailValidationRegex` for email validation on registration - [source](https://github.com/becem-gharbi/nuxt-auth/pull/37). 11 | - Define types of known OAuth options for `google` and `github` - [source](https://github.com/becem-gharbi/nuxt-auth/commit/06b9f821ccb8a9703d01943b1aa1831dbf1ee716). 12 | - Add `auth:fetchError` nuxt hook - [source](https://github.com/becem-gharbi/nuxt-auth/commit/ab89ac901b6721afee7f74f52f338cb4a73809a5). 13 | - Define types of route middleware `auth` and `guest` - [source](https://github.com/becem-gharbi/nuxt-auth/pull/35). 14 | 15 | ### ⚠️ Breaking changes 16 | 17 | - Rename the `RefreshToken` data model to `Session` - [source](https://github.com/becem-gharbi/nuxt-auth/pull/51). 18 | - Change response of GET sessions endpoint - [source](https://github.com/becem-gharbi/nuxt-auth/pull/51). 19 | - Change the return value of `useAuthSession().getAllSessions` - [source](https://github.com/becem-gharbi/nuxt-auth/pull/51). 20 | - Change path of session endpoints - [source](https://github.com/becem-gharbi/nuxt-auth/pull/48). 21 | - Rename `#auth` to `#auth_utils` - [source](https://github.com/becem-gharbi/nuxt-auth/commit/d9d1bcc6bd46604bb38803cca1104f66bb02a4ef). 22 | - Change `event.context.auth` definition - [source](https://github.com/becem-gharbi/nuxt-auth/pull/45). 23 | - Set default email provider to `hook` - [source](https://github.com/becem-gharbi/nuxt-auth/pull/43). 24 | - Change types `createdAt` `updatedAt` of User state from `Date` to `string` - [source](https://github.com/becem-gharbi/nuxt-auth/commit/82fc63cb0f5b6c3f132000dc99655a21b697b9cb). 25 | - Avoid registration of server handlers when respective configuration missing - [source](https://github.com/becem-gharbi/nuxt-auth/pull/33). 26 | - Change server response error messages - [source](https://github.com/becem-gharbi/nuxt-auth/pull/32). 27 | - Only accept `.html` custom email templates - [source](https://github.com/becem-gharbi/nuxt-auth/pull/29). 28 | - Rename `registration.enable` to `registration.enabled` - [source](https://github.com/becem-gharbi/nuxt-auth/pull/28). 29 | - Remove purge of expired sessions - [source](https://github.com/becem-gharbi/nuxt-auth/pull/27). 30 | - Remove `custom` email provider - [source](https://github.com/becem-gharbi/nuxt-auth/pull/26). 31 | - Remove internal prisma instantiation - [source](https://github.com/becem-gharbi/nuxt-auth/pull/25). 32 | - Remove `useAuthFetch` composable - [source](https://github.com/becem-gharbi/nuxt-auth/pull/24). 33 | -------------------------------------------------------------------------------- /docs/usage/composables.md: -------------------------------------------------------------------------------- 1 | # Composables 2 | 3 | The module provides essential composables, facilitating seamless integration with the backend. 4 | 5 | ## `useAuth` 6 | 7 | The module provides a `useAuth` composable, which equips you the user with a comprehensive set of functions for managing authentication seamlessly within your project. 8 | 9 | - `login` - Initiates the sign-in process using email and password credentials. Upon successful authentication, redirects to the specified `redirect.home` route. 10 | - `loginWithProvider` - Enables sign-in functionality using OAuth providers. After authentication, redirects to the designated `redirect.callback` route. 11 | - `logout` - Logs the user out of the application and redirects to the configured `redirect.logout` route. 12 | - `fetchUser` - Retrieves and updates the user's information, refreshing the reactive state of `useAuthSession`. 13 | - `register` - Facilitates user registration with required inputs including email, password, and name, providing seamless integration into the authentication flow. 14 | - `requestPasswordReset` - Initiates the process for sending a password reset email to the user, allowing them to securely reset their password. 15 | - `resetPassword` - Allows the user to reset their password securely after initiating a password reset request. 16 | - `requestEmailVerify` - Triggers the sending of an email verification email, ensuring the validity of the user's email address. 17 | - `changePassword` - Enables users to securely change their current password, providing an additional layer of account security. 18 | 19 | ### Error Handling 20 | 21 | Some specific error messages are thrown by these methods. 22 | 23 | - `login` 24 | - Account suspended 25 | - Account not verified 26 | - Wrong credentials 27 | - `register` 28 | - Email already used 29 | - Account not verified 30 | - `changePassword` 31 | - Wrong password 32 | - `resetPassword` 33 | - Password reset not requested 34 | 35 | ## `useNuxtApp().$auth.fetch` 36 | 37 | This function is a wrapper of `$fetch` API, provided by Nuxt, with automatic refresh of access token. It should be used for fetching data from the server, as it automatically refreshes the access token if it's expired. 38 | 39 | ## `useAuthSession` 40 | 41 | The `useAuthSession` composable is designed for session management, refresh, and storage. 42 | 43 | - `user` - A read-only reactive state that contains information about the currently logged-in user. 44 | - `getAccessToken` - Allows retrieval of a fresh access token, automatically refreshing it if it has expired. This functionality proves useful for passing the access token in fetch calls without relying on `useNuxtApp().$auth.fetch`. 45 | - `revokeAllSessions` - Revokes all active sessions except the current one, enhancing security by invalidating unused sessions. 46 | - `revokeSession` - Enables revocation of a single session, providing fine-grained control over session management. 47 | - `getAllSessions` - Retrieves information about all active sessions, offering insights into the user's session history. 48 | -------------------------------------------------------------------------------- /docs/usage/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | To extend the functionality of the module, custom hooks have been implemented. 4 | 5 | ## Nuxt hooks 6 | 7 | ### `auth:loggedIn` 8 | 9 | This hook is triggered on login and logout events, providing the opportunity to incorporate asynchronous logic before executing redirection. 10 | 11 | ```ts 12 | export default defineNuxtPlugin({ 13 | hooks: { 14 | "auth:loggedIn": (loggedIn) => {}, 15 | }, 16 | }); 17 | ``` 18 | 19 | ### `auth:fetchError` 20 | 21 | This hook is triggered on fetch error and can be useful to globally display API errors. 22 | 23 | ```ts 24 | export default defineNuxtPlugin({ 25 | hooks: { 26 | "auth:fetchError": (response) => {}, 27 | }, 28 | }); 29 | ``` 30 | 31 | ## Nitro hooks 32 | 33 | ### `auth:error` 34 | 35 | This hook is triggered on server errors and can be useful to log and report errors. 36 | 37 | ```ts 38 | export default defineNitroPlugin((nitroApp) => { 39 | nitroApp.hooks.hook("auth:error", (error) => {}); 40 | }); 41 | ``` 42 | 43 | ### `auth:registration` 44 | 45 | This hook is triggered after successful user registration. 46 | 47 | ```ts 48 | export default defineNitroPlugin((nitroApp) => { 49 | nitroApp.hooks.hook("auth:registration", (user) => {}); 50 | }); 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/usage/middleware.md: -------------------------------------------------------------------------------- 1 | # Middleware 2 | 3 | The module comes equipped with both client-side and server-side middleware, essential for redirection and authorization functionalities. 4 | 5 | ## Client-side 6 | 7 | For seamless navigation and authorization on the client-side, the module offers two distinct approaches to page redirection, which can be utilized individually or combined for enhanced flexibility: 8 | 9 | ### Global Configuration 10 | 11 | Enable redirection globally by setting the `enableGlobalAuthMiddleware` configuration option to `true`, with the ability to override this behavior on specific pages using the `auth` page meta. 12 | 13 | ```vue 14 | 17 | ``` 18 | 19 | ### Local Configuration 20 | 21 | Fine-tune redirection behavior on a per-page basis using the `auth` and `guest` middleware. 22 | 23 | ```vue 24 | 29 | ``` 30 | 31 | ## Server-side 32 | 33 | On the server-side, authorization is seamlessly managed through access to the access token's payload via the `event.context.auth.data` property. This allows for easy validation to reject unauthorized calls. 34 | -------------------------------------------------------------------------------- /docs/usage/utils.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | The module exposes several internal server utilities that are accessible through the `#auth_utils` module: 4 | 5 | - **`sendEmail`** - Enables the sending of email messages. 6 | - **`handleError`** - Facilitates the creation and throwing of `h3` errors, or redirects with the `error` parameter passed as a query. 7 | - **`hashSync, compareSync`** - Provides functions for hashing and verifying strings using the [`bcrypt-edge`](https://github.com/bruceharrison1984/bcrypt-edge) package. 8 | - **`encode, decode`** - Offers functionality for signing and verifying JSON Web Tokens (JWT) using the [`jose`](https://github.com/panva/jose) package. 9 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat' 2 | 3 | export default createConfigForNuxt({ 4 | features: { 5 | tooling: true, 6 | stylistic: true, 7 | }, 8 | }) 9 | .overrideRules({ 10 | 'vue/multi-word-component-names': 'off', 11 | 'vue/component-name-in-template-casing': ['error', 'kebab-case', { 12 | registeredComponentsOnly: false, 13 | }], 14 | }) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bg-dev/nuxt-auth", 3 | "version": "3.0.3", 4 | "license": "MIT", 5 | "type": "module", 6 | "author": "Becem Gharbi", 7 | "homepage": "https://nuxt-auth.bg.tn", 8 | "keywords": [ 9 | "nuxt", 10 | "auth", 11 | "oauth", 12 | "edge" 13 | ], 14 | "publishConfig": { 15 | "access": "public", 16 | "tag": "latest" 17 | }, 18 | "exports": { 19 | ".": { 20 | "import": "./dist/module.mjs", 21 | "require": "./dist/module.cjs" 22 | } 23 | }, 24 | "main": "./dist/module.cjs", 25 | "types": "./dist/types.d.ts", 26 | "files": [ 27 | "dist" 28 | ], 29 | "scripts": { 30 | "dev": "nuxi dev playground", 31 | "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground", 32 | "dev:build:ssr": "cross-env NUXT_SSR=true NODE_OPTIONS=--no-deprecation nuxi build playground", 33 | "dev:build:spa": "cross-env NUXT_SSR=false NODE_OPTIONS=--no-deprecation nuxi build playground", 34 | "test": "playwright test --ui", 35 | "test:prod:ssr": "npm run dev:build:ssr && cross-env NODE_ENV=production playwright test", 36 | "test:prod:spa": "npm run dev:build:spa && cross-env NODE_ENV=production playwright test", 37 | "test:prod": "npm run test:prod:ssr && npm run test:prod:spa", 38 | "lint": "eslint .", 39 | "typecheck": "nuxi typecheck", 40 | "prepack": "nuxt-module-build", 41 | "release:pre": "npm run lint && npm run prepack", 42 | "release": "npm run release:pre && npm run test:prod && changelogen --release && npm publish && git push --follow-tags", 43 | "prisma:generate": "cd playground && prisma generate", 44 | "prisma:migrate": "cd playground && prisma migrate dev", 45 | "prisma:push": "cd playground && prisma db push", 46 | "docs:dev": "vitepress dev docs", 47 | "docs:build": "vitepress build docs", 48 | "docs:preview": "vitepress preview docs" 49 | }, 50 | "dependencies": { 51 | "@nuxt/kit": "^3.15.4", 52 | "bcrypt-edge": "^0.1.0", 53 | "defu": "^6.1.4", 54 | "jose": "^5.10.0", 55 | "zod": "^3.24.2" 56 | }, 57 | "devDependencies": { 58 | "@nuxt/eslint-config": "^1.1.0", 59 | "@nuxt/module-builder": "^0.8.4", 60 | "@nuxt/schema": "^3.15.4", 61 | "@playwright/test": "^1.50.1", 62 | "@prisma/client": "^6.4.1", 63 | "@types/node": "^22.13.4", 64 | "changelogen": "^0.5.7", 65 | "cross-env": "^7.0.3", 66 | "eslint": "^9.20.1", 67 | "nuxt": "^3.15.4", 68 | "prisma": "^6.4.1", 69 | "vitepress": "^1.6.3" 70 | }, 71 | "repository": { 72 | "url": "git+https://github.com/becem-gharbi/nuxt-auth.git" 73 | }, 74 | "peerDependencies": { 75 | "h3": "^1", 76 | "ofetch": "^1", 77 | "ufo": "^1", 78 | "unstorage": "^1" 79 | }, 80 | "packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321" 81 | } -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | prisma_data 5 | unstorage_data -------------------------------------------------------------------------------- /playground/.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /playground/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '#auth_adapter' { 2 | } 3 | 4 | export {} 5 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | 3 | export default defineNuxtConfig({ 4 | modules: ['../src/module'], 5 | 6 | ssr: process.env.NUXT_SSR !== 'false', 7 | 8 | app: { 9 | head: { 10 | title: 'Nuxt Auth', 11 | }, 12 | }, 13 | 14 | future: { 15 | compatibilityVersion: 4, 16 | }, 17 | 18 | compatibilityDate: '2025-02-21', 19 | 20 | auth: { 21 | baseUrl: 'http://localhost:3000', 22 | 23 | accessToken: { 24 | jwtSecret: process.env.AUTH_ACCESS_TOKEN_SECRET || '', 25 | maxAge: 14, 26 | customClaims: { 27 | 'https://hasura.io/jwt/claims': { 28 | 'x-hasura-allowed-roles': ['user', 'admin'], 29 | 'x-hasura-default-role': '{{role}}', 30 | 'x-hasura-user-id': '{{id}}', 31 | }, 32 | }, 33 | }, 34 | 35 | refreshToken: { 36 | jwtSecret: process.env.AUTH_REFRESH_TOKEN_SECRET || 'abc', 37 | }, 38 | 39 | oauth: { 40 | google: { 41 | clientId: process.env.AUTH_OAUTH_GOOGLE_CLIENT_ID || '', 42 | clientSecret: process.env.AUTH_OAUTH_GOOGLE_CLIENT_SECRET || '', 43 | scopes: 'email profile', 44 | authorizeUrl: 'https://accounts.google.com/o/oauth2/auth', 45 | tokenUrl: 'https://accounts.google.com/o/oauth2/token', 46 | userUrl: 'https://www.googleapis.com/oauth2/v3/userinfo', 47 | }, 48 | github: { 49 | clientId: process.env.AUTH_OAUTH_GITHUB_CLIENT_ID || '', 50 | clientSecret: process.env.AUTH_OAUTH_GITHUB_CLIENT_SECRET || '', 51 | scopes: 'user:email', 52 | authorizeUrl: 'https://github.com/login/oauth/authorize', 53 | tokenUrl: 'https://github.com/login/oauth/access_token', 54 | userUrl: 'https://api.github.com/user', 55 | }, 56 | }, 57 | 58 | email: { 59 | from: process.env.AUTH_EMAIL_FROM!, 60 | }, 61 | 62 | registration: { 63 | defaultRole: 'user', 64 | requireEmailVerification: false, 65 | }, 66 | 67 | redirect: { 68 | login: '/auth/login', 69 | logout: '/auth/login', 70 | home: '/home', 71 | callback: '/auth/callback', 72 | passwordReset: '/auth/password-reset', 73 | emailVerify: '/auth/email-verify', 74 | }, 75 | }, 76 | }) 77 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "my-module-playground" 4 | } 5 | -------------------------------------------------------------------------------- /playground/pages/auth/callback.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/pages/auth/email-verify.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/pages/auth/login.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 60 | -------------------------------------------------------------------------------- /playground/pages/auth/password-reset.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | -------------------------------------------------------------------------------- /playground/pages/auth/register.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 66 | -------------------------------------------------------------------------------- /playground/pages/home.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 81 | -------------------------------------------------------------------------------- /playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /playground/plugins/auth.ts: -------------------------------------------------------------------------------- 1 | import consola from 'consola' 2 | import { defineNuxtPlugin } from '#imports' 3 | 4 | export default defineNuxtPlugin({ 5 | hooks: { 6 | 'auth:loggedIn': (state) => { 7 | consola.info('logged in', state) 8 | }, 9 | 'auth:fetchError': (response) => { 10 | consola.info('fetch error', response?._data?.message) 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /playground/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = "file:C:\\Users\\becem\\Desktop\\Work\\nuxt-auth\\playground\\prisma_data\\.db" 11 | } 12 | 13 | model User { 14 | id String @id @default(cuid()) 15 | name String 16 | email String @unique 17 | picture String 18 | role String 19 | provider String 20 | password String? 21 | verified Boolean 22 | suspended Boolean? 23 | sessions Session[] 24 | requestedPasswordReset Boolean? 25 | createdAt DateTime @default(now()) 26 | updatedAt DateTime @updatedAt 27 | } 28 | 29 | model Session { 30 | id String @id @default(cuid()) 31 | uid String 32 | userId String 33 | user User @relation(fields: [userId], references: [id]) 34 | userAgent String? 35 | createdAt DateTime @default(now()) 36 | updatedAt DateTime @updatedAt 37 | 38 | @@index([userId]) 39 | } 40 | -------------------------------------------------------------------------------- /playground/server/plugins/hooks.ts: -------------------------------------------------------------------------------- 1 | import type { NitroApp } from 'nitropack/types' 2 | import consola from 'consola' 3 | import { defineNitroPlugin } from 'nitropack/runtime' 4 | 5 | export default defineNitroPlugin((nitroApp: NitroApp) => { 6 | nitroApp.hooks.hook('auth:email', (from, message) => { 7 | consola.info('Email', from, message) 8 | }) 9 | 10 | nitroApp.hooks.hook('auth:registration', (user) => { 11 | consola.info('Registration', user) 12 | }) 13 | 14 | nitroApp.hooks.hook('auth:error', consola.error) 15 | }) 16 | -------------------------------------------------------------------------------- /playground/server/plugins/prisma.ts: -------------------------------------------------------------------------------- 1 | import type { NitroApp } from 'nitropack/types' 2 | import { PrismaClient } from '@prisma/client' 3 | import consola from 'consola' 4 | import { defineNitroPlugin } from 'nitropack/runtime' 5 | import { usePrismaAdapter, setEventContext } from '#auth_utils' 6 | 7 | export default defineNitroPlugin((nitroApp: NitroApp) => { 8 | if (process.env.NUXT_ADAPTER === 'prisma') { 9 | consola.success('Running with Prisma adapter') 10 | 11 | const prisma = new PrismaClient() 12 | 13 | const adapter = usePrismaAdapter(prisma) 14 | 15 | nitroApp.hooks.hook('request', event => setEventContext(event, adapter)) 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /playground/server/plugins/unstorage.ts: -------------------------------------------------------------------------------- 1 | import type { NitroApp } from 'nitropack/types' 2 | import { createStorage } from 'unstorage' 3 | // import fsDriver from 'unstorage/drivers/fs' 4 | import consola from 'consola' 5 | import { defineNitroPlugin } from 'nitropack/runtime' 6 | import { useUnstorageAdapter, setEventContext } from '#auth_utils' 7 | 8 | export default defineNitroPlugin((nitroApp: NitroApp) => { 9 | if (process.env.NUXT_ADAPTER === 'unstorage') { 10 | consola.success('Running with Unstorage adapter') 11 | 12 | // const storage = createStorage({ 13 | // driver: fsDriver({ base: './playground/unstorage_data' }), 14 | // }) 15 | 16 | const storage = createStorage() 17 | 18 | const adapter = useUnstorageAdapter(storage) 19 | 20 | nitroApp.hooks.hook('request', event => setEventContext(event, adapter)) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | /** 4 | * See https://playwright.dev/docs/test-configuration. 5 | */ 6 | export default defineConfig({ 7 | testDir: './tests', 8 | /* Run tests in files in parallel */ 9 | fullyParallel: true, 10 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 11 | forbidOnly: !!process.env.CI, 12 | /* Retry on CI only */ 13 | retries: process.env.CI ? 2 : 0, 14 | /* Opt out of parallel tests on CI. */ 15 | workers: process.env.CI ? 1 : undefined, 16 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 17 | reporter: 'html', 18 | 19 | /* Run your local dev server before starting the tests */ 20 | webServer: { 21 | command: process.env.NODE_ENV === 'production' ? 'nuxi preview playground' : 'nuxi dev playground', 22 | url: 'http://localhost:3000', 23 | reuseExistingServer: !process.env.CI, 24 | timeout: 120000, 25 | }, 26 | 27 | use: { 28 | baseURL: 'http://localhost:3000', 29 | trace: 'on-first-retry', 30 | }, 31 | 32 | /* Configure projects for major browsers */ 33 | projects: [ 34 | { 35 | name: 'register', 36 | teardown: 'logout', 37 | testMatch: /register\.setup\.ts/, 38 | use: { ...devices['Desktop Chrome'] }, 39 | }, 40 | { 41 | name: 'logout', 42 | testMatch: /logout\.teardown\.ts/, 43 | use: { ...devices['Desktop Chrome'] }, 44 | }, 45 | { 46 | name: 'chromium', 47 | dependencies: ['register'], 48 | use: { ...devices['Desktop Chrome'] }, 49 | }, 50 | ], 51 | }) 52 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtModule } from '@nuxt/kit' 2 | import { name, version } from '../package.json' 3 | import type { PrivateConfig, PublicConfig } from './runtime/types/config' 4 | import { setupBackend } from './setup_backend' 5 | import { setupFrontend } from './setup_frontend' 6 | 7 | export type ModuleOptions = PrivateConfig & PublicConfig 8 | 9 | export default defineNuxtModule({ 10 | meta: { 11 | name, 12 | version, 13 | configKey: 'auth', 14 | compatibility: { 15 | nuxt: '>=3.8.2', 16 | }, 17 | }, 18 | 19 | defaults: { 20 | backendEnabled: true, 21 | backendBaseUrl: '/', 22 | baseUrl: '', 23 | enableGlobalAuthMiddleware: false, 24 | loggedInFlagName: 'auth_logged_in', 25 | accessToken: { 26 | jwtSecret: '', 27 | maxAge: 15 * 60, 28 | }, 29 | refreshToken: { 30 | cookieName: 'auth_refresh_token', 31 | jwtSecret: '', 32 | maxAge: 7 * 24 * 60 * 60, 33 | }, 34 | redirect: { 35 | login: '', 36 | logout: '', 37 | home: '', 38 | callback: '', 39 | passwordReset: '', 40 | emailVerify: '', 41 | }, 42 | registration: { 43 | enabled: true, 44 | defaultRole: 'user', 45 | requireEmailVerification: true, 46 | passwordValidationRegex: '^.+$', 47 | emailValidationRegex: '^.+$', 48 | }, 49 | email: { 50 | actionTimeout: 30 * 60, 51 | from: '', 52 | provider: { 53 | name: 'hook', 54 | }, 55 | }, 56 | }, 57 | 58 | setup(options, nuxt) { 59 | setupFrontend(options, nuxt) 60 | 61 | if (options.backendEnabled) { 62 | setupBackend(options, nuxt) 63 | } 64 | }, 65 | }) 66 | -------------------------------------------------------------------------------- /src/runtime/composables/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { joinURL } from 'ufo' 2 | import type { ResponseOK, AuthenticationData } from '../types/common' 3 | import type { PublicConfig } from '../types/config' 4 | import { useAuthToken } from './useAuthToken' 5 | import { useRuntimeConfig, useRoute, useAuthSession, navigateTo, useNuxtApp } from '#imports' 6 | import type { User } from '#auth_adapter' 7 | 8 | interface LoginInput { 9 | email: string 10 | password: string 11 | } 12 | 13 | interface RegisterInput { 14 | email: string 15 | password: string 16 | name: string 17 | } 18 | 19 | interface ChangePasswordInput { 20 | currentPassword: string 21 | newPassword: string 22 | } 23 | 24 | export function useAuth() { 25 | const publicConfig = useRuntimeConfig().public.auth as PublicConfig 26 | const token = useAuthToken() 27 | const nuxtApp = useNuxtApp() 28 | 29 | /** 30 | * Asynchronously logs in the user with the provided email and password. 31 | * 32 | * @param {LoginInput} input - The login input object containing the email and password. 33 | * @return {Promise} A promise that resolves to the authentication data if the login is successful. 34 | */ 35 | async function login(input: LoginInput): Promise { 36 | const res = await $fetch('/api/auth/login', { 37 | baseURL: publicConfig.backendBaseUrl, 38 | method: 'POST', 39 | credentials: 'include', 40 | body: { 41 | email: input.email, 42 | password: input.password, 43 | }, 44 | async onResponseError({ response }) { 45 | await nuxtApp.callHook('auth:fetchError', response) 46 | }, 47 | }) 48 | 49 | token.value = { 50 | access_token: res.access_token, 51 | expires: new Date().getTime() + res.expires_in * 1000, 52 | } 53 | await _onLogin() 54 | 55 | return res 56 | } 57 | 58 | /** 59 | * Asynchronously logs in the user with the specified oauth provider. 60 | * 61 | * @param {User['provider']} provider - The oauth provider to log in with. 62 | * @return {Promise} A promise that resolves when the login is complete. 63 | */ 64 | async function loginWithProvider(provider: User['provider']): Promise { 65 | // The protected page the user has visited before redirect to login page 66 | const returnToPath = useRoute().query.redirect?.toString() 67 | 68 | await navigateTo({ 69 | path: joinURL(publicConfig.backendBaseUrl!, '/api/auth/login', provider), 70 | query: { 71 | redirect: returnToPath, 72 | }, 73 | }, 74 | { 75 | external: true, 76 | }) 77 | } 78 | 79 | /** 80 | * Fetches the user data. 81 | * If the fetch is successful, the user value is updated with the fetched user data. 82 | * If an error occurs during the fetch, the user value is set to null. 83 | * 84 | * @return {Promise} A Promise that resolves when the user data is fetched or an error occurs. 85 | */ 86 | async function fetchUser(): Promise { 87 | const { user } = useAuthSession() 88 | try { 89 | user.value = await nuxtApp.$auth.fetch('/api/auth/me') 90 | } 91 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 92 | catch (err) { 93 | user.value = null 94 | } 95 | } 96 | 97 | /** 98 | * Logs the user out. 99 | * 100 | * @return {Promise} A promise that resolves when the logout request is complete. 101 | */ 102 | async function logout(): Promise { 103 | await $fetch('/api/auth/logout', { 104 | baseURL: publicConfig.backendBaseUrl, 105 | method: 'POST', 106 | credentials: 'include', 107 | async onResponseError({ response }) { 108 | await nuxtApp.callHook('auth:fetchError', response) 109 | }, 110 | }).finally(_onLogout) 111 | } 112 | 113 | /** 114 | * Logs the user in by fetching the user data, checking if the user is logged in, 115 | * and redirecting to the specified page after calling the 'auth:loggedIn' hook. 116 | * 117 | * @return {Promise} A promise that resolves when the login process is complete. 118 | */ 119 | async function _onLogin(): Promise { 120 | await fetchUser() 121 | if (useAuthSession().user.value === null) { 122 | return 123 | } 124 | const returnToPath = useRoute().query.redirect?.toString() 125 | const redirectTo = returnToPath ?? publicConfig.redirect.home 126 | await nuxtApp.callHook('auth:loggedIn', true) 127 | await navigateTo(redirectTo) 128 | } 129 | 130 | /** 131 | * Logs the user out by calling the 'auth:loggedIn' hook with the value 'false', 132 | * setting the token value to null, and navigating to the logout page if the code 133 | * is running on the client side. 134 | * 135 | * @return {Promise} A promise that resolves when the logout process is complete. 136 | */ 137 | async function _onLogout(): Promise { 138 | await nuxtApp.callHook('auth:loggedIn', false) 139 | token.value = null 140 | if (import.meta.client) { 141 | await navigateTo(publicConfig.redirect.logout, { external: true }) 142 | } 143 | } 144 | 145 | /** 146 | * Registers a new user by sending a POST request to the '/api/auth/register' endpoint. 147 | * 148 | * @param {RegisterInput} input - The input object containing the user's name, email, and password. 149 | * @return {Promise} - A promise that resolves to a ResponseOK object if the registration is successful. 150 | */ 151 | async function register(input: RegisterInput): Promise { 152 | return await $fetch('/api/auth/register', { 153 | baseURL: publicConfig.backendBaseUrl, 154 | method: 'POST', 155 | body: { 156 | name: input.name, 157 | email: input.email, 158 | password: input.password, 159 | }, 160 | credentials: 'omit', 161 | async onResponseError({ response }) { 162 | await nuxtApp.callHook('auth:fetchError', response) 163 | }, 164 | }) 165 | } 166 | 167 | /** 168 | * Sends a request to reset the user's password. 169 | * 170 | * @param {string} email - The email address of the user. 171 | * @return {Promise} A Promise that resolves to the response from the server. 172 | */ 173 | async function requestPasswordReset(email: string): Promise { 174 | return await $fetch('/api/auth/password/request', { 175 | baseURL: publicConfig.backendBaseUrl, 176 | method: 'POST', 177 | credentials: 'omit', 178 | body: { 179 | email, 180 | }, 181 | async onResponseError({ response }) { 182 | await nuxtApp.callHook('auth:fetchError', response) 183 | }, 184 | }) 185 | } 186 | 187 | /** 188 | * Resets the user's password. 189 | * 190 | * @param {string} password - The new password for the user. 191 | * @return {Promise} A Promise that resolves to the response from the server. 192 | */ 193 | async function resetPassword(password: string): Promise { 194 | return await $fetch('/api/auth/password/reset', { 195 | baseURL: publicConfig.backendBaseUrl, 196 | method: 'PUT', 197 | credentials: 'omit', 198 | body: { 199 | password, 200 | token: useRoute().query.token, 201 | }, 202 | async onResponseError({ response }) { 203 | await nuxtApp.callHook('auth:fetchError', response) 204 | }, 205 | }) 206 | } 207 | 208 | /** 209 | * Sends a request to verify the user's email address. 210 | * 211 | * @param {string} email - The email address of the user. 212 | * @return {Promise} A Promise that resolves to the response from the server. 213 | */ 214 | async function requestEmailVerify(email: string): Promise { 215 | return await $fetch('/api/auth/email/request', { 216 | baseURL: publicConfig.backendBaseUrl, 217 | method: 'POST', 218 | credentials: 'omit', 219 | body: { 220 | email, 221 | }, 222 | async onResponseError({ response }) { 223 | await nuxtApp.callHook('auth:fetchError', response) 224 | }, 225 | }) 226 | } 227 | 228 | /** 229 | * Changes the user's password. 230 | * 231 | * @param {ChangePasswordInput} input - An object containing the current password and the new password. 232 | * @return {Promise} - A promise that resolves to a ResponseOK object if the password change is successful. 233 | */ 234 | function changePassword(input: ChangePasswordInput): Promise { 235 | return nuxtApp.$auth.fetch('/api/auth/password/change', { 236 | method: 'PUT', 237 | body: { 238 | currentPassword: input.currentPassword, 239 | newPassword: input.newPassword, 240 | }, 241 | }) 242 | } 243 | 244 | return { 245 | login, 246 | loginWithProvider, 247 | fetchUser, 248 | logout, 249 | register, 250 | requestPasswordReset, 251 | resetPassword, 252 | requestEmailVerify, 253 | changePassword, 254 | _onLogout, 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/runtime/composables/useAuthSession.ts: -------------------------------------------------------------------------------- 1 | import { deleteCookie, getCookie, splitCookiesString, appendResponseHeader } from 'h3' 2 | import type { Ref } from 'vue' 3 | import type { ResponseOK, AuthenticationData } from '../types/common' 4 | import type { PublicConfig } from '../types/config' 5 | import { useAuthToken } from './useAuthToken' 6 | import { useRequestEvent, useRuntimeConfig, useState, useRequestHeaders, useNuxtApp, useAuth } from '#imports' 7 | import type { User, Session } from '#auth_adapter' 8 | 9 | export function useAuthSession() { 10 | const event = useRequestEvent() 11 | const publicConfig = useRuntimeConfig().public.auth as PublicConfig 12 | const nuxtApp = useNuxtApp() 13 | 14 | const _refreshToken = { 15 | get: () => import.meta.server && getCookie(event!, publicConfig.refreshToken.cookieName!), 16 | clear: () => import.meta.server && deleteCookie(event!, publicConfig.refreshToken.cookieName!), 17 | } 18 | 19 | const _loggedInFlag = { 20 | get value() { 21 | return import.meta.client ? localStorage.getItem(publicConfig.loggedInFlagName!) === 'true' : false 22 | }, 23 | set value(value: boolean) { 24 | if (import.meta.client) { 25 | localStorage.setItem(publicConfig.loggedInFlagName!, value.toString()) 26 | } 27 | }, 28 | } 29 | 30 | const user: Ref = useState('auth-user', () => null) 31 | 32 | /** 33 | * Asynchronously refreshes the authentication session. 34 | * If the request is successful, updates the access token and its expiration time. 35 | * If the request fails, clears the refresh token and logs out the user. 36 | * 37 | * @return {Promise} A promise that resolves when the refresh operation is complete. 38 | */ 39 | async function _refresh(): Promise { 40 | async function handler() { 41 | const token = useAuthToken() 42 | const reqHeaders = useRequestHeaders(['cookie', 'user-agent']) 43 | const { _onLogout } = useAuth() 44 | 45 | await $fetch 46 | .raw('/api/auth/sessions/refresh', { 47 | baseURL: publicConfig.backendBaseUrl, 48 | method: 'POST', 49 | // Cloudflare Workers does not support "credentials" field 50 | ...(import.meta.client ? { credentials: 'include' } : {}), 51 | headers: import.meta.server ? reqHeaders : {}, 52 | async onResponseError({ response }) { 53 | await nuxtApp.callHook('auth:fetchError', response) 54 | }, 55 | }) 56 | .then((res) => { 57 | if (import.meta.server) { 58 | const cookies = splitCookiesString(res.headers.get('set-cookie') ?? '') 59 | 60 | for (const cookie of cookies) { 61 | appendResponseHeader(event!, 'set-cookie', cookie) 62 | } 63 | } 64 | 65 | if (res._data) { 66 | token.value = { 67 | access_token: res._data.access_token, 68 | expires: new Date().getTime() + res._data.expires_in * 1000, 69 | } 70 | } 71 | }) 72 | .catch(async () => { 73 | _refreshToken.clear() 74 | await _onLogout() 75 | }) 76 | } 77 | 78 | nuxtApp.$auth._refreshPromise ||= handler() 79 | await nuxtApp.$auth._refreshPromise.finally(() => { 80 | nuxtApp.$auth._refreshPromise = null 81 | }) 82 | } 83 | 84 | /** 85 | * Retrieves the access token. 86 | * 87 | * @return {Promise} The access token, or null if it is expired and cannot be refreshed, or undefined if the token is not set. 88 | */ 89 | async function getAccessToken(): Promise { 90 | const token = useAuthToken() 91 | 92 | if (token.expired) { 93 | await _refresh() 94 | } 95 | 96 | return token.value?.access_token 97 | } 98 | 99 | /** 100 | * Revokes all active sessions except the current one, enhancing security by invalidating unused sessions. 101 | * 102 | * @return {Promise} A promise that resolves with a ResponseOK object upon successful revocation of all sessions. 103 | */ 104 | function revokeAllSessions(): Promise { 105 | return nuxtApp.$auth.fetch('/api/auth/sessions', { 106 | method: 'DELETE', 107 | }) 108 | } 109 | 110 | /** 111 | * Revokes a single stored session of the active user. 112 | * 113 | * @param {Session['id']} id - The ID of the session to revoke. 114 | * @return {Promise} A promise that resolves with a ResponseOK object upon successful revocation of the session. 115 | */ 116 | function revokeSession(id: Session['id']): Promise { 117 | return nuxtApp.$auth.fetch(`/api/auth/sessions/${id}`, { 118 | method: 'DELETE', 119 | }) 120 | } 121 | 122 | /** 123 | * Retrieves information about all active sessions, offering insights into the user's session history. 124 | * 125 | * @return {Promise>} A promise that resolves with an array of Session objects representing all active sessions. The current session is moved to the top of the array. 126 | */ 127 | async function getAllSessions(): Promise> { 128 | const res = await nuxtApp.$auth.fetch<{ active: Session[], current?: Session }>('/api/auth/sessions') 129 | 130 | const sessions = res.active.filter(session => session.id !== res.current?.id) 131 | 132 | if (res.current) { 133 | sessions.unshift(res.current) 134 | } 135 | 136 | return sessions.map((session, index) => ({ current: index === 0, ...session })) 137 | } 138 | 139 | return { 140 | _refreshToken, 141 | _loggedInFlag, 142 | user, 143 | _refresh, 144 | getAccessToken, 145 | revokeAllSessions, 146 | revokeSession, 147 | getAllSessions, 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/runtime/composables/useAuthToken.ts: -------------------------------------------------------------------------------- 1 | import { useState } from '#imports' 2 | 3 | interface TokenStore { 4 | access_token: string 5 | expires: number 6 | } 7 | 8 | function memoryStorage() { 9 | let store: TokenStore | null = null 10 | 11 | return { 12 | get value() { 13 | return store 14 | }, 15 | set value(data: TokenStore | null) { 16 | if (import.meta.client) { 17 | store = data 18 | } 19 | }, 20 | } 21 | } 22 | 23 | const memory = memoryStorage() 24 | 25 | /** 26 | * This composable permits the storage of access token in memory 27 | * On server-side, it's stored with `useState`. On client-side its stored in a scoped memory. 28 | * Given that `useState` is accessible on global context, it's cleared on client-side. 29 | */ 30 | export function useAuthToken() { 31 | const state = useState('auth-token', () => null) 32 | 33 | if (import.meta.client && state.value) { 34 | memory.value = { ...state.value } 35 | state.value = null 36 | } 37 | 38 | return { 39 | get value() { 40 | return import.meta.client ? memory.value : state.value 41 | }, 42 | 43 | set value(data: TokenStore | null) { 44 | if (import.meta.client) { 45 | memory.value = data 46 | } 47 | else { 48 | state.value = data 49 | } 50 | }, 51 | 52 | get expired() { 53 | if (this.value) { 54 | const msRefreshBeforeExpires = 10000 55 | const expires = this.value.expires - msRefreshBeforeExpires 56 | return expires < Date.now() 57 | } 58 | return false 59 | }, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/runtime/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { useAuthToken } from '../composables/useAuthToken' 2 | import type { PublicConfig } from '../types/config' 3 | import { defineNuxtRouteMiddleware, useRuntimeConfig, navigateTo } from '#imports' 4 | 5 | export default defineNuxtRouteMiddleware((to) => { 6 | const publicConfig = useRuntimeConfig().public.auth as PublicConfig 7 | 8 | if ( 9 | to.path === publicConfig.redirect.login 10 | || to.path === publicConfig.redirect.callback 11 | ) { 12 | return 13 | } 14 | 15 | const isPageFound = to.matched.length > 0 16 | const isAuthDisabled = publicConfig.enableGlobalAuthMiddleware && to.meta.auth === false 17 | 18 | if (isAuthDisabled || (to.meta.middleware === 'guest') || (!isPageFound && import.meta.server)) { 19 | return 20 | } 21 | 22 | if (!useAuthToken().value) { 23 | return navigateTo({ 24 | path: publicConfig.redirect.login, 25 | query: { redirect: to.fullPath }, 26 | }) 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /src/runtime/middleware/common.ts: -------------------------------------------------------------------------------- 1 | import { useAuthToken } from '../composables/useAuthToken' 2 | import type { PublicConfig } from '../types/config' 3 | import { defineNuxtRouteMiddleware, useRuntimeConfig, navigateTo } from '#imports' 4 | 5 | export default defineNuxtRouteMiddleware((to, from) => { 6 | const publicConfig = useRuntimeConfig().public.auth as PublicConfig 7 | 8 | if ( 9 | to.path === publicConfig.redirect.login 10 | || to.path === publicConfig.redirect.callback 11 | ) { 12 | if (useAuthToken().value) { 13 | const returnToPath = from.query.redirect?.toString() 14 | const redirectTo = returnToPath ?? publicConfig.redirect.home 15 | return navigateTo(redirectTo) 16 | } 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /src/runtime/middleware/guest.ts: -------------------------------------------------------------------------------- 1 | import { useAuthToken } from '../composables/useAuthToken' 2 | import type { PublicConfig } from '../types/config' 3 | import { defineNuxtRouteMiddleware, useRuntimeConfig, navigateTo } from '#imports' 4 | 5 | export default defineNuxtRouteMiddleware((to) => { 6 | const publicConfig = useRuntimeConfig().public.auth as PublicConfig 7 | 8 | if ( 9 | to.path === publicConfig.redirect.login 10 | || to.path === publicConfig.redirect.callback 11 | ) { 12 | return 13 | } 14 | 15 | if (useAuthToken().value) { 16 | return navigateTo(publicConfig.redirect.home) 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /src/runtime/plugins/flow.ts: -------------------------------------------------------------------------------- 1 | import { useAuthToken } from '../composables/useAuthToken' 2 | import type { PublicConfig } from '../types/config' 3 | import { defineNuxtPlugin, useAuth, useRouter, useAuthSession } from '#imports' 4 | 5 | export default defineNuxtPlugin({ 6 | name: 'auth:flow', 7 | enforce: 'post', 8 | dependsOn: ['auth:provider'], 9 | 10 | setup: async (nuxtApp) => { 11 | const publicConfig = nuxtApp.$config.public.auth as PublicConfig 12 | const router = useRouter() 13 | const token = useAuthToken() 14 | const { _loggedInFlag } = useAuthSession() 15 | 16 | nuxtApp.hook('auth:loggedIn', (state) => { 17 | _loggedInFlag.value = state 18 | }) 19 | 20 | /** 21 | * Makes sure to sync login status between tabs 22 | */ 23 | nuxtApp.hook('app:mounted', () => { 24 | window.onstorage = (event) => { 25 | if (event.key === publicConfig.loggedInFlagName) { 26 | if (event.oldValue === 'true' && event.newValue === 'false' && token.value) { 27 | useAuth()._onLogout() 28 | } 29 | else if (event.oldValue === 'false' && event.newValue === 'true') { 30 | location.reload() 31 | } 32 | } 33 | } 34 | }) 35 | 36 | function isFirstTime() { 37 | const isPageFound = router.currentRoute.value?.matched.length > 0 38 | const isPrerenderd = typeof nuxtApp.payload.prerenderedAt === 'number' 39 | const isServerRendered = nuxtApp.payload.serverRendered 40 | const isServerValid = import.meta.server && !isPrerenderd && isPageFound 41 | const isClientValid = import.meta.client && (!isServerRendered || isPrerenderd || !isPageFound) 42 | return isServerValid || isClientValid 43 | } 44 | 45 | function canFetchUser() { 46 | const isCallback = router.currentRoute.value?.path === publicConfig.redirect.callback 47 | const isCallbackValid = isCallback && !router.currentRoute.value?.query.error 48 | const isRefreshTokenExists = !!useAuthSession()._refreshToken.get() 49 | return isCallbackValid || _loggedInFlag.value || isRefreshTokenExists 50 | } 51 | 52 | /** 53 | * Makes sure to refresh access token and set user state if possible (run once) 54 | */ 55 | if (isFirstTime() && canFetchUser()) { 56 | await useAuthSession()._refresh() 57 | if (token.value) { 58 | await useAuth().fetchUser() 59 | } 60 | } 61 | 62 | /** 63 | * Calls loggedIn hook and sets the loggedIn flag in localStorage 64 | */ 65 | if (token.value) { 66 | await nuxtApp.callHook('auth:loggedIn', true) 67 | } 68 | else { 69 | _loggedInFlag.value = false 70 | } 71 | }, 72 | }) 73 | -------------------------------------------------------------------------------- /src/runtime/plugins/provider.ts: -------------------------------------------------------------------------------- 1 | import { defu } from 'defu' 2 | import type { PublicConfig } from '../types/config' 3 | import { defineNuxtPlugin, useAuthSession, useRequestHeaders } from '#imports' 4 | 5 | export default defineNuxtPlugin({ 6 | name: 'auth:provider', 7 | enforce: 'post', 8 | 9 | setup: (nuxtApp) => { 10 | const publicConfig = nuxtApp.$config.public.auth as PublicConfig 11 | const reqHeaders = useRequestHeaders(['user-agent']) 12 | 13 | /** 14 | * A $fetch instance with auto authorization handler 15 | */ 16 | const fetch = $fetch.create({ 17 | baseURL: publicConfig.backendBaseUrl, 18 | 19 | async onRequest({ options }) { 20 | const accessToken = await useAuthSession().getAccessToken() 21 | 22 | if (accessToken) { 23 | options.headers = defu(options.headers, reqHeaders, { 24 | authorization: 'Bearer ' + accessToken, 25 | }) 26 | } 27 | 28 | options.credentials ||= 'omit' 29 | }, 30 | 31 | async onResponseError({ response }) { 32 | await nuxtApp.callHook('auth:fetchError', response) 33 | }, 34 | }) 35 | 36 | return { 37 | provide: { 38 | auth: { 39 | fetch, 40 | _refreshPromise: null, 41 | }, 42 | }, 43 | } 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /src/runtime/server/api/avatar.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, getValidatedQuery, setResponseHeaders } from 'h3' 2 | import { z } from 'zod' 3 | 4 | export default defineEventHandler(async (event) => { 5 | const schema = z.object({ 6 | name: z.string().min(1).max(20), 7 | color: z.string().regex(/^([a-f0-9]{6}|[a-f0-9]{3})$/).optional(), 8 | background: z.string().regex(/^([a-f0-9]{6}|[a-f0-9]{3})$/).optional(), 9 | }) 10 | 11 | const query = await getValidatedQuery(event, schema.parse) 12 | 13 | query.background ||= 'f0e9e9' 14 | query.color ||= '8b5d5d' 15 | 16 | setResponseHeaders(event, { 17 | 'Content-Type': 'image/svg+xml', 18 | 'Cache-Control': 'public, max-age=2592000, immutable', 19 | }) 20 | 21 | return ` 22 | 23 | 24 | 25 | ${query.name[0]!.toUpperCase()} 26 | 27 | 28 | ` 29 | }) 30 | -------------------------------------------------------------------------------- /src/runtime/server/api/email/request.post.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, readValidatedBody } from 'h3' 2 | import { resolveURL, withQuery } from 'ufo' 3 | import { z } from 'zod' 4 | import { mustache, getConfig, sendMail, createEmailVerifyToken, handleError } from '../../utils' 5 | 6 | export default defineEventHandler(async (event) => { 7 | const config = getConfig() 8 | 9 | try { 10 | const schema = z.object({ 11 | email: z.string().email().max(40), 12 | }) 13 | 14 | const { email } = await readValidatedBody(event, schema.parse) 15 | 16 | const user = await event.context.auth.adapter.user.findByEmail(email) 17 | 18 | if (user && !user.verified) { 19 | const emailVerifyToken = await createEmailVerifyToken({ 20 | userId: user.id, 21 | }) 22 | 23 | const redirectUrl = resolveURL(config.public.baseUrl, '/api/auth/email/verify') 24 | 25 | const link = withQuery(redirectUrl, { token: emailVerifyToken }) 26 | 27 | await sendMail({ 28 | to: user.email, 29 | subject: 'Email verification', 30 | html: mustache.render( 31 | config.private.email!.templates!.emailVerify!, 32 | { 33 | ...user, 34 | link, 35 | validityInMinutes: Math.round( 36 | config.private.email!.actionTimeout! / 60, 37 | ), 38 | }, 39 | ), 40 | }) 41 | } 42 | 43 | return { status: 'ok' } 44 | } 45 | catch (error) { 46 | await handleError(error) 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /src/runtime/server/api/email/verify.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, getValidatedQuery, sendRedirect } from 'h3' 2 | import { z } from 'zod' 3 | import { getConfig, verifyEmailVerifyToken, handleError } from '../../utils' 4 | 5 | export default defineEventHandler(async (event) => { 6 | const config = getConfig() 7 | 8 | try { 9 | const schema = z.object({ 10 | token: z.string().min(1), 11 | }) 12 | 13 | const { token } = await getValidatedQuery(event, schema.parse) 14 | 15 | const payload = await verifyEmailVerifyToken(token) 16 | 17 | await event.context.auth.adapter.user.update(payload.userId, { verified: true }) 18 | 19 | await sendRedirect(event, config.public.redirect.emailVerify!) 20 | } 21 | catch (error) { 22 | await handleError(error, { 23 | event, 24 | url: config.public.redirect.emailVerify!, 25 | }) 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /src/runtime/server/api/login/[provider].get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, sendRedirect, getValidatedQuery, getValidatedRouterParams } from 'h3' 2 | import { resolveURL, withQuery } from 'ufo' 3 | import { z } from 'zod' 4 | import { getConfig, handleError } from '../../utils' 5 | 6 | export default defineEventHandler(async (event) => { 7 | const config = getConfig() 8 | 9 | const providers = config.private.oauth ? Object.keys(config.private.oauth) : [] 10 | 11 | const pSchema = z.object({ 12 | provider: z.custom(value => providers.includes(value)), 13 | }) 14 | 15 | const { provider } = await getValidatedRouterParams(event, pSchema.parse) 16 | 17 | const qSchema = z.object({ 18 | redirect: z.string().startsWith('/').optional(), 19 | }) 20 | 21 | // The protected page the user has visited before redirect to login page 22 | const { redirect: returnToPath } = await getValidatedQuery(event, qSchema.parse) 23 | 24 | try { 25 | const oauthProvider = config.private.oauth![provider]! 26 | 27 | const redirectUrl = resolveURL(config.public.baseUrl, '/api/auth/login', provider, 'callback') 28 | 29 | const authorizationUrl = withQuery( 30 | oauthProvider.authorizeUrl, 31 | { 32 | ...oauthProvider.customParams, 33 | response_type: 'code', 34 | scope: oauthProvider.scopes, 35 | redirect_uri: redirectUrl, 36 | client_id: oauthProvider.clientId, 37 | state: returnToPath, 38 | }, 39 | ) 40 | 41 | await sendRedirect(event, authorizationUrl) 42 | } 43 | catch (error) { 44 | await handleError(error) 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /src/runtime/server/api/login/[provider]/callback.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, getValidatedQuery, sendRedirect, getValidatedRouterParams } from 'h3' 2 | import { z } from 'zod' 3 | import { $fetch } from 'ofetch' 4 | import { resolveURL, withQuery } from 'ufo' 5 | import { getConfig, createRefreshToken, setRefreshTokenCookie, handleError, signRefreshToken, createCustomError, createAccount } from '../../../utils' 6 | 7 | export default defineEventHandler(async (event) => { 8 | const config = getConfig() 9 | 10 | try { 11 | const providers = config.private.oauth ? Object.keys(config.private.oauth) : [] 12 | 13 | const pSchema = z.object({ 14 | provider: z.custom(value => providers.includes(value)), 15 | }) 16 | 17 | const { provider } = await getValidatedRouterParams(event, pSchema.parse) 18 | 19 | const oauthProvider = config.private.oauth![provider]! 20 | 21 | const qSchema = z.object({ 22 | code: z.string().min(1), 23 | state: z.string().startsWith('/').optional(), 24 | }) 25 | 26 | const { state: returnToPath, code } = await getValidatedQuery(event, qSchema.parse) 27 | 28 | const formData = new FormData() 29 | formData.append('grant_type', 'authorization_code') 30 | formData.append('code', code) 31 | formData.append('client_id', oauthProvider.clientId) 32 | formData.append('client_secret', oauthProvider.clientSecret) 33 | formData.append('redirect_uri', resolveURL(config.public.baseUrl, '/api/auth/login', provider, 'callback')) 34 | 35 | const { access_token: accessToken } = await $fetch( 36 | oauthProvider.tokenUrl, 37 | { 38 | method: 'POST', 39 | body: formData, 40 | headers: { 41 | Accept: 'application/json', 42 | }, 43 | }, 44 | ) 45 | 46 | const userInfo = await $fetch(oauthProvider.userUrl, { 47 | headers: { 48 | Authorization: `Bearer ${accessToken}`, 49 | }, 50 | }) 51 | 52 | if (!userInfo.name) { 53 | throw createCustomError(400, 'Oauth name not accessible') 54 | } 55 | 56 | if (!userInfo.email) { 57 | throw createCustomError(400, 'Oauth email not accessible') 58 | } 59 | 60 | const user = await event.context.auth.adapter.user.findByEmail(userInfo.email) 61 | 62 | let userId = user?.id 63 | 64 | if (user) { 65 | if (user.provider !== provider) { 66 | throw createCustomError(403, 'Email already used') 67 | } 68 | 69 | if (user.suspended) { 70 | throw createCustomError(403, 'Account suspended') 71 | } 72 | } 73 | else { 74 | const pictureKey = Object.keys(userInfo).find(el => 75 | [ 76 | 'avatar', 77 | 'avatar_url', 78 | 'picture', 79 | 'picture_url', 80 | 'photo', 81 | 'photo_url', 82 | ].includes(el), 83 | ) 84 | 85 | const picture = pictureKey ? userInfo[pictureKey] : null 86 | 87 | const newUser = await createAccount(event, { 88 | provider, 89 | verified: true, 90 | email: userInfo.email, 91 | name: userInfo.name, 92 | picture, 93 | }) 94 | 95 | userId = newUser.id 96 | } 97 | 98 | const payload = await createRefreshToken(event, userId!) 99 | 100 | const refreshToken = await signRefreshToken(payload) 101 | 102 | setRefreshTokenCookie(event, refreshToken) 103 | 104 | const redirectUrl = withQuery(config.public.redirect.callback!, { redirect: returnToPath }) 105 | 106 | await sendRedirect(event, redirectUrl) 107 | } 108 | catch (error) { 109 | await handleError(error, { event, url: config.public.redirect.callback! }) 110 | } 111 | }) 112 | -------------------------------------------------------------------------------- /src/runtime/server/api/login/index.post.ts: -------------------------------------------------------------------------------- 1 | import { readValidatedBody, defineEventHandler } from 'h3' 2 | import { z } from 'zod' 3 | import { createRefreshToken, setRefreshTokenCookie, createAccessToken, compareSync, handleError, signRefreshToken, createCustomError, checkUser } from '../../utils' 4 | 5 | export default defineEventHandler(async (event) => { 6 | try { 7 | const schema = z.object({ 8 | email: z.string().email().max(40), 9 | password: z.string().min(1), 10 | }) 11 | 12 | const { email, password } = await readValidatedBody(event, schema.parse) 13 | 14 | const user = await event.context.auth.adapter.user.findByEmail(email) 15 | 16 | if (user?.provider !== 'default' || !user.password || !compareSync(password, user.password)) { 17 | throw createCustomError(401, 'Wrong credentials') 18 | } 19 | 20 | checkUser(user) 21 | 22 | if (user.requestedPasswordReset) { 23 | await event.context.auth.adapter.user.update(user.id, { requestedPasswordReset: false }) 24 | } 25 | 26 | const payload = await createRefreshToken(event, user.id) 27 | const refreshToken = await signRefreshToken(payload) 28 | setRefreshTokenCookie(event, refreshToken) 29 | const sessionId = payload.id 30 | 31 | return createAccessToken(event, user, sessionId) 32 | } 33 | catch (error) { 34 | await handleError(error) 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /src/runtime/server/api/logout.post.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3' 2 | import { deleteRefreshTokenCookie, getRefreshTokenFromCookie, verifyRefreshToken, handleError, createUnauthorizedError } from '../utils' 3 | 4 | export default defineEventHandler(async (event) => { 5 | try { 6 | const refreshToken = getRefreshTokenFromCookie(event) 7 | 8 | if (!refreshToken) { 9 | throw createUnauthorizedError() 10 | } 11 | 12 | const payload = await verifyRefreshToken(event, refreshToken) 13 | 14 | await event.context.auth.adapter.session.delete(payload.id, payload.userId) 15 | 16 | deleteRefreshTokenCookie(event) 17 | 18 | return { status: 'ok' } 19 | } 20 | catch (error) { 21 | deleteRefreshTokenCookie(event) 22 | 23 | await handleError(error) 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /src/runtime/server/api/me.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3' 2 | import { handleError, createUnauthorizedError } from '../utils' 3 | 4 | export default defineEventHandler(async (event) => { 5 | try { 6 | const authData = event.context.auth.data 7 | 8 | if (!authData) { 9 | throw createUnauthorizedError() 10 | } 11 | 12 | const user = await event.context.auth.adapter.user.findById(authData.userId) 13 | 14 | if (!user) { 15 | throw createUnauthorizedError() 16 | } 17 | 18 | const { password, ...sanitizedUser } = user 19 | 20 | return sanitizedUser 21 | } 22 | catch (error) { 23 | await handleError(error) 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /src/runtime/server/api/password/change.put.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, readValidatedBody } from 'h3' 2 | import { z } from 'zod' 3 | import { getConfig, hashSync, compareSync, handleError, createUnauthorizedError, createCustomError } from '../../utils' 4 | 5 | export default defineEventHandler(async (event) => { 6 | try { 7 | const config = getConfig() 8 | 9 | const authData = event.context.auth.data 10 | 11 | if (authData?.provider !== 'default') { 12 | throw createUnauthorizedError() 13 | } 14 | 15 | const schema = z.object({ 16 | currentPassword: z.string().min(1), 17 | newPassword: z.string().regex(new RegExp(config.private.registration.passwordValidationRegex!)), 18 | }) 19 | 20 | const { currentPassword, newPassword } = await readValidatedBody(event, schema.parse) 21 | 22 | const user = await event.context.auth.adapter.user.findById(authData.userId) 23 | 24 | if (!user?.password || !compareSync(currentPassword, user.password)) { 25 | throw createCustomError(401, 'Wrong password') 26 | } 27 | 28 | const hashedPassword = hashSync(newPassword, 12) 29 | 30 | await event.context.auth.adapter.user.update(user.id, { 31 | password: hashedPassword, 32 | }) 33 | 34 | await event.context.auth.adapter.session.deleteManyByUserId(authData.userId, authData.sessionId) 35 | 36 | return { status: 'ok' } 37 | } 38 | catch (error) { 39 | await handleError(error) 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /src/runtime/server/api/password/request.post.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, readValidatedBody } from 'h3' 2 | import { resolveURL, withQuery } from 'ufo' 3 | import { z } from 'zod' 4 | import { mustache, getConfig, sendMail, createResetPasswordToken, handleError } from '../../utils' 5 | 6 | export default defineEventHandler(async (event) => { 7 | const config = getConfig() 8 | 9 | try { 10 | const schema = z.object({ 11 | email: z.string().email().max(40), 12 | }) 13 | 14 | const { email } = await readValidatedBody(event, schema.parse) 15 | 16 | const user = await event.context.auth.adapter.user.findByEmail(email) 17 | 18 | if (user && user.provider === 'default') { 19 | const resetPasswordToken = await createResetPasswordToken({ 20 | userId: user.id, 21 | }) 22 | 23 | const redirectUrl = resolveURL(config.public.baseUrl, config.public.redirect.passwordReset!) 24 | const link = withQuery(redirectUrl, { token: resetPasswordToken }) 25 | 26 | await sendMail({ 27 | to: user.email, 28 | subject: 'Password Reset', 29 | html: mustache.render( 30 | config.private.email!.templates!.passwordReset!, 31 | { 32 | ...user, 33 | link, 34 | validityInMinutes: Math.round( 35 | config.private.email!.actionTimeout! / 60, 36 | ), 37 | }, 38 | ), 39 | }) 40 | 41 | await event.context.auth.adapter.user.update(user.id, { requestedPasswordReset: true }) 42 | } 43 | 44 | return { status: 'ok' } 45 | } 46 | catch (error) { 47 | await handleError(error) 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /src/runtime/server/api/password/reset.put.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, readValidatedBody } from 'h3' 2 | import { z } from 'zod' 3 | import { getConfig, verifyResetPasswordToken, hashSync, handleError, createCustomError } from '../../utils' 4 | 5 | export default defineEventHandler(async (event) => { 6 | const config = getConfig() 7 | 8 | try { 9 | const schema = z.object({ 10 | token: z.string().min(1), 11 | password: z.string().regex(new RegExp(config.private.registration.passwordValidationRegex!)), 12 | }) 13 | 14 | const { password, token } = await readValidatedBody(event, schema.parse) 15 | 16 | const payload = await verifyResetPasswordToken(token) 17 | 18 | const user = await event.context.auth.adapter.user.findById(payload.userId) 19 | 20 | if (!user?.requestedPasswordReset) { 21 | throw createCustomError(403, 'Password reset not requested') 22 | } 23 | 24 | const hashedPassword = hashSync(password, 12) 25 | 26 | await event.context.auth.adapter.user.update(payload.userId, { 27 | password: hashedPassword, 28 | }) 29 | 30 | await event.context.auth.adapter.session.deleteManyByUserId(payload.userId) 31 | 32 | await event.context.auth.adapter.user.update(payload.userId, { requestedPasswordReset: false }) 33 | 34 | return { status: 'ok' } 35 | } 36 | catch (error) { 37 | await handleError(error) 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /src/runtime/server/api/register.post.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, readValidatedBody } from 'h3' 2 | import { z } from 'zod' 3 | import { getConfig, hashSync, handleError, createCustomError, createAccount, checkUser } from '../utils' 4 | 5 | export default defineEventHandler(async (event) => { 6 | try { 7 | const config = getConfig() 8 | 9 | const schema = z.object({ 10 | name: z.string().min(1).max(20), 11 | email: z.string().email().max(40), 12 | password: z.string().regex(new RegExp(config.private.registration.passwordValidationRegex!)), 13 | }) 14 | 15 | const { email, password, name } = await readValidatedBody(event, schema.parse) 16 | 17 | const user = await event.context.auth.adapter.user.findByEmail(email) 18 | 19 | if (user) { 20 | checkUser(user) 21 | throw createCustomError(403, 'Email already used') 22 | } 23 | 24 | const hashedPassword = hashSync(password, 12) 25 | 26 | await createAccount(event, { 27 | email, 28 | name, 29 | password: hashedPassword, 30 | }) 31 | 32 | return { status: 'ok' } 33 | } 34 | catch (error) { 35 | await handleError(error) 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /src/runtime/server/api/sessions/[id].delete.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, getValidatedRouterParams } from 'h3' 2 | import { z } from 'zod' 3 | import { handleError, createUnauthorizedError } from '../../utils' 4 | 5 | export default defineEventHandler(async (event) => { 6 | try { 7 | const authData = event.context.auth.data 8 | 9 | if (!authData) { 10 | throw createUnauthorizedError() 11 | } 12 | 13 | const schema = z.object({ 14 | id: z.string().min(1), 15 | }) 16 | 17 | const params = await getValidatedRouterParams(event, schema.parse) 18 | 19 | // @ts-expect-error id can either be string or number 20 | params.id = Number.isNaN(Number(params.id)) ? params.id : Number(params.id) 21 | 22 | const session = await event.context.auth.adapter.session.findById(params.id, authData.userId) 23 | 24 | if (session?.userId !== authData.userId) { 25 | throw createUnauthorizedError() 26 | } 27 | 28 | await event.context.auth.adapter.session.delete(params.id, authData.userId) 29 | 30 | return { status: 'ok' } 31 | } 32 | catch (error) { 33 | await handleError(error) 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /src/runtime/server/api/sessions/index.delete.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3' 2 | import { handleError, createUnauthorizedError } from '../../utils' 3 | 4 | export default defineEventHandler(async (event) => { 5 | try { 6 | const authData = event.context.auth.data 7 | 8 | if (!authData) { 9 | throw createUnauthorizedError() 10 | } 11 | 12 | await event.context.auth.adapter.session.deleteManyByUserId(authData.userId, authData.sessionId) 13 | 14 | return { status: 'ok' } 15 | } 16 | catch (error) { 17 | await handleError(error) 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /src/runtime/server/api/sessions/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3' 2 | import { handleError, createUnauthorizedError, getConfig } from '../../utils' 3 | import type { Session } from '#auth_adapter' 4 | 5 | export default defineEventHandler(async (event) => { 6 | try { 7 | const authData = event.context.auth.data 8 | 9 | if (!authData) { 10 | throw createUnauthorizedError() 11 | } 12 | 13 | const config = getConfig() 14 | const sessions = await event.context.auth.adapter.session.findManyByUserId(authData.userId) 15 | 16 | const expired: Session[] = [] 17 | const active: Session[] = [] 18 | 19 | sessions.forEach((session) => { 20 | const isExpired = new Date().getTime() - new Date(session.updatedAt).getTime() > config.private.refreshToken.maxAge! * 1000 21 | if (isExpired) 22 | expired.push(session) 23 | else 24 | active.push(session) 25 | }) 26 | 27 | await Promise.all(expired.map(session => event.context.auth.adapter.session.delete(session.id, authData.userId))) 28 | 29 | return { 30 | active, 31 | current: active.find(session => session.id === authData.sessionId), 32 | } 33 | } 34 | catch (error) { 35 | await handleError(error) 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /src/runtime/server/api/sessions/refresh.post.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3' 2 | import { createAccessToken, getRefreshTokenFromCookie, setRefreshTokenCookie, updateRefreshToken, verifyRefreshToken, deleteRefreshTokenCookie, handleError, createUnauthorizedError, checkUser } from '../../utils' 3 | 4 | export default defineEventHandler(async (event) => { 5 | try { 6 | const refreshToken = getRefreshTokenFromCookie(event) 7 | 8 | if (!refreshToken) { 9 | throw createUnauthorizedError() 10 | } 11 | 12 | const payload = await verifyRefreshToken(event, refreshToken) 13 | 14 | const user = await event.context.auth.adapter.user.findById(payload.userId) 15 | 16 | if (!user) { 17 | throw createUnauthorizedError() 18 | } 19 | 20 | checkUser(user) 21 | 22 | const newRefreshToken = await updateRefreshToken(event, payload.id, user.id) 23 | 24 | setRefreshTokenCookie(event, newRefreshToken) 25 | 26 | const sessionId = payload.id 27 | 28 | return createAccessToken(event, user, sessionId) 29 | } 30 | catch (error) { 31 | deleteRefreshTokenCookie(event) 32 | 33 | await handleError(error) 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /src/runtime/server/utils/adapter/prisma.ts: -------------------------------------------------------------------------------- 1 | import type { PrismaClient } from '@prisma/client' 2 | import { defineAdapter } from './utils' 3 | 4 | export const usePrismaAdapter = defineAdapter((prisma) => { 5 | if (!prisma) { 6 | throw new Error('[nuxt-auth] Prisma client not defined') 7 | } 8 | 9 | return { 10 | name: 'prisma', 11 | source: prisma, 12 | 13 | user: { 14 | async findById(id) { 15 | return prisma.user.findUnique({ 16 | where: { 17 | id, 18 | }, 19 | }) 20 | }, 21 | 22 | async findByEmail(email) { 23 | return prisma.user.findUnique({ 24 | where: { email }, 25 | }) 26 | }, 27 | 28 | async create(data) { 29 | return prisma.user.create({ 30 | data, 31 | }) 32 | }, 33 | 34 | async update(id, data) { 35 | await prisma.user.update({ 36 | where: { 37 | id, 38 | }, 39 | data, 40 | select: { 41 | id: true, 42 | }, 43 | }) 44 | }, 45 | }, 46 | 47 | session: { 48 | async findById(id, userId) { 49 | return prisma.session.findUnique({ 50 | where: { 51 | id, 52 | userId, 53 | }, 54 | }) 55 | }, 56 | 57 | async findManyByUserId(userId) { 58 | return prisma.session.findMany({ 59 | where: { 60 | userId, 61 | }, 62 | }) 63 | }, 64 | 65 | async create(data) { 66 | return prisma.session.create({ 67 | data, 68 | select: { 69 | id: true, 70 | }, 71 | }) 72 | }, 73 | 74 | async update(id, data) { 75 | await prisma.session.update({ 76 | where: { 77 | id, 78 | }, 79 | data: { 80 | uid: data.uid, 81 | }, 82 | select: { 83 | id: true, 84 | }, 85 | }) 86 | }, 87 | 88 | async delete(id, userId) { 89 | await prisma.session.delete({ 90 | where: { 91 | id, 92 | userId, 93 | }, 94 | select: { 95 | id: true, 96 | }, 97 | }) 98 | }, 99 | 100 | async deleteManyByUserId(userId, excludeId) { 101 | await prisma.session.deleteMany({ 102 | where: { 103 | userId, 104 | id: { 105 | not: excludeId, 106 | }, 107 | }, 108 | }) 109 | }, 110 | }, 111 | } 112 | }) 113 | -------------------------------------------------------------------------------- /src/runtime/server/utils/adapter/unstorage.ts: -------------------------------------------------------------------------------- 1 | import type { Storage } from 'unstorage' 2 | import { randomUUID } from 'uncrypto' 3 | import { defineAdapter } from './utils' 4 | import type { User, Session } from '#auth_adapter' 5 | 6 | export const useUnstorageAdapter = defineAdapter((storage) => { 7 | if (!storage) { 8 | throw new Error('[nuxt-auth] Unstorage client not defined') 9 | } 10 | 11 | return { 12 | name: 'unstorage', 13 | source: storage, 14 | 15 | user: { 16 | async findById(id) { 17 | return storage.getItem(`users:id:${id}:data`) 18 | }, 19 | 20 | async findByEmail(email) { 21 | const id = await storage.getItem(`users:email:${email}`) 22 | return id ? this.findById(id) : null 23 | }, 24 | 25 | async create(data) { 26 | const user: User = { 27 | ...data, 28 | id: randomUUID(), 29 | createdAt: new Date(), 30 | updatedAt: new Date(), 31 | } 32 | await storage.setItem(`users:id:${user.id}:data`, user) 33 | await storage.setItem(`users:email:${data.email}`, user.id) 34 | return user 35 | }, 36 | 37 | async update(id, data) { 38 | const user = await this.findById(id) 39 | await storage.setItem(`users:id:${id}:data`, { 40 | ...user, 41 | ...data, 42 | updatedAt: new Date(), 43 | }) 44 | }, 45 | }, 46 | 47 | session: { 48 | async findById(id, userId) { 49 | return storage.getItem(`users:id:${userId}:sessions:${id}`) 50 | }, 51 | 52 | async findManyByUserId(userId) { 53 | const sessions = await storage.getKeys(`users:id:${userId}:sessions`).then((keys) => { 54 | return storage.getItems(keys) 55 | }) 56 | return sessions.map(session => session.value) 57 | }, 58 | 59 | async create(data) { 60 | const id = randomUUID() 61 | await storage.setItem(`users:id:${data.userId}:sessions:${id}`, 62 | { 63 | ...data, 64 | id, 65 | createdAt: new Date(), 66 | updatedAt: new Date(), 67 | }) 68 | return { id } 69 | }, 70 | 71 | async update(id, data) { 72 | const session = await this.findById(id, data.userId) 73 | await storage.setItem(`users:id:${data.userId}:sessions:${id}`, { 74 | ...session, 75 | ...data, 76 | updatedAt: new Date(), 77 | }) 78 | }, 79 | 80 | async delete(id, userId) { 81 | await storage.removeItem(`users:id:${userId}:sessions:${id}`) 82 | }, 83 | 84 | async deleteManyByUserId(userId, excludeId) { 85 | const sessions = await this.findManyByUserId(userId) 86 | const sessionsFiltered = sessions.filter(session => session.id !== excludeId) 87 | await Promise.all(sessionsFiltered.map((session) => { 88 | return storage.removeItem(`users:id:${userId}:sessions:${session.id}`) 89 | })) 90 | }, 91 | }, 92 | } 93 | }) 94 | -------------------------------------------------------------------------------- /src/runtime/server/utils/adapter/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Adapter } from '#auth_adapter' 2 | 3 | export type AdapterFactory = (source: Source) => Adapter 4 | 5 | export function defineAdapter(factory: AdapterFactory): AdapterFactory { 6 | return factory 7 | } 8 | -------------------------------------------------------------------------------- /src/runtime/server/utils/avatar.ts: -------------------------------------------------------------------------------- 1 | import { withQuery } from 'ufo' 2 | 3 | export function generateAvatar(name: string) { 4 | // https://tailwindcss.com/docs/customizing-colors#default-color-palette 5 | // Variant 700 6 | const colors = [ 7 | 'b91c1c' /* red */, 8 | 'c2410c' /* orange */, 9 | 'b45309' /* amber */, 10 | 'a16207' /* yellow */, 11 | '4d7c0f' /* lime */, 12 | '15803d' /* green */, 13 | '0f766e' /* teal */, 14 | '0e7490' /* cyan */, 15 | '1d4ed8' /* blue */, 16 | '4338ca' /* indigo */, 17 | '6d28d9' /* violet */, 18 | 'be123c', /* rose */ 19 | ] 20 | 21 | const randomIndex = Math.floor(Math.random() * (colors.length - 1)) 22 | 23 | const url = withQuery('/api/auth/avatar', { 24 | name, 25 | color: 'f5f5f5', 26 | background: colors[randomIndex], 27 | }) 28 | 29 | return url 30 | } 31 | -------------------------------------------------------------------------------- /src/runtime/server/utils/bcrypt.ts: -------------------------------------------------------------------------------- 1 | import { hashSync, compareSync } from 'bcrypt-edge' 2 | 3 | export { hashSync, compareSync } 4 | -------------------------------------------------------------------------------- /src/runtime/server/utils/config.ts: -------------------------------------------------------------------------------- 1 | import type { PublicConfig, PrivateConfig } from '../../types/config' 2 | import { useRuntimeConfig } from '#imports' 3 | 4 | export function getConfig() { 5 | const privateConfig = useRuntimeConfig().auth as PrivateConfig & { backendEnabled: true } 6 | const publicConfig = useRuntimeConfig().public.auth as PublicConfig 7 | 8 | return { private: privateConfig, public: publicConfig } 9 | } 10 | -------------------------------------------------------------------------------- /src/runtime/server/utils/context.ts: -------------------------------------------------------------------------------- 1 | import type { H3Event } from 'h3' 2 | import { getAccessTokenFromHeader, verifyAccessToken } from './token/accessToken' 3 | import type { Adapter } from '#auth_adapter' 4 | 5 | export async function setEventContext(event: H3Event, adapter: Adapter) { 6 | if (!adapter) { 7 | throw new Error('[nuxt-auth] Adapter not defined') 8 | } 9 | 10 | event.context.auth = { adapter } 11 | 12 | const accessToken = getAccessTokenFromHeader(event) 13 | 14 | if (accessToken) { 15 | await verifyAccessToken(event, accessToken) 16 | .then((p) => { event.context.auth.data = p }) 17 | .catch(() => { }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/runtime/server/utils/error.ts: -------------------------------------------------------------------------------- 1 | import { createError, H3Error, sendRedirect } from 'h3' 2 | import { withQuery } from 'ufo' 3 | import type { H3Event } from 'h3' 4 | import type { NitroApp } from 'nitropack/types' 5 | import { useNitroApp } from 'nitropack/runtime' 6 | import type { KnownErrors } from '../../types/common' 7 | import { getConfig } from './config' 8 | import type { User } from '#auth_adapter' 9 | 10 | export function checkUser(data: Pick) { 11 | const config = getConfig() 12 | 13 | if (!data.verified && config.private.registration.requireEmailVerification) { 14 | throw createCustomError(403, 'Account not verified') 15 | } 16 | 17 | if (data.suspended) { 18 | throw createCustomError(403, 'Account suspended') 19 | } 20 | } 21 | 22 | export function createCustomError(statusCode: number, message: KnownErrors) { 23 | return createError({ message, statusCode }) 24 | } 25 | 26 | export function createUnauthorizedError() { 27 | return createCustomError(401, 'Unauthorized') 28 | } 29 | 30 | export async function handleError(error: unknown, redirect?: { event: H3Event, url: string }) { 31 | if (error instanceof H3Error) { 32 | if (redirect) { 33 | await sendRedirect(redirect.event, withQuery(redirect.url, { error: error.message })) 34 | return 35 | } 36 | else { 37 | throw error 38 | } 39 | } 40 | 41 | const nitroApp = useNitroApp() as NitroApp 42 | await nitroApp.hooks.callHook('auth:error', error) 43 | 44 | throw createCustomError(500, 'Something went wrong') 45 | } 46 | -------------------------------------------------------------------------------- /src/runtime/server/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config' 2 | export * from './mail' 3 | export * from './token/accessToken' 4 | export * from './token/refreshToken' 5 | export * from './token/emailVerify' 6 | export * from './token/passwordReset' 7 | export * from './error' 8 | export * from './bcrypt' 9 | export * from './token/jwt' 10 | export * from './mustache' 11 | export * from './avatar' 12 | export * from './user' 13 | export * from './adapter/prisma' 14 | export * from './adapter/utils' 15 | export * from './adapter/unstorage' 16 | export * from './context' 17 | -------------------------------------------------------------------------------- /src/runtime/server/utils/mail.ts: -------------------------------------------------------------------------------- 1 | import { $fetch } from 'ofetch' 2 | import type { NitroApp } from 'nitropack/types' 3 | import { useNitroApp } from 'nitropack/runtime' 4 | import type { MailMessage } from '../../types/common' 5 | import { getConfig } from './config' 6 | 7 | export async function sendMail(msg: MailMessage) { 8 | const config = getConfig() 9 | 10 | if (!config.private.email?.from) { 11 | throw new Error('[nuxt-auth] Email `from` address is not set') 12 | } 13 | 14 | const settings = config.private.email 15 | 16 | switch (settings.provider?.name) { 17 | case 'hook': 18 | return await withHook() 19 | case 'sendgrid': 20 | return await withSendgrid(settings.provider.apiKey) 21 | case 'resend': 22 | return await withResend(settings.provider.apiKey) 23 | default: 24 | throw new Error('[nuxt-auth] invalid email `provider`') 25 | } 26 | 27 | function withSendgrid(apiKey: string) { 28 | return $fetch('https://api.sendgrid.com/v3/mail/send', { 29 | method: 'POST', 30 | headers: { 31 | authorization: `Bearer ${apiKey}`, 32 | }, 33 | body: { 34 | personalizations: [ 35 | { 36 | to: [{ email: msg.to }], 37 | subject: msg.subject, 38 | }, 39 | ], 40 | content: [{ type: 'text/html', value: msg.html }], 41 | from: { email: settings.from }, 42 | reply_to: { email: settings.from }, 43 | }, 44 | }) 45 | } 46 | 47 | // https://resend.com/docs/api-reference/emails/send-email 48 | function withResend(apiKey: string) { 49 | return $fetch('https://api.resend.com/emails', { 50 | method: 'POST', 51 | headers: { 52 | Authorization: `Bearer ${apiKey}`, 53 | }, 54 | body: { 55 | from: settings.from, 56 | to: msg.to, 57 | subject: msg.subject, 58 | html: msg.html, 59 | }, 60 | }) 61 | } 62 | 63 | function withHook() { 64 | const nitroApp = useNitroApp() as NitroApp 65 | return nitroApp.hooks.callHook('auth:email', settings.from, msg) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/runtime/server/utils/mustache.ts: -------------------------------------------------------------------------------- 1 | export const mustache = { 2 | render: (template: string, view: object) => { 3 | const keys = Object.keys(view) 4 | 5 | keys.forEach((key) => { 6 | template = template.replaceAll(`{{${key}}}`, view[key as keyof typeof view]) 7 | }) 8 | 9 | return template 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /src/runtime/server/utils/token/accessToken.ts: -------------------------------------------------------------------------------- 1 | import { getRequestHeader } from 'h3' 2 | import type { H3Event } from 'h3' 3 | import { getConfig } from '../config' 4 | import { mustache } from '../mustache' 5 | import type { AccessTokenPayload } from '../../../types/common' 6 | import { encode, decode } from './jwt' 7 | import { getFingerprintHash, verifyFingerprint } from './fingerprint' 8 | import type { User, Session } from '#auth_adapter' 9 | 10 | export async function createAccessToken(event: H3Event, user: User, sessionId: Session['id']) { 11 | const config = getConfig() 12 | 13 | let customClaims = {} 14 | 15 | if (typeof config.private.accessToken.customClaims === 'object') { 16 | const template = JSON.stringify(config.private.accessToken.customClaims) 17 | const output = mustache.render(template, user) 18 | customClaims = JSON.parse(output) 19 | } 20 | 21 | const fingerprint = await getFingerprintHash(event) 22 | 23 | const payload: AccessTokenPayload = { 24 | ...customClaims, 25 | sessionId, 26 | fingerprint, 27 | userId: user.id, 28 | userRole: user.role, 29 | provider: user.provider, 30 | verified: user.verified, 31 | suspended: user.suspended, 32 | } 33 | 34 | const accessToken = await encode( 35 | payload, 36 | config.private.accessToken.jwtSecret, 37 | config.private.accessToken.maxAge!, 38 | ) 39 | 40 | return { 41 | access_token: accessToken, 42 | expires_in: config.private.accessToken.maxAge, 43 | } 44 | } 45 | 46 | /** 47 | * Get the access token from Authorization header 48 | * @param event 49 | * @returns accessToken 50 | */ 51 | export function getAccessTokenFromHeader(event: H3Event) { 52 | const authorization = getRequestHeader(event, 'Authorization') 53 | if (authorization) { 54 | const accessToken = authorization.split('Bearer ')[1] 55 | return accessToken 56 | } 57 | } 58 | 59 | /** 60 | * Check if the access token is issued by the server and not expired 61 | * @param event H3Event 62 | * @param accessToken 63 | * @returns accessTokenPayload 64 | */ 65 | export async function verifyAccessToken(event: H3Event, accessToken: string) { 66 | const config = getConfig() 67 | 68 | const payload = await decode( 69 | accessToken, 70 | config.private.accessToken.jwtSecret, 71 | ) 72 | 73 | await verifyFingerprint(event, payload.fingerprint) 74 | 75 | return payload 76 | } 77 | -------------------------------------------------------------------------------- /src/runtime/server/utils/token/emailVerify.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from '../config' 2 | import { decode, encode } from './jwt' 3 | import type { User } from '#auth_adapter' 4 | 5 | type EmailVerifyPayload = { 6 | userId: User['id'] 7 | } 8 | 9 | export async function createEmailVerifyToken(payload: EmailVerifyPayload) { 10 | const config = getConfig() 11 | 12 | const emailVerifyToken = await encode( 13 | payload, 14 | config.private.accessToken.jwtSecret + 'e', 15 | config.private.email!.actionTimeout!, 16 | ) 17 | 18 | return emailVerifyToken 19 | } 20 | 21 | export async function verifyEmailVerifyToken(emailVerifyToken: string) { 22 | const config = getConfig() 23 | 24 | const payload = await decode( 25 | emailVerifyToken, 26 | config.private.accessToken.jwtSecret + 'e', 27 | ) 28 | 29 | return payload 30 | } 31 | -------------------------------------------------------------------------------- /src/runtime/server/utils/token/fingerprint.ts: -------------------------------------------------------------------------------- 1 | import { getRequestFingerprint } from 'h3' 2 | import type { H3Event } from 'h3' 3 | import { createUnauthorizedError } from '../error' 4 | 5 | export async function getFingerprintHash(event: H3Event) { 6 | const fingerprint = await getRequestFingerprint(event, { 7 | ip: false, 8 | userAgent: true, 9 | hash: 'SHA-1', 10 | }) 11 | 12 | return fingerprint 13 | } 14 | 15 | export async function verifyFingerprint(event: H3Event, hash: string | null) { 16 | const fingerprintHash = await getFingerprintHash(event) 17 | 18 | if (fingerprintHash !== hash) { 19 | throw createUnauthorizedError() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/runtime/server/utils/token/jwt.ts: -------------------------------------------------------------------------------- 1 | import { jwtVerify, SignJWT } from 'jose' 2 | import type { JWTPayload } from 'jose' 3 | import { createUnauthorizedError } from '../error' 4 | 5 | async function encode(payload: JWTPayload, key: string, maxAge: number) { 6 | const secret = new TextEncoder().encode(key) 7 | const exp = `${maxAge}s` 8 | 9 | return await new SignJWT(payload) 10 | .setExpirationTime(exp) 11 | .setIssuer('nuxt-auth') 12 | .setProtectedHeader({ alg: 'HS256' }) 13 | .sign(secret) 14 | } 15 | 16 | async function decode(token: string, key: string) { 17 | const secret = new TextEncoder().encode(key) 18 | 19 | const { payload } = await jwtVerify(token, secret).catch(() => { 20 | throw createUnauthorizedError() 21 | }) 22 | 23 | return payload 24 | } 25 | 26 | export { encode, decode } 27 | -------------------------------------------------------------------------------- /src/runtime/server/utils/token/passwordReset.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from '../config' 2 | import { encode, decode } from './jwt' 3 | import type { User } from '#auth_adapter' 4 | 5 | type ResetPasswordPayload = { 6 | userId: User['id'] 7 | } 8 | 9 | export async function createResetPasswordToken(payload: ResetPasswordPayload) { 10 | const config = getConfig() 11 | const resetPasswordToken = await encode( 12 | payload, 13 | config.private.accessToken.jwtSecret + 'p', 14 | config.private.email!.actionTimeout!, 15 | ) 16 | 17 | return resetPasswordToken 18 | } 19 | 20 | export async function verifyResetPasswordToken(resetPasswordToken: string) { 21 | const config = getConfig() 22 | 23 | const payload = await decode( 24 | resetPasswordToken, 25 | config.private.accessToken.jwtSecret + 'p', 26 | ) 27 | 28 | return payload 29 | } 30 | -------------------------------------------------------------------------------- /src/runtime/server/utils/token/refreshToken.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'uncrypto' 2 | import { setCookie, getCookie, deleteCookie, getHeader } from 'h3' 3 | import type { H3Event } from 'h3' 4 | import { getConfig } from '../config' 5 | import { createUnauthorizedError } from '../error' 6 | import { encode, decode } from './jwt' 7 | import type { User, Session } from '#auth_adapter' 8 | 9 | type RefreshTokenPayload = Pick 10 | 11 | export async function createRefreshToken(event: H3Event, userId: User['id']) { 12 | const userAgent = getHeader(event, 'user-agent') 13 | 14 | const uid = randomUUID() 15 | 16 | const refreshTokenEntity = await event.context.auth.adapter.session.create({ 17 | userId, 18 | userAgent: userAgent ?? null, 19 | uid, 20 | }) 21 | 22 | const payload: RefreshTokenPayload = { 23 | id: refreshTokenEntity.id, 24 | uid, 25 | userId, 26 | } 27 | 28 | return payload 29 | } 30 | 31 | export async function signRefreshToken(payload: RefreshTokenPayload) { 32 | const config = getConfig() 33 | const refreshToken = await encode( 34 | payload, 35 | config.private.refreshToken.jwtSecret, 36 | config.private.refreshToken.maxAge!, 37 | ) 38 | 39 | return refreshToken 40 | } 41 | 42 | export async function decodeRefreshToken(refreshToken: string) { 43 | const config = getConfig() 44 | const payload = await decode( 45 | refreshToken, 46 | config.private.refreshToken.jwtSecret, 47 | ) 48 | 49 | return payload 50 | } 51 | 52 | export async function updateRefreshToken(event: H3Event, sessionId: Session['id'], userId: User['id']) { 53 | const uid = randomUUID() 54 | 55 | await event.context.auth.adapter.session.update(sessionId, { 56 | uid, 57 | userId, 58 | }) 59 | 60 | const refreshToken = await signRefreshToken({ 61 | id: sessionId, 62 | uid, 63 | userId, 64 | }) 65 | 66 | return refreshToken 67 | } 68 | 69 | export function setRefreshTokenCookie(event: H3Event, refreshToken: string) { 70 | const config = getConfig() 71 | setCookie(event, config.private.refreshToken.cookieName!, refreshToken, { 72 | httpOnly: true, 73 | secure: true, 74 | maxAge: config.private.refreshToken.maxAge, 75 | sameSite: 'lax', 76 | }) 77 | } 78 | 79 | export function getRefreshTokenFromCookie(event: H3Event) { 80 | const config = getConfig() 81 | const refreshToken = getCookie(event, config.private.refreshToken.cookieName!) 82 | return refreshToken 83 | } 84 | 85 | export async function verifyRefreshToken(event: H3Event, refreshToken: string) { 86 | // check if the refreshToken is issued by the auth server && if it's not expired 87 | const payload = await decodeRefreshToken(refreshToken) 88 | 89 | const session = await event.context.auth.adapter.session.findById(payload.id, payload.userId) 90 | 91 | if (!session) { 92 | throw createUnauthorizedError() 93 | } 94 | 95 | const userAgent = getHeader(event, 'user-agent') ?? null 96 | 97 | if (session.uid !== payload.uid || session.userAgent !== userAgent) { 98 | await event.context.auth.adapter.session.delete(payload.id, payload.userId) 99 | throw createUnauthorizedError() 100 | } 101 | 102 | return payload 103 | } 104 | 105 | export function deleteRefreshTokenCookie(event: H3Event) { 106 | deleteCookie(event, getConfig().private.refreshToken.cookieName!) 107 | } 108 | -------------------------------------------------------------------------------- /src/runtime/server/utils/user.ts: -------------------------------------------------------------------------------- 1 | import type { H3Event } from 'h3' 2 | import type { NitroApp } from 'nitropack/types' 3 | import { useNitroApp } from 'nitropack/runtime' 4 | import { getConfig } from './config' 5 | import { generateAvatar } from './avatar' 6 | import { createCustomError } from './error' 7 | import type { User } from '#auth_adapter' 8 | 9 | interface CreateAccountInput { 10 | name: User['name'] 11 | email: User['email'] 12 | password?: User['password'] 13 | verified?: User['verified'] 14 | provider?: User['provider'] 15 | picture?: User['picture'] 16 | } 17 | 18 | export async function createAccount(event: H3Event, data: CreateAccountInput) { 19 | const config = getConfig() 20 | 21 | if (config.private.registration.enabled === false) { 22 | throw createCustomError(500, 'Registration disabled') 23 | } 24 | 25 | const regex = new RegExp(config.private.registration.emailValidationRegex!) 26 | 27 | if (!regex.test(data.email)) { 28 | throw createCustomError(403, 'Email not accepted') 29 | } 30 | 31 | const user = await event.context.auth.adapter.user.create({ 32 | name: data.name, 33 | email: data.email, 34 | password: data.password, 35 | verified: data.verified ?? false, 36 | provider: data.provider ?? 'default', 37 | picture: data.picture ?? generateAvatar(data.name), 38 | role: config.private.registration.defaultRole!, 39 | }) 40 | 41 | const nitroApp = useNitroApp() as NitroApp 42 | await nitroApp.hooks.callHook('auth:registration', user) 43 | 44 | return user 45 | } 46 | -------------------------------------------------------------------------------- /src/runtime/templates/email_verification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 61 | 62 | 63 |
38 |

Hello {{name}}

39 |

40 | We have received a request to verify your email address. If you haven't made this request please 41 | ignore the following email. Otherwise, to complete the process, click the following link.

42 | 44 | 45 | 46 | 54 | 55 | 56 |
48 | 49 | 51 | Verify email 52 | 53 |
57 |
59 |

This link will expire in {{validityInMinutes}} minutes.

60 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /src/runtime/templates/password_reset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 61 | 62 | 63 |
38 |

Hello {{name}}

39 |

40 | We have received a request to reset your password. If you haven't made this request please 41 | ignore the following email. Otherwise, to complete the process, click the following link.

42 | 44 | 45 | 46 | 54 | 55 | 56 |
48 | 49 | 51 | Reset password 52 | 53 |
57 |
59 |

This link will expire in {{validityInMinutes}} minutes.

60 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /src/runtime/types/adapter.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Resolves to 'string' if T is 'any' (unresolved) and 'number' if T is a number 3 | */ 4 | type NumberOrString = (T extends string ? string : number) extends number ? number : string 5 | 6 | export interface User { 7 | id: NumberOrString 8 | name: string 9 | email: string 10 | picture: string 11 | role: string 12 | provider: string 13 | verified: boolean 14 | createdAt: Date 15 | updatedAt: Date 16 | suspended?: boolean | null 17 | password?: string | null 18 | requestedPasswordReset?: boolean | null 19 | } 20 | 21 | export interface Session { 22 | id: NumberOrString 23 | uid: string 24 | userAgent: string | null 25 | createdAt: Date 26 | updatedAt: Date 27 | userId: User['id'] 28 | } 29 | 30 | type UserCreateInput = Pick 31 | type UserCreateOutput = User 32 | type UserUpdateInput = Omit, 'id'> 33 | type SessionCreateInput = Pick 34 | type SessionCreateOutput = Pick 35 | type SessionUpdateInput = Pick 36 | 37 | export interface Adapter { 38 | name: string 39 | source: SourceT 40 | user: { 41 | findById: (id: User['id']) => Promise 42 | findByEmail: (email: User['email']) => Promise 43 | create: (input: UserCreateInput) => Promise 44 | update: (id: User['id'], input: UserUpdateInput) => Promise 45 | } 46 | session: { 47 | findById: (id: Session['id'], userId: User['id']) => Promise 48 | findManyByUserId: (id: User['id']) => Promise 49 | create: (input: SessionCreateInput) => Promise 50 | update: (id: Session['id'], input: SessionUpdateInput) => Promise 51 | delete: (id: Session['id'], userId: User['id']) => Promise 52 | deleteManyByUserId: (userId: User['id'], excludeId?: Session['id']) => Promise 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/runtime/types/common.d.ts: -------------------------------------------------------------------------------- 1 | import type { FetchError } from 'ofetch' 2 | import type { H3Error } from 'h3' 3 | import type { Adapter, User, Session } from '#auth_adapter' 4 | 5 | export type AccessTokenPayload = { 6 | fingerprint: string | null 7 | userId: User['id'] 8 | sessionId: Session['id'] 9 | userRole: User['role'] 10 | provider: User['provider'] 11 | verified: User['verified'] 12 | suspended: User['suspended'] 13 | } 14 | 15 | declare module 'h3' { 16 | interface H3EventContext { 17 | auth: { 18 | data?: AccessTokenPayload 19 | adapter: Adapter 20 | } 21 | } 22 | } 23 | 24 | declare module 'nitropack' { 25 | interface NitroRuntimeHooks { 26 | 'auth:email': (from: string, msg: MailMessage) => Promise | void 27 | 'auth:registration': (user: User) => Promise | void 28 | 'auth:error': (error: unknown) => Promise | void 29 | } 30 | } 31 | 32 | declare module '#app' { 33 | interface NuxtApp { 34 | $auth: { 35 | fetch: typeof $fetch 36 | _refreshPromise: Promise | null 37 | } 38 | } 39 | interface RuntimeNuxtHooks { 40 | 'auth:loggedIn': (state: boolean) => Promise | void 41 | 'auth:fetchError': (response: FetchError['response']) => Promise | void 42 | } 43 | } 44 | 45 | declare module 'vue-router' { 46 | interface RouteMeta { 47 | auth?: boolean 48 | } 49 | } 50 | 51 | declare module 'vue' { 52 | interface ComponentCustomProperties { 53 | $auth: { 54 | fetch: typeof $fetch 55 | _refreshPromise: Promise | null 56 | } 57 | } 58 | } 59 | 60 | export type KnownErrors = 61 | 'Unauthorized' 62 | | 'Account suspended' 63 | | 'Account not verified' 64 | | 'Wrong credentials' 65 | | 'Email already used' 66 | | 'Email not accepted' 67 | | 'Wrong password' 68 | | 'Password reset not requested' 69 | | 'Oauth name not accessible' 70 | | 'Oauth email not accessible' 71 | | 'Registration disabled' 72 | | 'Something went wrong' 73 | 74 | export type MailMessage = { 75 | to: string 76 | subject: string 77 | html: string 78 | } 79 | 80 | export interface AuthenticationData { 81 | access_token: string 82 | expires_in: number 83 | } 84 | 85 | export interface ResponseOK { 86 | status: 'ok' 87 | } 88 | -------------------------------------------------------------------------------- /src/runtime/types/config.d.ts: -------------------------------------------------------------------------------- 1 | interface MailSendgridProvider { 2 | name: 'sendgrid' 3 | apiKey: string 4 | } 5 | 6 | interface MailResendProvider { 7 | name: 'resend' 8 | apiKey: string 9 | } 10 | 11 | interface MailHookProvider { 12 | name: 'hook' 13 | } 14 | 15 | type OauthBase = Record 23 | }> 24 | 25 | type OauthGoogle = { 26 | google?: { 27 | clientId: string 28 | clientSecret: string 29 | scopes: 'email profile' 30 | authorizeUrl: 'https://accounts.google.com/o/oauth2/auth' 31 | tokenUrl: 'https://accounts.google.com/o/oauth2/token' 32 | userUrl: 'https://www.googleapis.com/oauth2/v3/userinfo' 33 | customParams?: Record 34 | } 35 | } 36 | 37 | type OauthGitHub = { 38 | github?: { 39 | clientId: string 40 | clientSecret: string 41 | scopes: 'user:email' 42 | authorizeUrl: 'https://github.com/login/oauth/authorize' 43 | tokenUrl: 'https://github.com/login/oauth/access_token' 44 | userUrl: 'https://api.github.com/user' 45 | customParams?: Record 46 | } 47 | } 48 | 49 | export type PrivateConfigWithoutBackend = { 50 | backendEnabled: false 51 | } 52 | 53 | export type PrivateConfigWithBackend = { 54 | backendEnabled: true 55 | 56 | accessToken: { 57 | jwtSecret: string 58 | maxAge?: number 59 | customClaims?: Record 60 | } 61 | 62 | refreshToken: { 63 | cookieName?: string 64 | jwtSecret: string 65 | maxAge?: number 66 | } 67 | 68 | oauth?: OauthBase & OauthGoogle & OauthGitHub 69 | 70 | email?: { 71 | from: string 72 | actionTimeout?: number 73 | provider?: MailSendgridProvider | MailResendProvider | MailHookProvider 74 | templates?: { 75 | passwordReset?: string 76 | emailVerify?: string 77 | } 78 | } 79 | 80 | registration: { 81 | enabled?: boolean 82 | requireEmailVerification?: boolean 83 | passwordValidationRegex?: string 84 | emailValidationRegex?: string 85 | defaultRole?: string 86 | } 87 | } 88 | 89 | export type PublicConfig = { 90 | backendEnabled?: boolean 91 | backendBaseUrl?: string 92 | baseUrl: string 93 | enableGlobalAuthMiddleware?: boolean 94 | loggedInFlagName?: string 95 | redirect: { 96 | login: string 97 | logout: string 98 | home: string 99 | callback?: string 100 | passwordReset?: string 101 | emailVerify?: string 102 | } 103 | refreshToken: { 104 | cookieName?: string 105 | } 106 | } 107 | 108 | export type PrivateConfig = PrivateConfigWithBackend | PrivateConfigWithoutBackend 109 | 110 | export type ModuleOptions = PrivateConfig & PublicConfig 111 | -------------------------------------------------------------------------------- /src/setup_backend.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | import { resolve as resolveAbsolute } from 'node:path' 3 | import { createResolver, addServerHandler, addTemplate } from '@nuxt/kit' 4 | import { defu } from 'defu' 5 | import type { Nuxt } from '@nuxt/schema' 6 | import { warnRequiredOption, info } from './utils' 7 | import type { ModuleOptions } from './runtime/types/config' 8 | 9 | export function setupBackend(options: ModuleOptions, nuxt: Nuxt) { 10 | if (!options.backendEnabled) { 11 | throw new Error('Backend should be enabled') 12 | } 13 | 14 | if (!options.refreshToken.jwtSecret) { 15 | warnRequiredOption('refreshToken.jwtSecret') 16 | } 17 | 18 | if (!options.accessToken.jwtSecret) { 19 | warnRequiredOption('accessToken.jwtSecret') 20 | } 21 | 22 | nuxt.options.runtimeConfig = defu(nuxt.options.runtimeConfig, { 23 | app: {}, 24 | public: {}, 25 | auth: { 26 | backendEnabled: options.backendEnabled, 27 | accessToken: options.accessToken, 28 | refreshToken: options.refreshToken, 29 | email: options.email, 30 | oauth: options.oauth, 31 | registration: options.registration, 32 | }, 33 | }) 34 | 35 | const { resolve } = createResolver(import.meta.url) 36 | 37 | // Add server utils 38 | nuxt.options.nitro = defu(nuxt.options.nitro, 39 | { 40 | alias: { 41 | '#auth_utils': resolve('./runtime/server/utils'), 42 | }, 43 | }, 44 | ) 45 | 46 | addTemplate({ 47 | filename: 'types/auth_utils.d.ts', 48 | getContents: () => 49 | [ 50 | 'declare module \'#auth_utils\' {', 51 | ` const encode: typeof import('${resolve('./runtime/server/utils')}').encode`, 52 | ` const decode: typeof import('${resolve('./runtime/server/utils')}').decode`, 53 | ` const compareSync: typeof import('${resolve('./runtime/server/utils')}').compareSync`, 54 | ` const hashSync: typeof import('${resolve('./runtime/server/utils')}').hashSync`, 55 | ` const sendMail: typeof import('${resolve('./runtime/server/utils')}').sendMail`, 56 | ` const handleError: typeof import('${resolve('./runtime/server/utils')}').handleError`, 57 | ` const setEventContext: typeof import('${resolve('./runtime/server/utils')}').setEventContext`, 58 | ` const defineAdapter: typeof import('${resolve('./runtime/server/utils')}').defineAdapter`, 59 | ` const usePrismaAdapter: typeof import('${resolve('./runtime/server/utils')}').usePrismaAdapter`, 60 | ` const useUnstorageAdapter: typeof import('${resolve('./runtime/server/utils')}').useUnstorageAdapter`, 61 | '}', 62 | ].join('\n'), 63 | }) 64 | 65 | nuxt.hook('prepare:types', (options) => { 66 | options.references.push({ 67 | path: resolve(nuxt.options.buildDir, 'types/auth_utils.d.ts'), 68 | }) 69 | }) 70 | 71 | // Add server routes 72 | addServerHandler({ 73 | route: '/api/auth/login', 74 | handler: resolve('./runtime/server/api/login/index.post'), 75 | }) 76 | 77 | addServerHandler({ 78 | route: '/api/auth/register', 79 | handler: resolve('./runtime/server/api/register.post'), 80 | }) 81 | 82 | addServerHandler({ 83 | route: '/api/auth/me', 84 | handler: resolve('./runtime/server/api/me.get'), 85 | }) 86 | 87 | addServerHandler({ 88 | route: '/api/auth/logout', 89 | handler: resolve('./runtime/server/api/logout.post'), 90 | }) 91 | 92 | addServerHandler({ 93 | route: '/api/auth/password/change', 94 | handler: resolve('./runtime/server/api/password/change.put'), 95 | }) 96 | 97 | addServerHandler({ 98 | route: '/api/auth/sessions/:id', 99 | handler: resolve('./runtime/server/api/sessions/[id].delete'), 100 | }) 101 | 102 | addServerHandler({ 103 | route: '/api/auth/sessions', 104 | handler: resolve('./runtime/server/api/sessions/index.delete'), 105 | }) 106 | 107 | addServerHandler({ 108 | route: '/api/auth/sessions/refresh', 109 | handler: resolve('./runtime/server/api/sessions/refresh.post'), 110 | }) 111 | 112 | addServerHandler({ 113 | route: '/api/auth/sessions', 114 | handler: resolve('./runtime/server/api/sessions/index.get'), 115 | }) 116 | 117 | addServerHandler({ 118 | route: '/api/auth/avatar', 119 | handler: resolve('./runtime/server/api/avatar.get'), 120 | }) 121 | 122 | if (!options.registration.enabled) { 123 | info('Registration is disabled') 124 | } 125 | 126 | if (options.oauth && Object.keys(options.oauth).length) { 127 | if (options.redirect.callback) { 128 | addServerHandler({ 129 | route: '/api/auth/login/:provider', 130 | handler: resolve('./runtime/server/api/login/[provider].get'), 131 | }) 132 | 133 | addServerHandler({ 134 | route: '/api/auth/login/:provider/callback', 135 | handler: resolve('./runtime/server/api/login/[provider]/callback.get'), 136 | }) 137 | } 138 | else { 139 | warnRequiredOption('redirect.callback') 140 | } 141 | } 142 | else { 143 | info('Oauth login is disabled') 144 | } 145 | 146 | if (options.email?.from) { 147 | if (options.redirect.passwordReset) { 148 | addServerHandler({ 149 | route: '/api/auth/password/request', 150 | handler: resolve('./runtime/server/api/password/request.post'), 151 | }) 152 | 153 | addServerHandler({ 154 | route: '/api/auth/password/reset', 155 | handler: resolve('./runtime/server/api/password/reset.put'), 156 | }) 157 | } 158 | else { 159 | warnRequiredOption('redirect.passwordReset') 160 | } 161 | 162 | if (options.redirect.emailVerify) { 163 | addServerHandler({ 164 | route: '/api/auth/email/request', 165 | handler: resolve('./runtime/server/api/email/request.post'), 166 | }) 167 | 168 | addServerHandler({ 169 | route: '/api/auth/email/verify', 170 | handler: resolve('./runtime/server/api/email/verify.get'), 171 | }) 172 | } 173 | else { 174 | warnRequiredOption('redirect.emailVerify') 175 | } 176 | 177 | options.email.templates ||= {} 178 | 179 | if (options.email.templates.emailVerify) { 180 | const emailVerifyPath = resolveAbsolute(nuxt.options.srcDir, options.email.templates.emailVerify) 181 | options.email.templates.emailVerify = readFileSync(emailVerifyPath, 'utf-8') 182 | } 183 | else { 184 | const emailVerifyPath = resolve('./runtime/templates/email_verification.html') 185 | options.email.templates.emailVerify = readFileSync(emailVerifyPath, 'utf-8') 186 | } 187 | 188 | if (options.email.templates.passwordReset) { 189 | const passwordResetPath = resolveAbsolute(nuxt.options.srcDir, options.email.templates.passwordReset) 190 | options.email.templates.passwordReset = readFileSync(passwordResetPath, 'utf-8') 191 | } 192 | else { 193 | const passwordResetPath = resolve('./runtime/templates/password_reset.html') 194 | options.email.templates.passwordReset = readFileSync(passwordResetPath, 'utf-8') 195 | } 196 | } 197 | else { 198 | info('Email verification and password reset are disabled') 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/setup_frontend.ts: -------------------------------------------------------------------------------- 1 | import { addPlugin, createResolver, addImports, addRouteMiddleware } from '@nuxt/kit' 2 | import { defu } from 'defu' 3 | import type { Nuxt } from '@nuxt/schema' 4 | import type { ModuleOptions } from './runtime/types/config' 5 | import { warnRequiredOption } from './utils' 6 | 7 | export function setupFrontend(options: ModuleOptions, nuxt: Nuxt) { 8 | if (!options.redirect.login) { 9 | warnRequiredOption('redirect.login') 10 | } 11 | 12 | if (!options.redirect.logout) { 13 | warnRequiredOption('redirect.logout') 14 | } 15 | 16 | if (!options.redirect.home) { 17 | warnRequiredOption('redirect.home') 18 | } 19 | 20 | if (!options.baseUrl) { 21 | warnRequiredOption('baseUrl') 22 | } 23 | 24 | if (options.backendEnabled === false && !options.backendBaseUrl) { 25 | warnRequiredOption('backendBaseUrl') 26 | } 27 | 28 | // Initialize the module options 29 | nuxt.options.runtimeConfig.public = defu(nuxt.options.runtimeConfig.public, { 30 | auth: { 31 | backendBaseUrl: options.backendBaseUrl, 32 | baseUrl: options.baseUrl, 33 | enableGlobalAuthMiddleware: options.enableGlobalAuthMiddleware, 34 | loggedInFlagName: options.loggedInFlagName, 35 | redirect: options.redirect, 36 | refreshToken: { 37 | cookieName: options.refreshToken.cookieName, 38 | }, 39 | }, 40 | }) 41 | 42 | const { resolve } = createResolver(import.meta.url) 43 | 44 | // Add nuxt plugins 45 | addPlugin(resolve('./runtime/plugins/provider')) 46 | addPlugin(resolve('./runtime/plugins/flow')) 47 | 48 | // Add composables 49 | addImports([ 50 | { 51 | name: 'useAuth', 52 | from: resolve('./runtime/composables/useAuth'), 53 | }, 54 | { 55 | name: 'useAuthSession', 56 | from: resolve('./runtime/composables/useAuthSession'), 57 | }, 58 | ]) 59 | 60 | addRouteMiddleware({ 61 | name: 'auth', 62 | path: resolve('./runtime/middleware/auth'), 63 | global: !!options.enableGlobalAuthMiddleware, 64 | }) 65 | 66 | addRouteMiddleware({ 67 | name: 'guest', 68 | path: resolve('./runtime/middleware/guest'), 69 | }) 70 | 71 | addRouteMiddleware({ 72 | name: '_auth-common', 73 | path: resolve('./runtime/middleware/common'), 74 | global: true, 75 | }) 76 | 77 | nuxt.options.alias = defu(nuxt.options.alias, { 78 | '#auth_adapter': resolve('./runtime/types/adapter.d.ts'), 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@nuxt/kit' 2 | 3 | export function warnRequiredOption(field: string) { 4 | logger.warn(`[nuxt-auth] config option '${field}' is required`) 5 | } 6 | 7 | export function info(message: string) { 8 | logger.info(`[nuxt-auth] ${message}`) 9 | } 10 | -------------------------------------------------------------------------------- /tests/basic.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | import { goto, login, reload, credentials } from './utils' 3 | 4 | test('should be logged in', async ({ browser }) => { 5 | const context = await browser.newContext() 6 | const page = await context.newPage() 7 | await login(page) 8 | 9 | await reload(page) 10 | await expect(page.getByRole('heading', { name: 'Home' })).toBeVisible() 11 | 12 | await goto(page, '/') 13 | await expect(page.getByRole('heading', { name: 'Home' })).toBeVisible() 14 | 15 | await page.goto('/404') 16 | await expect(page.getByRole('heading', { name: '404' })).toBeVisible() 17 | await page.getByRole('link', { name: 'Go back home' }).click() 18 | await expect(page.getByRole('heading', { name: 'Home' })).toBeVisible() 19 | }) 20 | 21 | test('should refresh session', async ({ browser }) => { 22 | const context = await browser.newContext() 23 | const page = await context.newPage() 24 | await login(page) 25 | 26 | const currentSession = await page.locator('li').first().textContent() 27 | expect(currentSession).toContain('true') 28 | await page.waitForTimeout(16000) 29 | await page.getByRole('button', { name: 'Update sessions' }).click() 30 | await page.waitForTimeout(2000) 31 | const newSession = await page.locator('li').first().textContent() 32 | expect(newSession).not.toEqual(currentSession) 33 | }) 34 | 35 | test('should render user avatar', async ({ browser }) => { 36 | const context = await browser.newContext() 37 | const page = await context.newPage() 38 | await login(page) 39 | 40 | await expect(page.locator('img')).not.toHaveJSProperty('naturalWidth', 0) 41 | }) 42 | 43 | test('should request password reset', async ({ page }) => { 44 | await goto(page, '/auth/login') 45 | await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible() 46 | await page.getByPlaceholder('email').fill(credentials.email) 47 | await page.getByRole('button', { name: 'Forgot password', exact: true }).click() 48 | await page.waitForTimeout(2000) 49 | const result = await page.getByTestId('password-reset-result').textContent() 50 | expect(result).toMatch(/ok/) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/logout.teardown.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | import { login, logout } from './utils' 3 | 4 | test('should revoke sessions', async ({ page }) => { 5 | await login(page) 6 | 7 | await page.getByRole('button', { name: 'Delete all my sessions' }).click() 8 | await page.getByRole('button', { name: 'Update sessions' }).click() 9 | await page.waitForTimeout(2000) 10 | const sessionsCount = await page.locator('li').count() 11 | expect(sessionsCount).toBe(1) 12 | 13 | await logout(page) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/register.setup.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | import { goto, credentials } from './utils' 3 | 4 | test('should register', async ({ page }) => { 5 | await goto(page, '/auth/register') 6 | await page.getByPlaceholder('name').fill('tester') 7 | await page.getByPlaceholder('email').fill(credentials.email) 8 | await page.getByPlaceholder('password').fill(credentials.password) 9 | await page.getByRole('button', { name: 'Register' }).click() 10 | await page.waitForTimeout(2000) 11 | const result = await page.getByTestId('registration-result').textContent() 12 | expect(result).toMatch(/ok|Email already used/) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { type Page, expect } from '@playwright/test' 2 | 3 | export const credentials = { email: 'test@test.com', password: 'abc123' } 4 | 5 | export async function goto(page: Page, path: string) { 6 | await page.goto(path) 7 | await expect(page.getByTestId('hydration-check')).toBeAttached() 8 | } 9 | 10 | export async function reload(page: Page) { 11 | await page.reload() 12 | await expect(page.getByTestId('hydration-check')).toBeAttached() 13 | } 14 | 15 | export async function login(page: Page) { 16 | await goto(page, '/auth/login') 17 | await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible() 18 | await page.getByPlaceholder('email').fill(credentials.email) 19 | await page.getByPlaceholder('password').fill(credentials.password) 20 | await page.getByRole('button', { name: 'Login', exact: true }).click() 21 | await expect(page.getByRole('heading', { name: 'Home' })).toBeVisible() 22 | } 23 | 24 | export async function logout(page: Page) { 25 | await page.getByRole('button', { name: 'Logout' }).click() 26 | await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible() 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./playground/.nuxt/tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------