@devcoffee/nuxt-core 1.1.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,73 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.2.3
4
+
5
+ [compare changes](https://github.com/coolkg1412/devcoffee-nuxt-core/compare/v1.2.2...v1.2.3)
6
+
7
+ ## v1.2.2
8
+
9
+ [compare changes](https://github.com/coolkg1412/devcoffee-nuxt-core/compare/v1.2.1...v1.2.2)
10
+
11
+ ### 🩹 Fixes
12
+
13
+ - Ensure sanitizeError function returns H3Error type consistently ([de54a04](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/de54a04))
14
+
15
+ ### 🏡 Chore
16
+
17
+ - Add publishConfig access public for scoped npm package ([3c080be](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/3c080be))
18
+
19
+ ### ❤️ Contributors
20
+
21
+ - Hieu Nguyen <hieu.nguyen@devcoffee.tech>
22
+
23
+ ## v1.2.1
24
+
25
+ [compare changes](https://github.com/coolkg1412/devcoffee-nuxt-core/compare/v1.2.0...v1.2.1)
26
+
27
+ ### 🩹 Fixes
28
+
29
+ - Resolve session double encryption issue by decrypting tokenSet in validateSession and updating getSession usage in updateSession ([6ed8fb6](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/6ed8fb6))
30
+ - Populate nodeReferences and sharedReferences in prepare:types hook ([83b41e2](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/83b41e2))
31
+ - Include devcoffee-nitro-core.d.ts in server TypeScript project via nitro:config hook ([20b35e0](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/20b35e0))
32
+ - **typecheck:** Exclude test, playground, and eslint config from tsconfig.app.json ([9d9c857](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/9d9c857))
33
+
34
+ ### ❤️ Contributors
35
+
36
+ - Hieu Nguyen <hieu.nguyen@devcoffee.tech>
37
+
38
+ ## v1.2.0
39
+
40
+ [compare changes](https://github.com/coolkg1412/devcoffee-nuxt-core/compare/v1.1.1...v1.2.0)
41
+
42
+ ### 🚀 Enhancements
43
+
44
+ - **quick-260328-uf7:** Create admin-board test fixture ([45dae18](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/45dae18))
45
+ - **quick-260328-uf7:** Add admin-board OIDC e2e test ([20bf504](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/20bf504))
46
+
47
+ ### 🩹 Fixes
48
+
49
+ - **types:** Explicitly type module export as NuxtModule for better type inference ([33e140e](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/33e140e))
50
+ - **types:** Resolve TypeScript errors in middleware and plugins ([df23475](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/df23475))
51
+ - Resolve #devcoffee-core subpath import warnings in build ([#19](https://github.com/coolkg1412/devcoffee-nuxt-core/pull/19))
52
+ - Eliminate duplicate Redis sessions on SSR first load and refactor event.context.session ([#20](https://github.com/coolkg1412/devcoffee-nuxt-core/pull/20))
53
+ - Rotate session on logout and remove console.log debug statement ([fbe129f](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/fbe129f))
54
+ - Resolve autoFetchUser SSR divergence on first render ([1f15ac7](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/1f15ac7))
55
+ - **lint:** Remove unused destructure aliases in SSR session mapping ([cfe7884](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/cfe7884))
56
+
57
+ ### 📖 Documentation
58
+
59
+ - **quick:** Create plan for admin-board OIDC e2e test ([ce8c611](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/ce8c611))
60
+ - **quick-260328-uf7:** Complete admin-board OIDC e2e plan ([23c0945](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/23c0945))
61
+
62
+ ### ✅ Tests
63
+
64
+ - Update deleteSession tests to reflect renewSession LOGOUT behavior ([b444baa](https://github.com/coolkg1412/devcoffee-nuxt-core/commit/b444baa))
65
+
66
+ ### ❤️ Contributors
67
+
68
+ - Hieu Nguyen <hieu.nguyen@devcoffee.tech>
69
+ - Hiếu Nguyễn ([@coolkg1412](https://github.com/coolkg1412))
70
+
3
71
  ## v1.1.1
4
72
 
5
73
  [compare changes](https://github.com/coolkg1412/devcoffee-nuxt-core/compare/v1.0...v1.1.1)
package/dist/module.d.mts CHANGED
@@ -1,6 +1,7 @@
1
- import { CookieSerializeOptions } from '#devcoffee-core/runtime/server/adapters/http';
2
- import { OidcUserInfo } from '#devcoffee-core/runtime/server/adapters/oidc';
3
- import { ConsolaInstance, LogLevel as LogLevel$1, ConsolaOptions } from 'consola';
1
+ import { NuxtModule } from '@nuxt/schema';
2
+ import { CookieSerializeOptions } from '../dist/runtime/server/adapters/http.js';
3
+ import { OidcUserInfo } from '../dist/runtime/server/adapters/oidc.js';
4
+ import { LogLevel as LogLevel$1, ConsolaOptions, ConsolaInstance } from 'consola';
4
5
 
5
6
  interface AuthorizedUser {
6
7
  id: string
@@ -294,7 +295,7 @@ type ModulePublicRuntimeConfig = Pick<ModuleOptions, 'defaultLocale' | 'defaultT
294
295
 
295
296
  type InputModuleOptions = DeepPartial<ModuleOptions>
296
297
 
297
- declare const _default: any;
298
+ declare const _module: NuxtModule<InputModuleOptions>;
298
299
 
299
- export { _default as default };
300
+ export { _module as default };
300
301
  export type { AuthData, AuthorizedUser, AuthtsMiddlewareMeta, AuthtsModuleOptions, CoreLogInstance, CoreLogLevel, InputModuleOptions, LoggingModuleOptions, LoggingOptions, ModuleOptions, ModulePublicRuntimeConfig, NuxtAuthOptions, NuxtCoreLogging, NuxtSessionContext, NuxtSessionUpdateContext, SessionContext };
package/dist/module.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { CookieSerializeOptions } from '#devcoffee-core/runtime/server/adapters/http';
2
- import { OidcUserInfo } from '#devcoffee-core/runtime/server/adapters/oidc';
3
- import { ConsolaInstance, LogLevel as LogLevel$1, ConsolaOptions } from 'consola';
1
+ import { NuxtModule } from '@nuxt/schema';
2
+ import { CookieSerializeOptions } from '../dist/runtime/server/adapters/http.js';
3
+ import { OidcUserInfo } from '../dist/runtime/server/adapters/oidc.js';
4
+ import { LogLevel as LogLevel$1, ConsolaOptions, ConsolaInstance } from 'consola';
4
5
 
5
6
  interface AuthorizedUser {
6
7
  id: string
@@ -294,7 +295,7 @@ type ModulePublicRuntimeConfig = Pick<ModuleOptions, 'defaultLocale' | 'defaultT
294
295
 
295
296
  type InputModuleOptions = DeepPartial<ModuleOptions>
296
297
 
297
- declare const _default: any;
298
+ declare const _module: NuxtModule<InputModuleOptions>;
298
299
 
299
- export { _default as default };
300
+ export { _module as default };
300
301
  export type { AuthData, AuthorizedUser, AuthtsMiddlewareMeta, AuthtsModuleOptions, CoreLogInstance, CoreLogLevel, InputModuleOptions, LoggingModuleOptions, LoggingOptions, ModuleOptions, ModulePublicRuntimeConfig, NuxtAuthOptions, NuxtCoreLogging, NuxtSessionContext, NuxtSessionUpdateContext, SessionContext };
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-core",
3
- "version": "1.1.1",
3
+ "version": "1.2.3",
4
4
  "configKey": "nuxtCore",
5
5
  "compatibility": {
6
6
  "nuxt": "^4.0.0"
package/dist/module.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { addCustomTab } from '@nuxt/devtools-kit';
2
- import { defineNuxtModule, useLogger, createResolver, addServerImports, addServerImportsDir, addServerPlugin, addImportsDir, addPlugin, addRouteMiddleware, addTemplate, addServerHandler } from '@nuxt/kit';
2
+ import { defineNuxtModule, useLogger, createResolver, addTemplate, addServerImports, addServerImportsDir, addServerPlugin, addImportsDir, addPlugin, addRouteMiddleware, addServerHandler } from '@nuxt/kit';
3
3
  import { deepMerge, pick } from '../dist/runtime/utils.js';
4
4
 
5
- const version = "1.1.1";
5
+ const version = "1.2.3";
6
6
 
7
7
  const defaultLocale = "vi-VN";
8
8
  const defaultLanguage = "vi";
@@ -113,7 +113,7 @@ function normalizePublicRuntimeConfig(inputOpts) {
113
113
 
114
114
  const moduleName = "nuxt-core";
115
115
  const configKey = "nuxtCore";
116
- const module = defineNuxtModule({
116
+ const _module = defineNuxtModule({
117
117
  meta: {
118
118
  name: moduleName,
119
119
  version,
@@ -140,6 +140,25 @@ const module = defineNuxtModule({
140
140
  }
141
141
  });
142
142
  _nuxt.options.alias["#devcoffee-core"] = resolver.resolve("./runtime");
143
+ const globalTypes = addTemplate({
144
+ filename: "types/devcoffee-global.d.ts",
145
+ src: resolver.resolve("./runtime/types/global.env.d.ts")
146
+ });
147
+ const nuxtTypes = addTemplate({
148
+ filename: "types/devcoffee-nuxt-core.d.ts",
149
+ src: resolver.resolve("./runtime/types/nuxt.d.ts")
150
+ });
151
+ const nitroTypes = addTemplate({
152
+ filename: "types/devcoffee-nitro-core.d.ts",
153
+ src: resolver.resolve("./runtime/types/nitro.d.ts")
154
+ });
155
+ _nuxt.options.nitro = deepMerge(_nuxt.options.nitro || {}, {
156
+ typescript: {
157
+ tsConfig: {
158
+ include: [globalTypes.dst]
159
+ }
160
+ }
161
+ });
143
162
  addServerImports([
144
163
  {
145
164
  from: resolver.resolve("./runtime/server/core/nuxtAuthtsHandler"),
@@ -185,29 +204,20 @@ const module = defineNuxtModule({
185
204
  addRouteMiddleware([{ name: "authts", path: resolver.resolve("./runtime/app/middleware/authts"), global: true }], {
186
205
  prepend: true
187
206
  });
188
- const globalTypes = addTemplate({
189
- filename: "types/devcoffee-global.d.ts",
190
- src: resolver.resolve("./runtime/types/global.env.d.ts")
191
- });
192
- _nuxt.options.nitro = deepMerge(_nuxt.options.nitro || {}, {
193
- typescript: {
194
- tsConfig: {
195
- include: [globalTypes.dst]
196
- }
197
- }
198
- });
199
- const nuxtTypes = addTemplate({
200
- filename: "types/devcoffee-nuxt-core.d.ts",
201
- src: resolver.resolve("./runtime/types/nuxt.d.ts")
202
- });
203
- const nitroTypes = addTemplate({
204
- filename: "types/devcoffee-nitro-core.d.ts",
205
- src: resolver.resolve("./runtime/types/nitro.d.ts")
206
- });
207
207
  _nuxt.hook("prepare:types", (opts) => {
208
208
  opts.references.push({ path: globalTypes.dst });
209
209
  opts.references.push({ path: nuxtTypes.dst });
210
210
  opts.references.push({ path: nitroTypes.dst });
211
+ opts.nodeReferences.push({ path: globalTypes.dst });
212
+ opts.nodeReferences.push({ path: nitroTypes.dst });
213
+ opts.sharedReferences.push({ path: globalTypes.dst });
214
+ opts.sharedReferences.push({ path: globalTypes.dst });
215
+ });
216
+ _nuxt.hook("nitro:config", (nitroConfig) => {
217
+ nitroConfig.typescript ??= {};
218
+ nitroConfig.typescript.tsConfig ??= {};
219
+ nitroConfig.typescript.tsConfig.include ??= [];
220
+ nitroConfig.typescript.tsConfig.include.push(nitroTypes.dst);
211
221
  });
212
222
  if (_nuxt.options?.devtools) {
213
223
  addCustomTab({
@@ -228,4 +238,4 @@ const module = defineNuxtModule({
228
238
  }
229
239
  });
230
240
 
231
- export { module as default };
241
+ export { _module as default };
@@ -1,4 +1,4 @@
1
- import { useRequestURL } from "#app";
1
+ import { useRequestEvent, useRequestURL, useRuntimeConfig } from "#app";
2
2
  import {
3
3
  computed,
4
4
  createError,
@@ -9,6 +9,7 @@ import {
9
9
  useRequestFetch,
10
10
  useSessionContext
11
11
  } from "#imports";
12
+ import { deleteCookie, setCookie } from "h3";
12
13
  export function useAuthContext(initiator) {
13
14
  const { callHook, runWithContext } = useNuxtApp();
14
15
  const { getValue } = useSessionContext();
@@ -58,7 +59,15 @@ export function useAuthContext(initiator) {
58
59
  processing.value = true;
59
60
  }
60
61
  }).then(
61
- async ({ redirectUrl }) => runWithContext(async () => await navigateTo(redirectUrl, { external: true, replace: true }))
62
+ async ({ redirectUrl, cookies }) => runWithContext(async () => {
63
+ if (import.meta.server) {
64
+ const event = useRequestEvent();
65
+ if (event) {
66
+ cookies.forEach(([n, v, o]) => setCookie(event, n, v, o));
67
+ }
68
+ }
69
+ return await navigateTo(redirectUrl, { external: true, replace: true });
70
+ })
62
71
  ).catch((ex) => {
63
72
  logger.log(`[login] failed error:${ex}`, ex);
64
73
  throw sanitizeError(ex);
@@ -76,7 +85,18 @@ export function useAuthContext(initiator) {
76
85
  await runWithContext(async () => await callHook("user:loggedIn"));
77
86
  return response;
78
87
  }).then(
79
- async ({ redirectUrl }) => runWithContext(async () => await navigateTo(redirectUrl))
88
+ async ({ redirectUrl }) => runWithContext(async () => {
89
+ if (import.meta.server) {
90
+ const event = useRequestEvent();
91
+ if (event) {
92
+ const { cookieOpts, names: cookienames } = useRuntimeConfig(event).nuxtCore.authts.sessions;
93
+ Array.of(cookienames.state, cookienames.pkce, cookienames.redirectUrl).forEach(
94
+ (cookie) => deleteCookie(event, cookie, cookieOpts)
95
+ );
96
+ }
97
+ }
98
+ return await navigateTo(redirectUrl);
99
+ })
80
100
  ).catch((ex) => {
81
101
  logger.log(`[authorize] failed error:${ex}`, ex);
82
102
  throw sanitizeError(ex);
@@ -90,7 +110,16 @@ export function useAuthContext(initiator) {
90
110
  processing.value = true;
91
111
  }
92
112
  }).then(async (response) => {
93
- await runWithContext(async () => await callHook("user:loggedOut"));
113
+ await runWithContext(async () => {
114
+ if (import.meta.server) {
115
+ const event = useRequestEvent();
116
+ if (event) {
117
+ const { cookieOpts, names: cookienames } = useRuntimeConfig(event).nuxtCore.authts.sessions;
118
+ deleteCookie(event, cookienames.sessionId, cookieOpts);
119
+ }
120
+ }
121
+ return await callHook("user:loggedOut");
122
+ });
94
123
  return response;
95
124
  }).then(
96
125
  async ({ redirectUrl }) => runWithContext(async () => await navigateTo(redirectUrl))
@@ -3,7 +3,6 @@ import {
3
3
  createError,
4
4
  defineNuxtRouteMiddleware,
5
5
  navigateTo,
6
- refreshNuxtData,
7
6
  useCookie,
8
7
  useNuxtApp,
9
8
  useRequestEvent,
@@ -68,7 +67,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
68
67
  logger.debug(`Bypassing auth checks for ignored path: ${normalizedPath}`);
69
68
  return;
70
69
  }
71
- if (import.meta.env.DEV && ignoreRegexPatternsDev.length && ignoreRegexPatternsDev.some((pattern) => pattern.test(normalizedPath))) {
70
+ if (import.meta.dev && ignoreRegexPatternsDev.length && ignoreRegexPatternsDev.some((pattern) => pattern.test(normalizedPath))) {
72
71
  logger.debug(`Bypassing auth checks for ignored path (dev): ${normalizedPath}`);
73
72
  return;
74
73
  }
@@ -83,7 +82,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
83
82
  );
84
83
  }
85
84
  if (import.meta.client) {
86
- await refreshNuxtData("authts:session");
85
+ await nuxtApp.$sessionContext.refetch();
87
86
  }
88
87
  const { isAuthenticated } = useAuthContext("core.app.authts.middleware");
89
88
  const loginPath = loginUri.toLowerCase();
@@ -2,10 +2,10 @@ import { watch } from "vue";
2
2
  import {
3
3
  createError,
4
4
  defineNuxtPlugin,
5
- refreshNuxtData,
6
5
  useAsyncData,
7
6
  useCookie,
8
7
  useNuxtApp,
8
+ useRequestEvent,
9
9
  useRequestFetch,
10
10
  useRuntimeConfig,
11
11
  useState
@@ -21,8 +21,25 @@ export default defineNuxtPlugin(async (_nuxtApp) => {
21
21
  }
22
22
  );
23
23
  async function __getServerSession(initiator) {
24
+ logger.debug(`[__getServerSession] Called with initiator: '${initiator}'`);
25
+ if (import.meta.server) {
26
+ const event = useRequestEvent();
27
+ const session = event?.context?.session ?? null;
28
+ if (!session) {
29
+ throw createError({
30
+ status: 500,
31
+ fatal: true,
32
+ statusMessage: "Failed to fetch session",
33
+ message: "Server session not found on event context"
34
+ });
35
+ }
36
+ const { auth, ...rest } = session;
37
+ return {
38
+ ...rest,
39
+ isAuthenticated: auth?.status === "authenticated" && Boolean(auth?.tokenSet && session.user?.id)
40
+ };
41
+ }
24
42
  try {
25
- logger.debug(`[__getServerSession] Called with initiator: '${initiator}'`);
26
43
  return await fetchRequest("/api/_auth/session");
27
44
  } catch (ex) {
28
45
  throw createError({
@@ -41,36 +58,17 @@ export default defineNuxtPlugin(async (_nuxtApp) => {
41
58
  _nuxtApp.provide("sessionReady", sessionReady);
42
59
  const sessionId = useCookie(useRuntimeConfig().public.nuxtCore.authts.sessionCookie);
43
60
  logger.debug(`Initial session fetch on plugin init with sessionId: ${sessionId.value}`);
44
- if (sessionId.value) {
45
- const { data } = await useAsyncData("authts:session", () => __getServerSession("plugin initialization"));
46
- if (data.value) {
47
- context.value = data.value;
48
- }
49
- watch(data, (newData) => {
61
+ const { data, refresh } = await useAsyncData("authts:session", () => __getServerSession("plugin initialization"));
62
+ watch(
63
+ data,
64
+ (newData) => {
50
65
  if (newData) context.value = newData;
51
- });
52
- } else {
53
- context.value = {
54
- id: "",
55
- isAuthenticated: false,
56
- user: {
57
- id: "",
58
- sub: "",
59
- email: "",
60
- firstName: "Anonymous",
61
- lastName: "User",
62
- locale: "",
63
- language: "",
64
- timezone: ""
65
- },
66
- data: {}
67
- };
68
- }
69
- resolveReady();
66
+ },
67
+ { immediate: true }
68
+ );
70
69
  _nuxtApp.hooks.addHooks({
71
- // session:fetch calls refreshNuxtData to stay coherent with useAsyncData cache (D-04)
72
70
  "session:fetch": async (_initiator) => {
73
- await refreshNuxtData("authts:session");
71
+ await refresh({ dedupe: "cancel" });
74
72
  },
75
73
  "user:loggedIn": async () => {
76
74
  await _nuxtApp.callHook("session:fetch", "user:loggedIn");
@@ -88,4 +86,5 @@ export default defineNuxtPlugin(async (_nuxtApp) => {
88
86
  await _nuxtApp.callHook("session:fetch");
89
87
  }
90
88
  _nuxtApp.provide("sessionContext", { getValue, refetch });
89
+ resolveReady();
91
90
  });
@@ -10,8 +10,8 @@ type SessionCreateOptions = {
10
10
  /**
11
11
  * Check whether a candidate redirect URL is same-origin with the request URL.
12
12
  *
13
- * Uses the WHATWG URL constructor — malformed URLs and relative paths return false
14
- * instead of throwing, guarding against attacker-controlled input.
13
+ * Uses the WHATWG URL constructor — relative paths are considered same-origin.
14
+ * Malformed URLs return false, guarding against attacker-controlled input.
15
15
  *
16
16
  * @param redirectUrl - The candidate redirect URL string (from query param or cookie).
17
17
  * @param requestUrl - The trusted request URL from the h3 event.
@@ -47,8 +47,11 @@ function getSessionStorageKey(storagePrefix, sessionId) {
47
47
  return storagePrefix ? `${storagePrefix}:${sessionId}` : sessionId;
48
48
  }
49
49
  export function isSameOrigin(redirectUrl, requestUrl) {
50
+ if (redirectUrl.includes(" ")) {
51
+ return false;
52
+ }
50
53
  try {
51
- return new URL(redirectUrl).origin === requestUrl.origin;
54
+ return new URL(redirectUrl, requestUrl.href).origin === requestUrl.origin;
52
55
  } catch {
53
56
  return false;
54
57
  }
@@ -112,6 +115,14 @@ export async function validateSession(sessionCookieId, opts) {
112
115
  await removeSessionData(opts.storageName, deleteSessionKey);
113
116
  }
114
117
  await setSessionData(opts.storageName, sessionKey, session, opts.expiresIn / 1e3 | 0);
118
+ if (session.auth?.tokenSet && session.auth.tokenSet.encrypted === true) {
119
+ if (opts.secret) {
120
+ const decrypted = decryptTokenSet(session.auth.tokenSet, opts.secret);
121
+ session.auth.tokenSet = decrypted ?? void 0;
122
+ } else {
123
+ session.auth.tokenSet = void 0;
124
+ }
125
+ }
115
126
  return session;
116
127
  }
117
128
  export async function updateSession(sessionId, input, opts) {
@@ -121,7 +132,7 @@ export async function updateSession(sessionId, input, opts) {
121
132
  input,
122
133
  ["id", "issuedAt", "expiresAt"]
123
134
  );
124
- let session = await getSessionData(opts.storageName, serverKey);
135
+ let session = await getSession(sessionId, opts);
125
136
  if (!session) {
126
137
  throw createError({
127
138
  status: 500,
@@ -1,4 +1,10 @@
1
1
  import type { NuxtAuthOptions } from '#devcoffee-core/server/adapters/http';
2
+ declare module 'nitropack' {
3
+ interface NitroApp {
4
+ /** Registered userInfo callback from NuxtAuthtsHandler — used by server plugin for SSR autoFetchUser enrichment. */
5
+ _sessionUserInfo?: NuxtAuthOptions['userInfo'];
6
+ }
7
+ }
2
8
  /**
3
9
  * Creates a universal authentication handler for Nuxt, integrating with OpenID Connect.
4
10
  *
@@ -10,15 +10,14 @@ import {
10
10
  useRuntimeConfig
11
11
  } from "#devcoffee-core/server/adapters/http";
12
12
  import { deepMerge, omit } from "#devcoffee-core/server/adapters/utils";
13
- import { useStorage } from "nitropack/runtime";
13
+ import { useNitroApp, useStorage } from "nitropack/runtime";
14
14
  import {
15
15
  authorizationCodeGrant,
16
16
  buildAuthorizationUrl,
17
17
  constructTokenSet,
18
- deleteSession,
19
18
  fetchUserInfo,
20
- getSession,
21
19
  isSameOrigin,
20
+ renewSession,
22
21
  revokeTokens,
23
22
  updateSession
24
23
  } from "./helpers.js";
@@ -67,20 +66,17 @@ const defaultNuxtAuthOptions = {
67
66
  export default function NuxtAuthtsHandler(options) {
68
67
  const { enabled: authEnabled, sessions: sessionConfig, openid, auth } = useRuntimeConfig().nuxtCore.authts;
69
68
  const nuxtAuthOptions = deepMerge({ ...defaultNuxtAuthOptions }, options || {});
69
+ useNitroApp()._sessionUserInfo = nuxtAuthOptions.userInfo;
70
70
  return eventHandler(async (event) => {
71
71
  const requestUrl = getRequestURL(event);
72
72
  const queryParams = getQuery(event);
73
73
  const authAction = getAuthAction(requestUrl);
74
- let session = await getSession(event.context.sessionId, {
75
- storageName: sessionConfig.storage.name,
76
- storagePrefix: sessionConfig.storage.prefix,
77
- secret: sessionConfig.secret || ""
78
- });
74
+ let session = event.context.session;
79
75
  if (!session) {
80
76
  throw createError({
81
77
  status: 500,
82
78
  fatal: true,
83
- message: `session '${event.context.sessionId}' was not found!`
79
+ message: `session was not found on event context!`
84
80
  });
85
81
  }
86
82
  if (!authEnabled && !["session" /* GET_SESSION */].includes(authAction)) {
@@ -114,7 +110,7 @@ export default function NuxtAuthtsHandler(options) {
114
110
  secret: sessionConfig.secret || ""
115
111
  });
116
112
  }
117
- event.context.sessionId = session.id;
113
+ event.context.session = session;
118
114
  return nuxtAuthOptions.session(omit(session, ["auth"]), session.auth);
119
115
  case "authorize-url" /* AUTHORIZE_URL */:
120
116
  const { authorizeUrl, state, pkceCodeVerifier } = await buildAuthorizationUrl(session, {
@@ -131,8 +127,10 @@ export default function NuxtAuthtsHandler(options) {
131
127
  sessionStorageName: sessionConfig.storage.name,
132
128
  sessionStoragePrefix: sessionConfig.storage.prefix
133
129
  });
130
+ const result = { redirectUrl: authorizeUrl, cookies: [] };
134
131
  if (pkceCodeVerifier) {
135
132
  setCookie(event, sessionConfig.names.pkce, pkceCodeVerifier, authCookieOpts);
133
+ result.cookies.push([sessionConfig.names.pkce, pkceCodeVerifier, authCookieOpts]);
136
134
  }
137
135
  if (queryParams.redirectUrl) {
138
136
  const candidateUrl = String(queryParams.redirectUrl);
@@ -144,10 +142,12 @@ export default function NuxtAuthtsHandler(options) {
144
142
  });
145
143
  }
146
144
  setCookie(event, sessionConfig.names.redirectUrl, candidateUrl, authCookieOpts);
145
+ result.cookies.push([sessionConfig.names.redirectUrl, candidateUrl, authCookieOpts]);
147
146
  }
148
147
  setCookie(event, sessionConfig.names.state, state, authCookieOpts);
149
- event.context.sessionId = session.id;
150
- return { redirectUrl: authorizeUrl };
148
+ result.cookies.push([sessionConfig.names.state, state, authCookieOpts]);
149
+ event.context.session = session;
150
+ return result;
151
151
  case "token" /* TOKEN */:
152
152
  const formData = await readFormData(event);
153
153
  const openIdTokenSet = await authorizationCodeGrant(
@@ -198,13 +198,13 @@ export default function NuxtAuthtsHandler(options) {
198
198
  });
199
199
  sessionData.user = await nuxtAuthOptions.userInfo(session.user, { openidUser, tokenSet });
200
200
  }
201
- await updateSession(session.id, sessionData, {
201
+ session = await updateSession(session.id, sessionData, {
202
202
  storageName: sessionConfig.storage.name,
203
203
  storagePrefix: sessionConfig.storage.prefix,
204
204
  expiresIn: sessionConfig.expiresIn,
205
205
  secret: sessionConfig.secret || ""
206
206
  });
207
- event.context.sessionId = session.id;
207
+ event.context.session = session;
208
208
  return { redirectUrl };
209
209
  case "logout" /* LOGOUT */:
210
210
  redirectUrl = auth.defaultLogoutRedirectUri;
@@ -217,14 +217,15 @@ export default function NuxtAuthtsHandler(options) {
217
217
  wellKnownUrl: openid.wellKnownUrl
218
218
  });
219
219
  }
220
- await deleteSession(session.id, {
220
+ const newSession = await renewSession(session.id, {
221
221
  storageName: sessionConfig.storage.name,
222
- storagePrefix: sessionConfig.storage.prefix
222
+ storagePrefix: sessionConfig.storage.prefix,
223
+ expiresIn: sessionConfig.expiresIn,
224
+ secret: sessionConfig.secret || ""
223
225
  });
224
226
  const cacheStorage = useStorage("cache");
225
227
  await cacheStorage.removeItem(`${openid.cache.prefix}:userinfo:${session.id}`);
226
- deleteCookie(event, sessionConfig.names.sessionId, authCookieOpts);
227
- event.context.sessionId = "";
228
+ event.context.session = newSession;
228
229
  return { redirectUrl };
229
230
  default:
230
231
  break;
@@ -7,7 +7,7 @@ import {
7
7
  } from "#devcoffee-core/server/adapters/http";
8
8
  import { deepMerge } from "#devcoffee-core/server/adapters/utils";
9
9
  import useServerLogger from "#devcoffee-core/server/composables/useServerLogger";
10
- import { getOpenIdConfiguration, getSession } from "./helpers.js";
10
+ import { getOpenIdConfiguration } from "./helpers.js";
11
11
  const defaultOpts = {
12
12
  logLevel: 2,
13
13
  proxyPrefix: ""
@@ -34,15 +34,11 @@ export default function NuxtForwardRequestHandler(opts) {
34
34
  });
35
35
  }
36
36
  const {
37
- openid: { wellKnownUrl, cache, clientId, clientSecret },
38
- sessions: {
39
- secret = "",
40
- storage: { name: storageName, prefix: storagePrefix }
41
- }
37
+ openid: { wellKnownUrl, cache, clientId, clientSecret }
42
38
  } = useRuntimeConfig().nuxtCore.authts;
43
39
  return eventHandler(async (event) => {
44
40
  const oidConfig = await getOpenIdConfiguration(wellKnownUrl, { cache, clientId, clientSecret });
45
- const session = await getSession(event.context.sessionId, { storageName, storagePrefix, secret });
41
+ const session = event.context.session;
46
42
  const headers = getRequestHeaders(event);
47
43
  if (session?.auth?.status === "authenticated" && session.auth.tokenSet?.accessToken) {
48
44
  headers.Authorization = `${session.auth.tokenSet.tokenType} ${session.auth.tokenSet?.accessToken}`;
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, any>;
2
2
  export default _default;
@@ -1,8 +1,4 @@
1
1
  import { defineEventHandler } from "h3";
2
- import { useRuntimeConfig } from "nitropack/runtime";
3
- import { getSession } from "../../core/helpers.js";
4
- export default defineEventHandler(async (event) => {
5
- const { name: storageName, prefix: storagePrefix } = useRuntimeConfig(event).nuxtCore.authts.sessions.storage;
6
- const { secret = "" } = useRuntimeConfig(event).nuxtCore.authts.sessions;
7
- return await getSession(event.context.sessionId, { storageName, storagePrefix, secret });
2
+ export default defineEventHandler((event) => {
3
+ return event.context.session;
8
4
  });
@@ -2,8 +2,8 @@
2
2
  * 🔐 Nitro plugin for session validation and cookie management.
3
3
  *
4
4
  * This plugin automatically validates the session ID from the incoming request cookie.
5
- * If the session is invalid or expired, it will create or refresh the session ID and
6
- * reassign it back to the request context and cookie.
5
+ * If the session is invalid or expired, it will create or refresh the session and
6
+ * store the full session object on event.context.session for downstream handlers.
7
7
  *
8
8
  * @since 1.0.0
9
9
  */
@@ -1,11 +1,21 @@
1
1
  import { defineNitroPlugin, getCookie, setCookie, useRuntimeConfig } from "#devcoffee-core/server/adapters/http";
2
2
  import { signSessionId } from "#devcoffee-core/server/core/crypto";
3
- import { getSession, refreshTokenIfNeeded, updateSession, validateSession } from "#devcoffee-core/server/core/helpers";
3
+ import { refreshTokenIfNeeded, updateSession, validateSession } from "#devcoffee-core/server/core/helpers";
4
+ import { useNitroApp, useStorage } from "nitropack/runtime";
4
5
  export default defineNitroPlugin((nitroApp) => {
5
6
  nitroApp.hooks.hook("request", async (event) => {
6
7
  const {
7
8
  enabled: authtsEnabled,
8
- openid: { wellKnownUrl, cache, clientId, clientSecret, tokenRefreshBufferMs, distributedLock },
9
+ openid: {
10
+ wellKnownUrl,
11
+ cache,
12
+ clientId,
13
+ clientSecret,
14
+ tokenRefreshBufferMs,
15
+ distributedLock,
16
+ autoFetchUser,
17
+ autoFetchUserTtl
18
+ },
9
19
  sessions: {
10
20
  expiresIn,
11
21
  secret = "",
@@ -13,7 +23,7 @@ export default defineNitroPlugin((nitroApp) => {
13
23
  names: { sessionId: cookieName }
14
24
  }
15
25
  } = useRuntimeConfig(event).nuxtCore.authts;
16
- const session = await validateSession(getCookie(event, cookieName), {
26
+ let session = await validateSession(getCookie(event, cookieName), {
17
27
  storageName,
18
28
  storagePrefix,
19
29
  expiresIn,
@@ -30,22 +40,35 @@ export default defineNitroPlugin((nitroApp) => {
30
40
  distributedLock
31
41
  });
32
42
  if (Object.keys(sessionUpdate).length > 0) {
33
- await updateSession(session.id, sessionUpdate, { storageName, storagePrefix, expiresIn, secret });
43
+ session = await updateSession(session.id, sessionUpdate, { storageName, storagePrefix, expiresIn, secret });
44
+ }
45
+ if (autoFetchUser) {
46
+ const cacheStorage = useStorage("cache");
47
+ const userInfoCacheKey = `${cache.prefix}:userinfo:${session.id}`;
48
+ let cachedUser = await cacheStorage.getItem(userInfoCacheKey);
49
+ if (!cachedUser) {
50
+ const userInfoFn = useNitroApp()._sessionUserInfo;
51
+ if (userInfoFn && session.auth.tokenSet) {
52
+ cachedUser = await userInfoFn(session.user, { tokenSet: session.auth.tokenSet });
53
+ await cacheStorage.setItem(userInfoCacheKey, cachedUser, { ttl: autoFetchUserTtl });
54
+ }
55
+ }
56
+ if (cachedUser) {
57
+ session = { ...session, user: cachedUser };
58
+ }
34
59
  }
35
60
  }
36
- event.context.sessionId = session.id;
61
+ event.context.session = session;
37
62
  });
38
63
  nitroApp.hooks.hook("beforeResponse", async (event) => {
39
64
  const {
40
65
  cookieOpts,
41
66
  secret = "",
42
- storage: { name: storageName, prefix: storagePrefix },
43
67
  names: { sessionId: cookieName }
44
68
  } = useRuntimeConfig(event).nuxtCore.authts.sessions;
45
- const session = await getSession(event.context.sessionId, { storageName, storagePrefix, secret });
69
+ const session = event.context.session;
46
70
  if (session) {
47
71
  const cookieValue = secret ? signSessionId(session.id, secret) : session.id;
48
- event.context.sessionId = session.id;
49
72
  setCookie(event, cookieName, cookieValue, {
50
73
  ...cookieOpts,
51
74
  expires: new Date(session.expiresAt)
@@ -1,9 +1,9 @@
1
- import type { CoreLogInstance, CoreLogLevel, NuxtAuthOptions } from '@devcoffee/nuxt-core'
1
+ import type { CoreLogInstance, CoreLogLevel, NuxtAuthOptions, SessionContext } from '@devcoffee/nuxt-core'
2
2
 
3
3
  declare module 'h3' {
4
4
  interface H3EventContext {
5
5
  logger: CoreLogInstance
6
- sessionId: string
6
+ session: SessionContext | null
7
7
  }
8
8
  }
9
9
 
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@devcoffee/nuxt-core",
3
- "version": "1.1.1",
3
+ "version": "1.2.3",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
4
7
  "author": "Hieu Nguyen <hieunguyen@devcoffee.tech>",
5
8
  "description": "Nuxt 4 module providing OpenID Connect / OAuth 2.0 authorization code grant with PKCE, server-side session management via Nitro, client-side auth state composables, and universal route protection middleware.",
6
9
  "keywords": [
@@ -51,13 +54,15 @@
51
54
  "dev:build": "nuxi build playground",
52
55
  "prepack": "nuxt-module-build build",
53
56
  "release": "npm run lint && npm run test:all && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
54
- "lint": "eslint --fix src test",
57
+ "lint": "eslint",
58
+ "lint:fix": "eslint --fix",
55
59
  "test": "cross-env NODE_OPTIONS=--no-deprecation vitest run test/unit",
56
60
  "test:e2e": "cross-env NODE_OPTIONS=--no-deprecation vitest run test/e2e",
57
61
  "test:e2e:ui": "cross-env NODE_OPTIONS=--no-deprecation PLAYWRIGHT_HEADLESS=false vitest run test/e2e",
58
62
  "test:all": "cross-env NODE_OPTIONS=--no-deprecation vitest run",
59
63
  "test:watch": "cross-env NODE_OPTIONS=--no-deprecation vitest watch test/unit",
60
- "test:types": "vue-tsc --noEmit src --skipLibCheck --strict"
64
+ "test:types": "vue-tsc --noEmit",
65
+ "typecheck": "vue-tsc --noEmit -p .nuxt/tsconfig.app.json"
61
66
  },
62
67
  "dependencies": {
63
68
  "@nuxt/kit": "^4.1.3",
@@ -1,3 +0,0 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
- declare const _default: typeof __VLS_export;
3
- export default _default;
@@ -1,32 +0,0 @@
1
- <script setup>
2
- import { computed, onMounted } from "vue";
3
- import { createError, useRoute } from "#app";
4
- import { useAuthContext } from "#imports";
5
- const { query } = useRoute();
6
- const { authorize } = useAuthContext("core.app.pages.authorize");
7
- const searchParms = computed(() => {
8
- const params = new URLSearchParams();
9
- if (query.code) {
10
- params.set("code", query.code);
11
- }
12
- if (query.state) {
13
- params.set("state", query.state);
14
- }
15
- return params;
16
- });
17
- if (!searchParms.value.has("code")) {
18
- throw createError({
19
- status: 400,
20
- message: "Invalid code params"
21
- });
22
- }
23
- onMounted(async () => {
24
- if (searchParms.value.has("code")) {
25
- await authorize(searchParms.value);
26
- }
27
- });
28
- </script>
29
-
30
- <template>
31
- <div>Nuxt module auth callback!</div>
32
- </template>
@@ -1,3 +0,0 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
- declare const _default: typeof __VLS_export;
3
- export default _default;