From e0136bd3ad04fa03ada775105675a3e02ecd9913 Mon Sep 17 00:00:00 2001 From: kingm Date: Sat, 24 Aug 2024 15:18:40 +0100 Subject: [PATCH 01/15] added enhancment to allow signUp function return response if SignUpOption?.PreventLoginFlow is true --- pnpm-lock.yaml | 268 +---------------------- src/runtime/composables/local/useAuth.ts | 11 +- src/runtime/types.ts | 9 + 3 files changed, 17 insertions(+), 271 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e0ffeee..68e9e80e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,7 +77,7 @@ importers: version: 8.57.0 nuxt: specifier: ^3.12.4 - version: 3.12.4(@parcel/watcher@2.4.1)(@types/node@20.12.7)(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.3)(rollup@4.19.2)(terser@5.30.3)(typescript@5.5.4)(vite@5.3.3(@types/node@18.19.42)(terser@5.30.3))(vue-tsc@2.0.29(typescript@5.5.4)) + version: 3.12.4(@parcel/watcher@2.4.1)(@types/node@20.12.7)(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.3)(rollup@4.19.2)(terser@5.30.3)(typescript@5.5.4)(vite@5.3.5(@types/node@20.12.7)(terser@5.30.3))(vue-tsc@2.0.29(typescript@5.5.4)) typescript: specifier: ^5.5.4 version: 5.5.4 @@ -125,43 +125,6 @@ importers: specifier: ^2.0.29 version: 2.0.29(typescript@5.5.4) - playground-refresh: - dependencies: - jsonwebtoken: - specifier: ^9.0.2 - version: 9.0.2 - zod: - specifier: ^3.23.8 - version: 3.23.8 - devDependencies: - '@nuxt/test-utils': - specifier: ^3.14.1 - version: 3.14.1(@playwright/test@1.46.0)(@vue/test-utils@2.4.6)(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(encoding@0.1.13)(magicast@0.3.4))(playwright-core@1.46.0)(rollup@4.19.2)(vite@5.3.5(@types/node@20.12.7)(terser@5.30.3))(vitest@1.6.0(@types/node@20.12.7)(terser@5.30.3))(vue-router@4.4.2(vue@3.4.35(typescript@5.5.4)))(vue@3.4.35(typescript@5.5.4)) - '@playwright/test': - specifier: ^1.46.0 - version: 1.46.0 - '@types/jsonwebtoken': - specifier: ^9.0.6 - version: 9.0.6 - '@vue/test-utils': - specifier: ^2.4.6 - version: 2.4.6 - eslint: - specifier: ^8.57.0 - version: 8.57.0 - nuxt: - specifier: ^3.12.4 - version: 3.12.4(@parcel/watcher@2.4.1)(@types/node@20.12.7)(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.3)(rollup@4.19.2)(terser@5.30.3)(typescript@5.5.4)(vite@5.3.5(@types/node@20.12.7)(terser@5.30.3))(vue-tsc@2.0.29(typescript@5.5.4)) - typescript: - specifier: ^5.5.4 - version: 5.5.4 - vitest: - specifier: ^1.6.0 - version: 1.6.0(@types/node@20.12.7)(terser@5.30.3) - vue-tsc: - specifier: ^2.0.29 - version: 2.0.29(typescript@5.5.4) - packages: '@aashutoshrathi/word-wrap@1.2.6': @@ -7008,46 +6971,6 @@ snapshots: - rollup - supports-color - '@nuxt/test-utils@3.14.1(@playwright/test@1.46.0)(@vue/test-utils@2.4.6)(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(encoding@0.1.13)(magicast@0.3.4))(playwright-core@1.46.0)(rollup@4.19.2)(vite@5.3.5(@types/node@20.12.7)(terser@5.30.3))(vitest@1.6.0(@types/node@20.12.7)(terser@5.30.3))(vue-router@4.4.2(vue@3.4.35(typescript@5.5.4)))(vue@3.4.35(typescript@5.5.4))': - dependencies: - '@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@4.19.2) - '@nuxt/schema': 3.12.4(rollup@4.19.2) - c12: 1.11.1(magicast@0.3.4) - consola: 3.2.3 - defu: 6.1.4 - destr: 2.0.3 - estree-walker: 3.0.3 - execa: 8.0.1 - fake-indexeddb: 6.0.0 - get-port-please: 3.1.2 - h3: 1.12.0 - local-pkg: 0.5.0 - magic-string: 0.30.11 - nitropack: 2.9.7(encoding@0.1.13)(magicast@0.3.4) - node-fetch-native: 1.6.4 - ofetch: 1.3.4 - pathe: 1.1.2 - perfect-debounce: 1.0.0 - radix3: 1.1.2 - scule: 1.3.0 - std-env: 3.7.0 - ufo: 1.5.4 - unenv: 1.10.0 - unplugin: 1.12.1 - vite: 5.3.5(@types/node@20.12.7)(terser@5.30.3) - vitest-environment-nuxt: 1.0.0(@playwright/test@1.46.0)(@vue/test-utils@2.4.6)(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(encoding@0.1.13)(magicast@0.3.4))(playwright-core@1.46.0)(rollup@4.19.2)(vite@5.3.5(@types/node@20.12.7)(terser@5.30.3))(vitest@1.6.0(@types/node@20.12.7)(terser@5.30.3))(vue-router@4.4.2(vue@3.4.35(typescript@5.5.4)))(vue@3.4.35(typescript@5.5.4)) - vue: 3.4.35(typescript@5.5.4) - vue-router: 4.4.2(vue@3.4.35(typescript@5.5.4)) - optionalDependencies: - '@playwright/test': 1.46.0 - '@vue/test-utils': 2.4.6 - playwright-core: 1.46.0 - vitest: 1.6.0(@types/node@20.12.7)(terser@5.30.3) - transitivePeerDependencies: - - magicast - - rollup - - supports-color - '@nuxt/vite-builder@3.12.4(@types/node@18.19.42)(eslint@8.57.0)(magicast@0.3.4)(optionator@0.9.3)(rollup@4.19.2)(terser@5.30.3)(typescript@5.5.4)(vue-tsc@2.0.29(typescript@5.5.4))(vue@3.4.35(typescript@5.5.4))': dependencies: '@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@4.19.2) @@ -10612,112 +10535,6 @@ snapshots: - vue-tsc - xml2js - nuxt@3.12.4(@parcel/watcher@2.4.1)(@types/node@20.12.7)(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.3)(rollup@4.19.2)(terser@5.30.3)(typescript@5.5.4)(vite@5.3.3(@types/node@18.19.42)(terser@5.30.3))(vue-tsc@2.0.29(typescript@5.5.4)): - dependencies: - '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 1.3.9(rollup@4.19.2)(vite@5.3.3(@types/node@18.19.42)(terser@5.30.3)) - '@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@4.19.2) - '@nuxt/schema': 3.12.4(rollup@4.19.2) - '@nuxt/telemetry': 2.5.4(magicast@0.3.4)(rollup@4.19.2) - '@nuxt/vite-builder': 3.12.4(@types/node@20.12.7)(eslint@8.57.0)(magicast@0.3.4)(optionator@0.9.3)(rollup@4.19.2)(terser@5.30.3)(typescript@5.5.4)(vue-tsc@2.0.29(typescript@5.5.4))(vue@3.4.35(typescript@5.5.4)) - '@unhead/dom': 1.9.16 - '@unhead/ssr': 1.9.16 - '@unhead/vue': 1.9.16(vue@3.4.35(typescript@5.5.4)) - '@vue/shared': 3.4.35 - acorn: 8.12.1 - c12: 1.11.1(magicast@0.3.4) - chokidar: 3.6.0 - compatx: 0.1.8 - consola: 3.2.3 - cookie-es: 1.1.0 - defu: 6.1.4 - destr: 2.0.3 - devalue: 5.0.0 - errx: 0.1.0 - esbuild: 0.23.0 - escape-string-regexp: 5.0.0 - estree-walker: 3.0.3 - globby: 14.0.2 - h3: 1.12.0 - hookable: 5.5.3 - ignore: 5.3.1 - jiti: 1.21.6 - klona: 2.0.6 - knitwork: 1.1.0 - magic-string: 0.30.10 - mlly: 1.7.1 - nitropack: 2.9.7(encoding@0.1.13)(magicast@0.3.4) - nuxi: 3.12.0 - nypm: 0.3.9 - ofetch: 1.3.4 - ohash: 1.1.3 - pathe: 1.1.2 - perfect-debounce: 1.0.0 - pkg-types: 1.1.3 - radix3: 1.1.2 - scule: 1.3.0 - semver: 7.6.3 - std-env: 3.7.0 - strip-literal: 2.1.0 - ufo: 1.5.4 - ultrahtml: 1.5.3 - uncrypto: 0.1.3 - unctx: 2.3.1 - unenv: 1.10.0 - unimport: 3.9.1(rollup@4.19.2) - unplugin: 1.12.0 - unplugin-vue-router: 0.10.2(rollup@4.19.2)(vue-router@4.4.2(vue@3.4.35(typescript@5.5.4)))(vue@3.4.35(typescript@5.5.4)) - unstorage: 1.10.2(ioredis@5.4.1) - untyped: 1.4.2 - vue: 3.4.35(typescript@5.5.4) - vue-bundle-renderer: 2.1.0 - vue-devtools-stub: 0.1.0 - vue-router: 4.4.2(vue@3.4.35(typescript@5.5.4)) - optionalDependencies: - '@parcel/watcher': 2.4.1 - '@types/node': 20.12.7 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@biomejs/biome' - - '@capacitor/preferences' - - '@libsql/client' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/kv' - - better-sqlite3 - - bufferutil - - drizzle-orm - - encoding - - eslint - - idb-keyval - - ioredis - - less - - lightningcss - - magicast - - meow - - optionator - - rollup - - sass - - stylelint - - stylus - - sugarss - - supports-color - - terser - - typescript - - uWebSockets.js - - utf-8-validate - - vite - - vls - - vti - - vue-tsc - - xml2js - nuxt@3.12.4(@parcel/watcher@2.4.1)(@types/node@20.12.7)(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.3)(rollup@4.19.2)(terser@5.30.3)(typescript@5.5.4)(vite@5.3.5(@types/node@20.12.7)(terser@5.30.3))(vue-tsc@2.0.29(typescript@5.5.4)): dependencies: '@nuxt/devalue': 2.0.2 @@ -12291,23 +12108,6 @@ snapshots: - supports-color - terser - vite-node@1.6.0(@types/node@20.12.7)(terser@5.30.3): - dependencies: - cac: 6.7.14 - debug: 4.3.4 - pathe: 1.1.2 - picocolors: 1.0.0 - vite: 5.2.9(@types/node@20.12.7)(terser@5.30.3) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - vite-node@2.0.5(@types/node@18.19.42)(terser@5.30.3): dependencies: cac: 6.7.14 @@ -12537,16 +12337,6 @@ snapshots: fsevents: 2.3.3 terser: 5.30.3 - vite@5.2.9(@types/node@20.12.7)(terser@5.30.3): - dependencies: - esbuild: 0.20.2 - postcss: 8.4.38 - rollup: 4.14.3 - optionalDependencies: - '@types/node': 20.12.7 - fsevents: 2.3.3 - terser: 5.30.3 - vite@5.3.3(@types/node@18.19.42)(terser@5.30.3): dependencies: esbuild: 0.21.5 @@ -12657,29 +12447,6 @@ snapshots: - vue - vue-router - vitest-environment-nuxt@1.0.0(@playwright/test@1.46.0)(@vue/test-utils@2.4.6)(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(encoding@0.1.13)(magicast@0.3.4))(playwright-core@1.46.0)(rollup@4.19.2)(vite@5.3.5(@types/node@20.12.7)(terser@5.30.3))(vitest@1.6.0(@types/node@20.12.7)(terser@5.30.3))(vue-router@4.4.2(vue@3.4.35(typescript@5.5.4)))(vue@3.4.35(typescript@5.5.4)): - dependencies: - '@nuxt/test-utils': 3.14.1(@playwright/test@1.46.0)(@vue/test-utils@2.4.6)(h3@1.12.0)(magicast@0.3.4)(nitropack@2.9.7(encoding@0.1.13)(magicast@0.3.4))(playwright-core@1.46.0)(rollup@4.19.2)(vite@5.3.5(@types/node@20.12.7)(terser@5.30.3))(vitest@1.6.0(@types/node@20.12.7)(terser@5.30.3))(vue-router@4.4.2(vue@3.4.35(typescript@5.5.4)))(vue@3.4.35(typescript@5.5.4)) - transitivePeerDependencies: - - '@cucumber/cucumber' - - '@jest/globals' - - '@playwright/test' - - '@testing-library/vue' - - '@vitest/ui' - - '@vue/test-utils' - - h3 - - happy-dom - - jsdom - - magicast - - nitropack - - playwright-core - - rollup - - supports-color - - vite - - vitest - - vue - - vue-router - vitest@1.6.0(@types/node@18.19.44)(terser@5.30.3): dependencies: '@vitest/expect': 1.6.0 @@ -12713,39 +12480,6 @@ snapshots: - supports-color - terser - vitest@1.6.0(@types/node@20.12.7)(terser@5.30.3): - dependencies: - '@vitest/expect': 1.6.0 - '@vitest/runner': 1.6.0 - '@vitest/snapshot': 1.6.0 - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - acorn-walk: 8.3.2 - chai: 4.4.1 - debug: 4.3.4 - execa: 8.0.1 - local-pkg: 0.5.0 - magic-string: 0.30.7 - pathe: 1.1.2 - picocolors: 1.0.0 - std-env: 3.7.0 - strip-literal: 2.0.0 - tinybench: 2.6.0 - tinypool: 0.8.4 - vite: 5.2.9(@types/node@20.12.7)(terser@5.30.3) - vite-node: 1.6.0(@types/node@20.12.7)(terser@5.30.3) - why-is-node-running: 2.2.2 - optionalDependencies: - '@types/node': 20.12.7 - transitivePeerDependencies: - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - vscode-jsonrpc@6.0.0: {} vscode-languageclient@7.0.0: diff --git a/src/runtime/composables/local/useAuth.ts b/src/runtime/composables/local/useAuth.ts index bef5f287..b31d7b15 100644 --- a/src/runtime/composables/local/useAuth.ts +++ b/src/runtime/composables/local/useAuth.ts @@ -1,6 +1,6 @@ import { readonly, type Ref } from 'vue' import { callWithNuxt } from '#app/nuxt' -import type { CommonUseAuthReturn, SignOutFunc, SignInFunc, GetSessionFunc, SecondarySignInOptions, SignUpOptions, GetSessionOptions } from '../../types' +import type { CommonUseAuthReturn, SignOutFunc, SignInFunc, GetSessionFunc, SecondarySignInOptions, SignUpOptions, GetSessionOptions, SignUpResponse } from '../../types' import { jsonPointerGet, objectFromJsonPointer, useTypedBackendConfig } from '../../helpers' import { _fetch } from '../../utils/fetch' import { getRequestURLWN } from '../../utils/callWithNuxt' @@ -150,17 +150,20 @@ const getSession: GetSessionFunc = async (getSessionO return data.value } -const signUp = async (credentials: Credentials, signInOptions?: SecondarySignInOptions, signUpOptions?: SignUpOptions) => { +const signUp = async (credentials: Credentials, signInOptions?: SecondarySignInOptions, signUpOptions?: SignUpOptions): Promise => { const nuxt = useNuxtApp() const { path, method } = useTypedBackendConfig(useRuntimeConfig(), 'local').endpoints.signUp - await _fetch(nuxt, path, { + + // Holds result from fetch to be returned if signUpOptions?.preventLoginFlow is true + const result = await _fetch(nuxt, path, { method, body: credentials }) if (signUpOptions?.preventLoginFlow) { - return + // Returns result + return result } return signIn(credentials, signInOptions) diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 851bb251..f8a5cb67 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -53,6 +53,15 @@ export type SessionDataObject = { | SessionDataObject; }; +/** + * Type of data returned from sign up functiom if preventLoginFlow is true +*/ +export type SignUpResponse = { + id?: string; // Optional id field + token?: string; // Optional token field + [key: string]: any; // Allow additional fields with any type +}; + /** * Available `nuxt-auth` authentication providers. */ From e3bf0fca5ce2b62685a0c202c7260c36eb53825f Mon Sep 17 00:00:00 2001 From: kingm Date: Sun, 8 Sep 2024 15:38:48 +0100 Subject: [PATCH 02/15] sign up function to return data, test written --- playground-local/nuxt.config.ts | 3 +- playground-local/pages/index.vue | 4 ++ playground-local/pages/register.vue | 43 +++++++++++++++++++ .../server/api/auth/signup.post.ts | 40 +++++++++++++++++ playground-local/tests/local.spec.ts | 26 +++++++++++ 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 playground-local/pages/register.vue create mode 100644 playground-local/server/api/auth/signup.post.ts diff --git a/playground-local/nuxt.config.ts b/playground-local/nuxt.config.ts index 3dabe199..f9d7ba0c 100644 --- a/playground-local/nuxt.config.ts +++ b/playground-local/nuxt.config.ts @@ -8,7 +8,8 @@ export default defineNuxtConfig({ provider: { type: 'local', endpoints: { - getSession: { path: '/user' } + getSession: { path: '/user' }, + signUp:{path:'/signup', method: 'post'} }, pages: { login: '/' diff --git a/playground-local/pages/index.vue b/playground-local/pages/index.vue index 5b9707ab..a87a48b9 100644 --- a/playground-local/pages/index.vue +++ b/playground-local/pages/index.vue @@ -10,6 +10,10 @@ definePageMeta({ auth: false }) -> manual login, logout, refresh button
+ + -> Click to signup + +
-> globally protected page diff --git a/playground-local/pages/register.vue b/playground-local/pages/register.vue new file mode 100644 index 00000000..c013b510 --- /dev/null +++ b/playground-local/pages/register.vue @@ -0,0 +1,43 @@ + + + diff --git a/playground-local/server/api/auth/signup.post.ts b/playground-local/server/api/auth/signup.post.ts new file mode 100644 index 00000000..661e7c8d --- /dev/null +++ b/playground-local/server/api/auth/signup.post.ts @@ -0,0 +1,40 @@ +import { createError, eventHandler, readBody } from 'h3' +import { z } from 'zod' +import { sign } from 'jsonwebtoken' + +export const SECRET = 'dummy' + +export default eventHandler(async (event) => { + // Define the schema for validating the incoming data + const result = z.object({ + username: z.string(), + password: z.string().min(6) + }).safeParse(await readBody(event)) + + // If validation fails, return an error + if (!result.success) { + throw createError({ + statusCode: 400, + statusMessage: 'Invalid input, please provide a valid email and a password of at least 6 characters.' + }) + } + + const { username, password } = result.data + + const expiresIn = '1h' //token expiry (1 hour) + const user = { username } // Payload for the token, includes the email + + // Sign the JWT with the user payload and secret + const accessToken = sign(user, SECRET, { expiresIn }) + + // Return a success response with the email and the token + return { + message: 'Signup successful!', + user: { + username + }, + token: { + accessToken + } + } +}) \ No newline at end of file diff --git a/playground-local/tests/local.spec.ts b/playground-local/tests/local.spec.ts index 29abeae5..e42900f0 100644 --- a/playground-local/tests/local.spec.ts +++ b/playground-local/tests/local.spec.ts @@ -59,4 +59,30 @@ describe('local Provider', async () => { await signoutButton.click() await playwrightExpect(status).toHaveText(STATUS_UNAUTHENTICATED) }) + + it('should sign up and return signup data when preventLoginFlow: true', async () => { + const page = await createPage('/register') // Navigate to signup page + + const [ + usernameInput, + passwordInput, + submitButton, + ] = await Promise.all([ + page.getByTestId('regUsername'), + page.getByTestId('regPassword'), + page.getByTestId('regSubmit') + ]) + + await usernameInput.fill('newuser') + await passwordInput.fill('hunter2') + + // Click button and wait for API to finish + const responsePromise = page.waitForResponse(/\/api\/auth\/signup/) + await submitButton.click() + const response = await responsePromise + + // Expect the response to return signup data + const responseBody = await response.json() // Parse response + playwrightExpect(responseBody).toBeDefined() // Ensure data is returned + }) }) From 1bdf14422d376c9c9972e6f3c0dcb359fed31045 Mon Sep 17 00:00:00 2001 From: Zoey Date: Fri, 13 Sep 2024 01:38:42 +0200 Subject: [PATCH 03/15] Discard changes to pnpm-lock.yaml --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 264707fe..0f80d620 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,7 +80,7 @@ importers: devDependencies: nuxt: specifier: ^3.12.4 - version: 3.12.4(@parcel/watcher@2.4.1)(@types/node@20.12.7)(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.3)(rollup@4.19.2)(terser@5.30.3)(typescript@5.5.4)(vite@5.3.5(@types/node@20.12.7)(terser@5.30.3))(vue-tsc@2.0.29(typescript@5.5.4)) + version: 3.12.4(@parcel/watcher@2.4.1)(@types/node@20.12.7)(encoding@0.1.13)(eslint@8.57.0)(ioredis@5.4.1)(magicast@0.3.4)(optionator@0.9.3)(rollup@4.19.2)(terser@5.30.3)(typescript@5.5.4)(vite@5.3.3(@types/node@18.19.42)(terser@5.30.3))(vue-tsc@2.0.29(typescript@5.5.4)) typescript: specifier: ^5.5.4 version: 5.5.4 From d9951f67e45f35e8f86f70c947d1ca066f139017 Mon Sep 17 00:00:00 2001 From: Zoey Date: Fri, 13 Sep 2024 01:42:22 +0200 Subject: [PATCH 04/15] fix: oxlint issue --- playground-local/server/api/auth/signup.post.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playground-local/server/api/auth/signup.post.ts b/playground-local/server/api/auth/signup.post.ts index 661e7c8d..4232c6e4 100644 --- a/playground-local/server/api/auth/signup.post.ts +++ b/playground-local/server/api/auth/signup.post.ts @@ -19,7 +19,7 @@ export default eventHandler(async (event) => { }) } - const { username, password } = result.data + const { username } = result.data const expiresIn = '1h' //token expiry (1 hour) const user = { username } // Payload for the token, includes the email @@ -37,4 +37,4 @@ export default eventHandler(async (event) => { accessToken } } -}) \ No newline at end of file +}) From 0caeefcbc3e0ddf1b1776a53c7798b0cdaac72e6 Mon Sep 17 00:00:00 2001 From: kingm Date: Tue, 17 Sep 2024 09:52:46 +0100 Subject: [PATCH 05/15] enh: added generic type support to signUp function for flexible return types --- src/runtime/composables/local/useAuth.ts | 6 +++--- src/runtime/types.ts | 9 --------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/runtime/composables/local/useAuth.ts b/src/runtime/composables/local/useAuth.ts index c8ce1d8a..d5528202 100644 --- a/src/runtime/composables/local/useAuth.ts +++ b/src/runtime/composables/local/useAuth.ts @@ -1,5 +1,5 @@ import { readonly, type Ref } from 'vue' -import type { CommonUseAuthReturn, SignOutFunc, SignInFunc, SecondarySignInOptions, SignUpOptions, GetSessionOptions, SignUpResponse } from '../../types' +import type { CommonUseAuthReturn, SignOutFunc, SignInFunc, SecondarySignInOptions, SignUpOptions, GetSessionOptions } from '../../types' import { jsonPointerGet, objectFromJsonPointer, useTypedBackendConfig } from '../../helpers' import { _fetch } from '../../utils/fetch' import { getRequestURLWN } from '../../utils/callWithNuxt' @@ -153,13 +153,13 @@ async function getSession(getSessionOptions?: GetSessionOptions): Promise => { +const signUp = async (credentials: Credentials, signInOptions?: SecondarySignInOptions, signUpOptions?: SignUpOptions): Promise => { const nuxt = useNuxtApp() const { path, method } = useTypedBackendConfig(useRuntimeConfig(), 'local').endpoints.signUp // Holds result from fetch to be returned if signUpOptions?.preventLoginFlow is true - const result = await _fetch(nuxt, path, { + const result = await _fetch(nuxt, path, { method, body: credentials }) diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 17740dc1..17152cec 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -53,15 +53,6 @@ export interface SessionDataObject { | SessionDataObject } -/** - * Type of data returned from sign up functiom if preventLoginFlow is true -*/ -export type SignUpResponse = { - id?: string; // Optional id field - token?: string; // Optional token field - [key: string]: any; // Allow additional fields with any type -}; - /** * Available `nuxt-auth` authentication providers. */ From 9cff966689e1a375ab05853603a9cac886dfd566 Mon Sep 17 00:00:00 2001 From: kingm Date: Tue, 17 Sep 2024 10:48:25 +0100 Subject: [PATCH 06/15] Resolved lint errors and formatting issues --- playground-local/nuxt.config.ts | 2 +- playground-local/pages/register.vue | 48 ++++++++++--------- .../server/api/auth/signup.post.ts | 2 +- playground-local/tests/local.spec.ts | 6 +-- src/runtime/composables/local/useAuth.ts | 6 +-- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/playground-local/nuxt.config.ts b/playground-local/nuxt.config.ts index 02a9039e..94a2c967 100644 --- a/playground-local/nuxt.config.ts +++ b/playground-local/nuxt.config.ts @@ -9,7 +9,7 @@ export default defineNuxtConfig({ type: 'local', endpoints: { getSession: { path: '/user' }, - signUp:{path:'/signup', method: 'post'} + signUp: { path: '/signup', method: 'post' } }, pages: { login: '/' diff --git a/playground-local/pages/register.vue b/playground-local/pages/register.vue index c013b510..31956a59 100644 --- a/playground-local/pages/register.vue +++ b/playground-local/pages/register.vue @@ -1,34 +1,36 @@ diff --git a/playground-local/server/api/auth/signup.post.ts b/playground-local/server/api/auth/signup.post.ts index 4232c6e4..849f7d3a 100644 --- a/playground-local/server/api/auth/signup.post.ts +++ b/playground-local/server/api/auth/signup.post.ts @@ -21,7 +21,7 @@ export default eventHandler(async (event) => { const { username } = result.data - const expiresIn = '1h' //token expiry (1 hour) + const expiresIn = '1h' // token expiry (1 hour) const user = { username } // Payload for the token, includes the email // Sign the JWT with the user payload and secret diff --git a/playground-local/tests/local.spec.ts b/playground-local/tests/local.spec.ts index e42900f0..bb155a21 100644 --- a/playground-local/tests/local.spec.ts +++ b/playground-local/tests/local.spec.ts @@ -61,7 +61,7 @@ describe('local Provider', async () => { }) it('should sign up and return signup data when preventLoginFlow: true', async () => { - const page = await createPage('/register') // Navigate to signup page + const page = await createPage('/register') // Navigate to signup page const [ usernameInput, @@ -82,7 +82,7 @@ describe('local Provider', async () => { const response = await responsePromise // Expect the response to return signup data - const responseBody = await response.json() // Parse response - playwrightExpect(responseBody).toBeDefined() // Ensure data is returned + const responseBody = await response.json() // Parse response + playwrightExpect(responseBody).toBeDefined() // Ensure data is returned }) }) diff --git a/src/runtime/composables/local/useAuth.ts b/src/runtime/composables/local/useAuth.ts index d5528202..8e09b40d 100644 --- a/src/runtime/composables/local/useAuth.ts +++ b/src/runtime/composables/local/useAuth.ts @@ -1,5 +1,5 @@ -import { readonly, type Ref } from 'vue' -import type { CommonUseAuthReturn, SignOutFunc, SignInFunc, SecondarySignInOptions, SignUpOptions, GetSessionOptions } from '../../types' +import { type Ref, readonly } from 'vue' +import type { CommonUseAuthReturn, GetSessionOptions, SecondarySignInOptions, SignInFunc, SignOutFunc, SignUpOptions } from '../../types' import { jsonPointerGet, objectFromJsonPointer, useTypedBackendConfig } from '../../helpers' import { _fetch } from '../../utils/fetch' import { getRequestURLWN } from '../../utils/callWithNuxt' @@ -153,7 +153,7 @@ async function getSession(getSessionOptions?: GetSessionOptions): Promise (credentials: Credentials, signInOptions?: SecondarySignInOptions, signUpOptions?: SignUpOptions): Promise => { +async function signUp(credentials: Credentials, signInOptions?: SecondarySignInOptions, signUpOptions?: SignUpOptions): Promise { const nuxt = useNuxtApp() const { path, method } = useTypedBackendConfig(useRuntimeConfig(), 'local').endpoints.signUp From b25597b890e94ceddea25aad117d8f7df0404201 Mon Sep 17 00:00:00 2001 From: King-David Francis <93971653+iamKiNG-Fr@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:55:46 +0100 Subject: [PATCH 07/15] Update src/runtime/composables/local/useAuth.ts Co-authored-by: Zoey --- src/runtime/composables/local/useAuth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/composables/local/useAuth.ts b/src/runtime/composables/local/useAuth.ts index 62da29c6..5a998cdf 100644 --- a/src/runtime/composables/local/useAuth.ts +++ b/src/runtime/composables/local/useAuth.ts @@ -159,7 +159,7 @@ async function getSession(getSessionOptions?: GetSessionOptions): Promise(credentials: Credentials, signInOptions?: SecondarySignInOptions, signUpOptions?: SignUpOptions): Promise { +async function signUp(credentials: Credentials, signInOptions?: SecondarySignInOptions, signUpOptions?: SignUpOptions): Promise { const nuxt = useNuxtApp() const { path, method } = useTypedBackendConfig(useRuntimeConfig(), 'local').endpoints.signUp From 86fd25aaa655151f51d163b62175ce7aac4f41ff Mon Sep 17 00:00:00 2001 From: King-David Francis <93971653+iamKiNG-Fr@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:11:00 +0100 Subject: [PATCH 08/15] Update playground-local/pages/register.vue by aligning test-ids Co-authored-by: Marsel Shayhin <18054980+phoenix-ru@users.noreply.github.com> --- playground-local/pages/register.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playground-local/pages/register.vue b/playground-local/pages/register.vue index 31956a59..911d44a6 100644 --- a/playground-local/pages/register.vue +++ b/playground-local/pages/register.vue @@ -31,9 +31,9 @@ definePageMeta({

*password should have at least 6 characters

- - -
From 94cd4a349d768fd6d480350fd0390f9d14a58ce2 Mon Sep 17 00:00:00 2001 From: King-David Francis <93971653+iamKiNG-Fr@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:12:53 +0100 Subject: [PATCH 09/15] Update playground-local/pages/register.vue to remove unused test-id regResponse Co-authored-by: Marsel Shayhin <18054980+phoenix-ru@users.noreply.github.com> --- playground-local/pages/register.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground-local/pages/register.vue b/playground-local/pages/register.vue index 911d44a6..f23ac214 100644 --- a/playground-local/pages/register.vue +++ b/playground-local/pages/register.vue @@ -39,7 +39,7 @@ definePageMeta({

Response

-
{{ response }}
+
{{ response }}
From f11f9c468b5204214ffe867ac06e7714c06bf16a Mon Sep 17 00:00:00 2001 From: King-David Francis <93971653+iamKiNG-Fr@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:34:50 +0100 Subject: [PATCH 10/15] Update playground-local/tests/local.spec.ts by unifying updated test-ids Co-authored-by: Marsel Shayhin <18054980+phoenix-ru@users.noreply.github.com> --- playground-local/tests/local.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playground-local/tests/local.spec.ts b/playground-local/tests/local.spec.ts index bb155a21..33cd9b37 100644 --- a/playground-local/tests/local.spec.ts +++ b/playground-local/tests/local.spec.ts @@ -68,9 +68,9 @@ describe('local Provider', async () => { passwordInput, submitButton, ] = await Promise.all([ - page.getByTestId('regUsername'), - page.getByTestId('regPassword'), - page.getByTestId('regSubmit') + page.getByTestId('username'), + page.getByTestId('password'), + page.getByTestId('submit') ]) await usernameInput.fill('newuser') From 98e33a9de914344ce36dac438b6e004e5ccd402c Mon Sep 17 00:00:00 2001 From: King-David Francis <93971653+iamKiNG-Fr@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:37:58 +0100 Subject: [PATCH 11/15] Update src/runtime/composables/local/useAuth.ts Co-authored-by: Marsel Shayhin <18054980+phoenix-ru@users.noreply.github.com> --- src/runtime/composables/local/useAuth.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/runtime/composables/local/useAuth.ts b/src/runtime/composables/local/useAuth.ts index 5a998cdf..51fb0a8a 100644 --- a/src/runtime/composables/local/useAuth.ts +++ b/src/runtime/composables/local/useAuth.ts @@ -171,7 +171,6 @@ async function signUp(credentials: Credentials, signInOptions?: SecondarySign }) if (signUpOptions?.preventLoginFlow) { - // Returns result return result } From 8fd0e247188b2ef293ae5c1f580f56311c162d90 Mon Sep 17 00:00:00 2001 From: Marsel Shaikhin Date: Thu, 10 Apr 2025 14:52:14 +0200 Subject: [PATCH 12/15] fix: fix a type in `signUp` function --- src/runtime/composables/local/useAuth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/composables/local/useAuth.ts b/src/runtime/composables/local/useAuth.ts index 06f1cacc..bee1aa3d 100644 --- a/src/runtime/composables/local/useAuth.ts +++ b/src/runtime/composables/local/useAuth.ts @@ -165,7 +165,7 @@ async function getSession(getSessionOptions?: GetSessionOptions): Promise(credentials: Credentials, signInOptions?: SecondarySignInOptions, signUpOptions?: SignUpOptions): Promise { +async function signUp(credentials: Credentials, signInOptions?: SecondarySignInOptions, signUpOptions?: SignUpOptions): Promise { const nuxt = useNuxtApp() const runtimeConfig = useRuntimeConfig() const config = useTypedBackendConfig(runtimeConfig, 'local') From f9ac88fbabfff13fa3dc88a06c39ecf3d9db60c3 Mon Sep 17 00:00:00 2001 From: Marsel Shaikhin Date: Thu, 10 Apr 2025 16:24:11 +0200 Subject: [PATCH 13/15] refact: make demo implementation more unified --- .../server/api/auth/login.post.ts | 82 +------- .../server/api/auth/refresh.post.ts | 37 ++-- .../server/api/auth/signup.post.ts | 34 +--- playground-local/server/api/auth/user.get.ts | 25 ++- playground-local/server/utils/session.ts | 181 ++++++++++++++++++ 5 files changed, 226 insertions(+), 133 deletions(-) create mode 100644 playground-local/server/utils/session.ts diff --git a/playground-local/server/api/auth/login.post.ts b/playground-local/server/api/auth/login.post.ts index c03fabe6..877f3077 100644 --- a/playground-local/server/api/auth/login.post.ts +++ b/playground-local/server/api/auth/login.post.ts @@ -1,52 +1,11 @@ import { createError, eventHandler, readBody } from 'h3' -import { z } from 'zod' -import { sign } from 'jsonwebtoken' +import { createUserTokens, credentialsSchema, getUser } from '~/server/utils/session' /* * DISCLAIMER! * This is a demo implementation, please create your own handlers */ -/** - * This is a demo secret. - * Please ensure that your secret is properly protected. - */ -export const SECRET = 'dummy' - -/** 30 seconds */ -export const ACCESS_TOKEN_TTL = 30 - -export interface User { - username: string - name: string - picture: string -} - -export interface JwtPayload extends User { - scope: Array<'test' | 'user'> - exp?: number -} - -interface TokensByUser { - access: Map - refresh: Map -} - -/** - * Tokens storage. - * You will need to implement your own, connect with DB/etc. - */ -export const tokensByUser: Map = new Map() - -/** - * We use a fixed password for demo purposes. - * You can use any implementation fitting your usecase. - */ -const credentialsSchema = z.object({ - username: z.string().min(1), - password: z.literal('hunter2') -}) - export default eventHandler(async (event) => { const result = credentialsSchema.safeParse(await readBody(event)) if (!result.success) { @@ -56,42 +15,13 @@ export default eventHandler(async (event) => { }) } - // Emulate login - const { username } = result.data - const user = { - username, - picture: 'https://github.com/nuxt.png', - name: `User ${username}` - } + // Emulate successful login + const user = await getUser(result.data.username) - const tokenData: JwtPayload = { ...user, scope: ['test', 'user'] } - const accessToken = sign(tokenData, SECRET, { - expiresIn: ACCESS_TOKEN_TTL - }) - const refreshToken = sign(tokenData, SECRET, { - // 1 day - expiresIn: 60 * 60 * 24 - }) - - // Naive implementation - please implement properly yourself! - const userTokens: TokensByUser = tokensByUser.get(username) ?? { - access: new Map(), - refresh: new Map() - } - userTokens.access.set(accessToken, refreshToken) - userTokens.refresh.set(refreshToken, accessToken) - tokensByUser.set(username, userTokens) + // Sign the tokens + const tokens = await createUserTokens(user) return { - token: { - accessToken, - refreshToken - } + token: tokens } }) - -export function extractToken(authorizationHeader: string) { - return authorizationHeader.startsWith('Bearer ') - ? authorizationHeader.slice(7) - : authorizationHeader -} diff --git a/playground-local/server/api/auth/refresh.post.ts b/playground-local/server/api/auth/refresh.post.ts index 45640263..bc41b531 100644 --- a/playground-local/server/api/auth/refresh.post.ts +++ b/playground-local/server/api/auth/refresh.post.ts @@ -1,6 +1,6 @@ import { createError, eventHandler, getRequestHeader, readBody } from 'h3' import { sign, verify } from 'jsonwebtoken' -import { type JwtPayload, SECRET, type User, extractToken, tokensByUser } from './login.post' +import { checkUserTokens, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser, invalidateAccessToken, refreshUserAccessToken } from '~/server/utils/session' /* * DISCLAIMER! @@ -20,7 +20,7 @@ export default eventHandler(async (event) => { } // Verify - const decoded = verify(refreshToken, SECRET) as JwtPayload | undefined + const decoded = decodeToken(refreshToken) if (!decoded) { throw createError({ statusCode: 401, @@ -28,8 +28,8 @@ export default eventHandler(async (event) => { }) } - // Get tokens - const userTokens = tokensByUser.get(decoded.username) + // Get the helper (only for demo, use a DB in your implementation) + const userTokens = getTokensByUser(decoded.username) if (!userTokens) { throw createError({ statusCode: 401, @@ -38,12 +38,12 @@ export default eventHandler(async (event) => { } // Check against known token - const requestAccessToken = extractToken(authorizationHeader) - const knownAccessToken = userTokens.refresh.get(body.refreshToken) - if (!knownAccessToken || knownAccessToken !== requestAccessToken) { + const requestAccessToken = extractTokenFromAuthorizationHeader(authorizationHeader) + const tokensValidityCheck = checkUserTokens(userTokens, requestAccessToken, refreshToken) + if (!tokensValidityCheck.valid) { console.log({ msg: 'Tokens mismatch', - knownAccessToken, + knownAccessToken: tokensValidityCheck.knownAccessToken, requestAccessToken }) throw createError({ @@ -52,25 +52,10 @@ export default eventHandler(async (event) => { }) } - // Invalidate old access token - userTokens.access.delete(knownAccessToken) - - const user: User = { - username: decoded.username, - picture: decoded.picture, - name: decoded.name - } - - const accessToken = sign({ ...user, scope: ['test', 'user'] }, SECRET, { - expiresIn: 60 * 5 // 5 minutes - }) - userTokens.refresh.set(refreshToken, accessToken) - userTokens.access.set(accessToken, refreshToken) + // Call the token refresh logic + const tokens = await refreshUserAccessToken(userTokens, refreshToken) return { - token: { - accessToken, - refreshToken - } + token: tokens } }) diff --git a/playground-local/server/api/auth/signup.post.ts b/playground-local/server/api/auth/signup.post.ts index 849f7d3a..92ee6cd8 100644 --- a/playground-local/server/api/auth/signup.post.ts +++ b/playground-local/server/api/auth/signup.post.ts @@ -1,40 +1,24 @@ import { createError, eventHandler, readBody } from 'h3' -import { z } from 'zod' -import { sign } from 'jsonwebtoken' - -export const SECRET = 'dummy' +import { createUserTokens, credentialsSchema, getUser } from '~/server/utils/session' export default eventHandler(async (event) => { - // Define the schema for validating the incoming data - const result = z.object({ - username: z.string(), - password: z.string().min(6) - }).safeParse(await readBody(event)) - - // If validation fails, return an error + const result = credentialsSchema.safeParse(await readBody(event)) if (!result.success) { throw createError({ statusCode: 400, - statusMessage: 'Invalid input, please provide a valid email and a password of at least 6 characters.' + statusMessage: `Invalid input, please provide a valid username, and a password must be 'hunter2' for this demo.` }) } - const { username } = result.data - - const expiresIn = '1h' // token expiry (1 hour) - const user = { username } // Payload for the token, includes the email + // Emulate successful registration + const user = await getUser(result.data.username) - // Sign the JWT with the user payload and secret - const accessToken = sign(user, SECRET, { expiresIn }) + // Create the sign-in tokens + const tokens = await createUserTokens(user) // Return a success response with the email and the token return { - message: 'Signup successful!', - user: { - username - }, - token: { - accessToken - } + user, + token: tokens } }) diff --git a/playground-local/server/api/auth/user.get.ts b/playground-local/server/api/auth/user.get.ts index d2dde1bf..fbe49a3c 100644 --- a/playground-local/server/api/auth/user.get.ts +++ b/playground-local/server/api/auth/user.get.ts @@ -1,6 +1,5 @@ import { createError, eventHandler, getRequestHeader } from 'h3' -import { verify } from 'jsonwebtoken' -import { type JwtPayload, SECRET, extractToken, tokensByUser } from './login.post' +import { type JwtPayload, checkUserAccessToken, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser } from '~/server/utils/session' export default eventHandler((event) => { const authorizationHeader = getRequestHeader(event, 'Authorization') @@ -8,10 +7,15 @@ export default eventHandler((event) => { throw createError({ statusCode: 403, statusMessage: 'Need to pass valid Bearer-authorization header to access this endpoint' }) } - const extractedToken = extractToken(authorizationHeader) + const requestAccessToken = extractTokenFromAuthorizationHeader(authorizationHeader) let decoded: JwtPayload try { - decoded = verify(extractedToken, SECRET) as JwtPayload + const decodeTokenResult = decodeToken(requestAccessToken) + + if (!decodeTokenResult) { + throw new Error('Expected decoded JwtPayload to be non-empty') + } + decoded = decodeTokenResult } catch (error) { console.error({ @@ -21,9 +25,18 @@ export default eventHandler((event) => { throw createError({ statusCode: 403, statusMessage: 'You must be logged in to use this endpoint' }) } + // Get tokens of a user (only for demo, use a DB in your implementation) + const userTokens = getTokensByUser(decoded.username) + if (!userTokens) { + throw createError({ + statusCode: 404, + statusMessage: 'User not found' + }) + } + // Check against known token - const userTokens = tokensByUser.get(decoded.username) - if (!userTokens || !userTokens.access.has(extractedToken)) { + const tokensValidityCheck = checkUserAccessToken(userTokens, requestAccessToken) + if (!tokensValidityCheck.valid) { throw createError({ statusCode: 401, statusMessage: 'Unauthorized, user is not logged in' diff --git a/playground-local/server/utils/session.ts b/playground-local/server/utils/session.ts new file mode 100644 index 00000000..ddfef461 --- /dev/null +++ b/playground-local/server/utils/session.ts @@ -0,0 +1,181 @@ +/* + * DISCLAIMER! + * This is a demo implementation, please create your own handlers + */ + +import { sign, verify } from 'jsonwebtoken' +import { z } from 'zod' + +/** + * This is a demo secret. + * Please ensure that your secret is properly protected. + */ +const SECRET = 'dummy' + +/** 30 seconds */ +const ACCESS_TOKEN_TTL = 30 + +export interface User { + username: string + name: string + picture: string +} + +export interface JwtPayload extends User { + scope: Array<'test' | 'user'> + exp?: number +} + +interface TokensByUser { + access: Map + refresh: Map +} + +/** + * Tokens storage. + * You will need to implement your own, connect with DB/etc. + */ +const tokensByUser: Map = new Map() + +/** + * We use a fixed password for demo purposes. + * You can use any implementation fitting your usecase. + */ +export const credentialsSchema = z.object({ + username: z.string().min(1), + password: z.literal('hunter2') +}) + +/** + * Stub function for creating/getting a user. + * Your implementation can use a DB call or any other method. + */ +export function getUser(username: string): Promise { + // Emulate async work + return Promise.resolve({ + username, + picture: 'https://github.com/nuxt.png', + name: `User ${username}` + }) +} + +interface UserTokens { + accessToken: string + refreshToken: string +} + +/** + * Demo function for signing user tokens. + * Your implementation may differ. + */ +export function createUserTokens(user: User): Promise { + const tokenData: JwtPayload = { ...user, scope: ['test', 'user'] } + const accessToken = sign(tokenData, SECRET, { + expiresIn: ACCESS_TOKEN_TTL + }) + const refreshToken = sign(tokenData, SECRET, { + // 1 day + expiresIn: 60 * 60 * 24 + }) + + // Naive implementation - please implement properly yourself! + const userTokens: TokensByUser = tokensByUser.get(user.username) ?? { + access: new Map(), + refresh: new Map() + } + userTokens.access.set(accessToken, refreshToken) + userTokens.refresh.set(refreshToken, accessToken) + tokensByUser.set(user.username, userTokens) + + // Emulate async work + return Promise.resolve({ + accessToken, + refreshToken + }) +} + +/** + * Function for getting the data from a JWT + */ +export function decodeToken(token: string): JwtPayload | undefined { + return verify(token, SECRET) as JwtPayload | undefined +} + +/** + * Helper only for demo purposes. + * Your implementation will likely never need this and will rely on User ID and DB. + */ +export function getTokensByUser(username: string): TokensByUser | undefined { + return tokensByUser.get(username) +} + +type CheckUserTokensResult = { valid: true, knownAccessToken: string } | { valid: false, knownAccessToken: undefined } + +/** + * Function for checking the validity of the access/refresh token pair. + * Your implementation will probably use the DB call. + * @param tokensByUser A helper for demo purposes + */ +export function checkUserTokens(tokensByUser: TokensByUser, requestAccessToken: string, requestRefreshToken: string): CheckUserTokensResult { + const knownAccessToken = tokensByUser.refresh.get(requestRefreshToken) + + return { + valid: !!knownAccessToken && knownAccessToken === requestAccessToken, + knownAccessToken + } as CheckUserTokensResult +} + +export function checkUserAccessToken(tokensByUser: TokensByUser, requestAccessToken: string): CheckUserTokensResult { + const knownAccessToken = tokensByUser.access.has(requestAccessToken) ? requestAccessToken : undefined + + return { + valid: !!knownAccessToken, + knownAccessToken + } as CheckUserTokensResult +} + +export function invalidateAccessToken(tokensByUser: TokensByUser, accessToken: string) { + tokensByUser.access.delete(accessToken) +} + +export function refreshUserAccessToken(tokensByUser: TokensByUser, refreshToken: string): Promise { + // Get the access token + const oldAccessToken = tokensByUser.refresh.get(refreshToken) + if (!oldAccessToken) { + // Promises to emulate async work (e.g. of a DB call) + return Promise.resolve(undefined) + } + + // Invalidate old access token + invalidateAccessToken(tokensByUser, oldAccessToken) + + // Get the user data. In a real implementation this is likely a DB call. + // In this demo we simply re-use the existing JWT data + const jwtUser = decodeToken(refreshToken) + if (!jwtUser) { + return Promise.resolve(undefined) + } + + const user: User = { + username: jwtUser.username, + picture: jwtUser.picture, + name: jwtUser.name + } + + const accessToken = sign({ ...user, scope: ['test', 'user'] }, SECRET, { + expiresIn: 60 * 5 // 5 minutes + }) + tokensByUser.refresh.set(refreshToken, accessToken) + tokensByUser.access.set(accessToken, refreshToken) + + return Promise.resolve({ + accessToken, + refreshToken + }) +} + +export function extractTokenFromAuthorizationHeader(authorizationHeader: string): string { + return authorizationHeader.startsWith('Bearer ') + ? authorizationHeader.slice(7) + : authorizationHeader +} From 916956fcbcd30e5ceeaf94296596b8b6f37ae356 Mon Sep 17 00:00:00 2001 From: Marsel Shaikhin Date: Thu, 10 Apr 2025 16:33:07 +0200 Subject: [PATCH 14/15] test: fix E2E tests --- playground-local/pages/register.vue | 6 +++--- playground-local/tests/local.spec.ts | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/playground-local/pages/register.vue b/playground-local/pages/register.vue index f23ac214..b5393572 100644 --- a/playground-local/pages/register.vue +++ b/playground-local/pages/register.vue @@ -31,9 +31,9 @@ definePageMeta({

*password should have at least 6 characters

- - -
diff --git a/playground-local/tests/local.spec.ts b/playground-local/tests/local.spec.ts index 33cd9b37..6337b026 100644 --- a/playground-local/tests/local.spec.ts +++ b/playground-local/tests/local.spec.ts @@ -67,10 +67,12 @@ describe('local Provider', async () => { usernameInput, passwordInput, submitButton, + status ] = await Promise.all([ - page.getByTestId('username'), - page.getByTestId('password'), - page.getByTestId('submit') + page.getByTestId('register-username'), + page.getByTestId('register-password'), + page.getByTestId('register-submit'), + page.getByTestId('status') ]) await usernameInput.fill('newuser') @@ -84,5 +86,8 @@ describe('local Provider', async () => { // Expect the response to return signup data const responseBody = await response.json() // Parse response playwrightExpect(responseBody).toBeDefined() // Ensure data is returned + + // Since we use `preventLoginFlow`, status should be unauthenticated + await playwrightExpect(status).toHaveText(STATUS_UNAUTHENTICATED) }) }) From 653f80cf301a93494ea670656690ef8cc1de1cc7 Mon Sep 17 00:00:00 2001 From: Marsel Shaikhin Date: Thu, 10 Apr 2025 16:46:14 +0200 Subject: [PATCH 15/15] chore: remove unused imports --- playground-local/server/api/auth/refresh.post.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/playground-local/server/api/auth/refresh.post.ts b/playground-local/server/api/auth/refresh.post.ts index bc41b531..98603e0a 100644 --- a/playground-local/server/api/auth/refresh.post.ts +++ b/playground-local/server/api/auth/refresh.post.ts @@ -1,6 +1,5 @@ import { createError, eventHandler, getRequestHeader, readBody } from 'h3' -import { sign, verify } from 'jsonwebtoken' -import { checkUserTokens, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser, invalidateAccessToken, refreshUserAccessToken } from '~/server/utils/session' +import { checkUserTokens, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser, refreshUserAccessToken } from '~/server/utils/session' /* * DISCLAIMER!