@icecat-studio/nuxt-auth 0.1.0

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.
@@ -0,0 +1,131 @@
1
+ import { defineNuxtPlugin, useAuth, useRuntimeConfig, useState } from "#imports";
2
+ export default defineNuxtPlugin(async () => {
3
+ const config = useRuntimeConfig().public.auth;
4
+ const auth = useAuth();
5
+ const isServerManaged = config.refreshToken.serverManaged;
6
+ const shouldAutoFetch = config.user.autoFetch && config.endpoints.user;
7
+ const ssrRefreshResult = useState("auth:ssr-refresh-result", () => null);
8
+ if (import.meta.dev) {
9
+ console.log("[Auth] Session initialization starting:", {
10
+ isServer: import.meta.server,
11
+ isServerManaged,
12
+ canRefresh: auth.canRefresh.value,
13
+ shouldAutoFetch,
14
+ ssrRefreshResult: ssrRefreshResult.value
15
+ });
16
+ }
17
+ if (import.meta.client && import.meta.dev && ssrRefreshResult.value) {
18
+ console.log("[Auth] SSR refresh result:", ssrRefreshResult.value);
19
+ }
20
+ try {
21
+ await initializeSession();
22
+ if (import.meta.dev) {
23
+ console.log("[Auth] Session initialization completed:", {
24
+ isServer: import.meta.server,
25
+ status: auth.status.value,
26
+ hasAccessToken: !!auth.accessToken.value
27
+ });
28
+ }
29
+ } catch (error) {
30
+ if (import.meta.dev) {
31
+ console.error("[Auth] Session initialization failed:", error);
32
+ }
33
+ } finally {
34
+ if (import.meta.client) {
35
+ ssrRefreshResult.value = null;
36
+ }
37
+ }
38
+ async function initializeSession() {
39
+ const hasAccessToken = !!auth.accessToken.value;
40
+ const hasRefreshToken = !!auth.refreshToken.value;
41
+ if (import.meta.dev) {
42
+ console.log("[Auth] Checking session state:", {
43
+ isServer: import.meta.server,
44
+ hasAccessToken,
45
+ hasRefreshToken,
46
+ currentStatus: auth.status.value
47
+ });
48
+ }
49
+ if (auth.canRefresh.value) {
50
+ if (import.meta.dev) {
51
+ console.log("[Auth] Attempting refresh on first load", {
52
+ isServer: import.meta.server,
53
+ mode: isServerManaged ? "server-managed" : "client-managed"
54
+ });
55
+ }
56
+ if (import.meta.server) {
57
+ try {
58
+ await tryRefreshAndFetchUser();
59
+ ssrRefreshResult.value = {
60
+ attempted: true,
61
+ success: true
62
+ };
63
+ } catch (error) {
64
+ ssrRefreshResult.value = {
65
+ attempted: true,
66
+ success: false
67
+ };
68
+ throw error;
69
+ }
70
+ return;
71
+ }
72
+ if (ssrRefreshResult.value?.attempted) {
73
+ if (import.meta.dev) {
74
+ console.log("[Auth] Skipping client refresh - SSR already attempted", {
75
+ success: ssrRefreshResult.value.success
76
+ });
77
+ }
78
+ if (ssrRefreshResult.value.success) {
79
+ await fetchUserIfNeeded();
80
+ }
81
+ return;
82
+ }
83
+ await tryRefreshAndFetchUser();
84
+ return;
85
+ }
86
+ if (hasAccessToken) {
87
+ if (import.meta.dev) {
88
+ console.log("[Auth] Case 3: Has access token - restoring session");
89
+ }
90
+ await fetchUserIfNeeded();
91
+ } else if (import.meta.dev) {
92
+ console.log("[Auth] No tokens available - skipping session initialization");
93
+ }
94
+ }
95
+ async function tryRefreshAndFetchUser() {
96
+ try {
97
+ await auth.refresh();
98
+ await fetchUserIfNeeded();
99
+ } catch (error) {
100
+ if (!isServerManaged) {
101
+ auth.refreshToken.value = null;
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+ async function fetchUserIfNeeded() {
107
+ if (import.meta.server) {
108
+ auth.status.value = "authenticated";
109
+ return;
110
+ }
111
+ if (!auth.accessToken.value && auth.canRefresh.value) {
112
+ try {
113
+ await auth.refresh();
114
+ } catch {
115
+ return;
116
+ }
117
+ }
118
+ if (shouldAutoFetch) {
119
+ try {
120
+ await auth.fetchUser();
121
+ } catch (error) {
122
+ if (import.meta.dev) {
123
+ console.error("[Auth] Failed to fetch user:", error);
124
+ }
125
+ auth.status.value = "authenticated";
126
+ }
127
+ } else {
128
+ auth.status.value = "authenticated";
129
+ }
130
+ }
131
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
2
+ export default _default;
@@ -0,0 +1,102 @@
1
+ import { defineNuxtPlugin, useAuth, useRuntimeConfig, watch } from "#imports";
2
+ import { useTokenManager } from "../composables/useTokenManager.js";
3
+ export default defineNuxtPlugin((nuxtApp) => {
4
+ const config = useRuntimeConfig().public.auth;
5
+ if (!config?.autoRefresh?.enabled || config?.endpoints?.refresh === false) {
6
+ return;
7
+ }
8
+ const auth = useAuth();
9
+ const tokenManager = useTokenManager();
10
+ let refreshTimer = null;
11
+ let isPageVisible = true;
12
+ let hasStarted = false;
13
+ const refreshInterval = config.autoRefresh.interval;
14
+ const pauseOnInactive = config.autoRefresh.pauseOnInactive;
15
+ const enableTabCoordination = config.autoRefresh.enableTabCoordination;
16
+ const coordinationThreshold = config.autoRefresh.coordinationThreshold;
17
+ const stop = () => {
18
+ if (refreshTimer) {
19
+ clearTimeout(refreshTimer);
20
+ refreshTimer = null;
21
+ }
22
+ };
23
+ const shouldSkipDueToCoordination = () => {
24
+ if (!enableTabCoordination) return false;
25
+ const lastCoordination = tokenManager.coordinationTimestamp.value;
26
+ if (!lastCoordination) return false;
27
+ return Date.now() - lastCoordination < coordinationThreshold * 1e3;
28
+ };
29
+ const performRefresh = async () => {
30
+ if (shouldSkipDueToCoordination()) {
31
+ if (import.meta.dev) {
32
+ console.debug("[Auth] Skipping refresh - another tab refreshed recently");
33
+ }
34
+ schedule();
35
+ return;
36
+ }
37
+ try {
38
+ await auth.refresh();
39
+ schedule();
40
+ } catch (error) {
41
+ const responseStatus = error?.response?.status;
42
+ const isServerRejection = responseStatus !== void 0 && responseStatus >= 400 && responseStatus < 500;
43
+ if (isServerRejection) {
44
+ if (import.meta.dev) {
45
+ console.debug("[Auth] Refresh rejected by server, stopping auto-refresh");
46
+ }
47
+ } else {
48
+ if (import.meta.dev) {
49
+ console.debug("[Auth] Refresh failed (network), retrying later:", error);
50
+ }
51
+ refreshTimer = setTimeout(performRefresh, refreshInterval * 2 * 1e3);
52
+ }
53
+ }
54
+ };
55
+ const schedule = (immediate = false) => {
56
+ stop();
57
+ if (pauseOnInactive && !isPageVisible) {
58
+ if (import.meta.dev) {
59
+ console.debug("[Auth] Pausing refresh - page is not visible");
60
+ }
61
+ return;
62
+ }
63
+ if (auth.canRefresh.value) {
64
+ if (immediate) {
65
+ refreshTimer = setTimeout(performRefresh, 0);
66
+ } else {
67
+ refreshTimer = setTimeout(performRefresh, refreshInterval * 1e3);
68
+ }
69
+ }
70
+ };
71
+ nuxtApp.provide("refreshManager", {
72
+ start: schedule,
73
+ stop,
74
+ refresh: () => auth.refresh()
75
+ });
76
+ if (auth.loggedIn.value) {
77
+ schedule();
78
+ hasStarted = true;
79
+ }
80
+ watch(() => auth.loggedIn.value, (isLoggedIn) => {
81
+ if (isLoggedIn && !hasStarted) {
82
+ schedule();
83
+ hasStarted = true;
84
+ } else if (!isLoggedIn) {
85
+ stop();
86
+ hasStarted = false;
87
+ }
88
+ });
89
+ if (pauseOnInactive && typeof document !== "undefined") {
90
+ document.addEventListener("visibilitychange", () => {
91
+ isPageVisible = !document.hidden;
92
+ if (import.meta.dev) {
93
+ console.debug("[Auth] Page visibility changed:", isPageVisible ? "visible" : "hidden");
94
+ }
95
+ if (isPageVisible && hasStarted) {
96
+ schedule(true);
97
+ } else if (!isPageVisible) {
98
+ stop();
99
+ }
100
+ });
101
+ }
102
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json",
3
+ }
@@ -0,0 +1,201 @@
1
+ import type { RouterMethod } from 'h3';
2
+ import type { RequiredDeep } from 'type-fest';
3
+ /**
4
+ * Base token options shared by access and refresh tokens
5
+ */
6
+ export interface BaseTokenOptions {
7
+ /** Property name in API response containing the token. Default: 'accessToken' */
8
+ property?: string;
9
+ /** Cookie name. Default: 'auth.{tokenType}_token' */
10
+ cookieName?: string;
11
+ /** Prevents client-side access to the cookie. Default: false for accessToken, true for refreshToken */
12
+ httpOnly?: boolean;
13
+ /** Ensures cookie is only sent over HTTPS. Default: true in production */
14
+ secure?: boolean;
15
+ /** Controls cookie cross-site behavior. Default: 'lax' */
16
+ sameSite?: 'lax' | 'strict' | 'none';
17
+ /** Cookie path. Default: '/' */
18
+ path?: string;
19
+ /** Cookie domain */
20
+ domain?: string;
21
+ /** Cookie expiration time in seconds */
22
+ maxAge?: number;
23
+ }
24
+ /**
25
+ * API endpoint configuration
26
+ */
27
+ export interface Endpoint {
28
+ /** API endpoint path */
29
+ path?: string;
30
+ /** HTTP method for the request */
31
+ method?: RouterMethod;
32
+ /** Additional fetch options for this endpoint */
33
+ fetchOptions?: RequestInit;
34
+ }
35
+ /**
36
+ * Access token configuration
37
+ */
38
+ export interface AccessTokenOptions extends BaseTokenOptions {
39
+ /** Token type prefix. Default: 'Bearer' */
40
+ type?: string;
41
+ /** Authorization header name. Default: 'Authorization' */
42
+ headerName?: string;
43
+ }
44
+ /**
45
+ * Refresh token configuration
46
+ */
47
+ export interface RefreshTokenOptions extends BaseTokenOptions {
48
+ /** Storage name for localStorage/cookie. Default: 'auth.refresh_token' */
49
+ name?: string;
50
+ /**
51
+ * Indicates that refresh token is managed by the server via httpOnly cookie.
52
+ * When true, the refresh token won't be sent in the request body.
53
+ * Default: false
54
+ */
55
+ serverManaged?: boolean;
56
+ /**
57
+ * Property name used as key when sending refresh token in the request body.
58
+ * Only used when serverManaged is false.
59
+ * Default: 'refreshToken'
60
+ */
61
+ bodyProperty?: string;
62
+ }
63
+ /**
64
+ * Auto-refresh configuration
65
+ */
66
+ export interface AutoRefreshOptions {
67
+ /** Enable automatic token refresh. Default: true */
68
+ enabled?: boolean;
69
+ /**
70
+ * Token refresh interval in seconds.
71
+ * When not set, automatically calculated as `accessToken.maxAge * 0.75`.
72
+ * For example, with default maxAge of 900s (15 min), interval will be 675s (~11 min).
73
+ */
74
+ interval?: number;
75
+ /**
76
+ * Pause refresh when page is not visible (hidden tab).
77
+ * Resumes when tab becomes visible again.
78
+ * Default: true
79
+ */
80
+ pauseOnInactive?: boolean;
81
+ /**
82
+ * Enable tab coordination to prevent multiple tabs from refreshing simultaneously.
83
+ * Uses a shared cookie to track the last refresh timestamp across all tabs.
84
+ * Default: true
85
+ */
86
+ enableTabCoordination?: boolean;
87
+ /**
88
+ * Cookie name for storing last refresh timestamp for tab coordination.
89
+ * Default: 'auth.last_refresh'
90
+ */
91
+ coordinationCookieName?: string;
92
+ /**
93
+ * Minimum time between refreshes across all tabs in seconds.
94
+ * If a refresh happened in another tab within this threshold, skip refresh.
95
+ * Default: 5 (5 seconds)
96
+ */
97
+ coordinationThreshold?: number;
98
+ }
99
+ /**
100
+ * User data configuration
101
+ */
102
+ export interface UserOptions {
103
+ /**
104
+ * Property name in API response containing user data.
105
+ * If not specified or empty string, the entire response will be treated as user data.
106
+ * Default: undefined (entire response is user)
107
+ */
108
+ property?: string;
109
+ /** Automatically fetch user data on initialization. Default: true */
110
+ autoFetch?: boolean;
111
+ }
112
+ /**
113
+ * Redirect target configuration
114
+ */
115
+ export interface RedirectTarget {
116
+ /** URL to redirect to */
117
+ url: string;
118
+ /**
119
+ * Use external navigation (allows redirecting to external URLs).
120
+ * When true, performs a full page reload.
121
+ * Default: false
122
+ */
123
+ external?: boolean;
124
+ }
125
+ /**
126
+ * Redirect configuration for auth flows
127
+ */
128
+ export interface RedirectOptions {
129
+ /** Redirect path after logout or when unauthenticated. Default: '/login' */
130
+ login?: string | RedirectTarget;
131
+ /** Redirect path after logout. Default: '/' */
132
+ logout?: string | RedirectTarget;
133
+ /** Redirect path after successful login. Default: '/' */
134
+ home?: string | RedirectTarget;
135
+ }
136
+ /**
137
+ * Options for login function
138
+ */
139
+ export interface LoginOptions {
140
+ /**
141
+ * Control redirect behavior after successful login:
142
+ * - false: Disable redirect
143
+ * - string: Custom redirect URL (overrides config.redirect.home)
144
+ * - RedirectTarget: Custom redirect with options
145
+ * - undefined: Use default redirect from config
146
+ */
147
+ redirect?: false | string | RedirectTarget;
148
+ }
149
+ /**
150
+ * Page meta configuration for auth
151
+ */
152
+ export interface AuthPageMeta {
153
+ /**
154
+ * Auth configuration for the page:
155
+ * - true: Require authentication (redirect to login if not authenticated)
156
+ * - false: Public page (accessible to everyone)
157
+ * - 'guest' | 'guestOnly': Only for guests (redirect to home if authenticated)
158
+ * - undefined: Uses globalMiddleware setting
159
+ * @default undefined (uses globalMiddleware setting)
160
+ */
161
+ auth?: boolean | 'guest' | 'guestOnly';
162
+ }
163
+ /**
164
+ * Auth module configuration options
165
+ * These options can be passed in nuxt.config.ts under the 'auth' key
166
+ */
167
+ export interface ModuleOptions {
168
+ /** Base URL for all auth API endpoints */
169
+ baseUrl?: string;
170
+ /** API endpoint configurations */
171
+ endpoints?: {
172
+ /** Login endpoint. Required for authentication */
173
+ login?: Endpoint;
174
+ /** Logout endpoint. Set to false to disable */
175
+ logout?: Endpoint | false;
176
+ /** Registration endpoint. Set to false to disable */
177
+ register?: Endpoint | false;
178
+ /** Token refresh endpoint. Set to false to disable */
179
+ refresh?: Endpoint | false;
180
+ /** User data fetch endpoint */
181
+ user?: Endpoint;
182
+ };
183
+ /** Access token configuration */
184
+ accessToken?: AccessTokenOptions;
185
+ /** Refresh token configuration */
186
+ refreshToken?: RefreshTokenOptions;
187
+ /** Auto-refresh configuration */
188
+ autoRefresh?: AutoRefreshOptions;
189
+ /** User data configuration */
190
+ user?: UserOptions;
191
+ /** Redirect paths for auth flows */
192
+ redirect?: RedirectOptions;
193
+ /** Enable global auth middleware on all pages. Default: false */
194
+ globalMiddleware?: boolean;
195
+ }
196
+ /**
197
+ * Module options with all defaults applied
198
+ * All fields are guaranteed to exist after defu merges with defaults
199
+ * This is what the composables receive at runtime
200
+ */
201
+ export type ResolvedModuleOptions = RequiredDeep<ModuleOptions>;
File without changes
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Type guard to check if value is an object with index signature
3
+ * @param value - The value to check
4
+ * @returns True if the value is an indexable object
5
+ */
6
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
7
+ /**
8
+ * Get nested property value from an object using dot notation
9
+ * @param obj - The object to get the value from
10
+ * @param path - The path to the property (e.g., 'data.user.profile')
11
+ * @returns The value at the specified path or undefined
12
+ */
13
+ export declare function getNestedProperty(obj: unknown, path: string): unknown;
14
+ /**
15
+ * Set nested property value in an object using dot notation
16
+ * @param obj - The object to set the value in
17
+ * @param path - The path to the property (e.g., 'data.user.profile')
18
+ * @param value - The value to set
19
+ */
20
+ export declare function setNestedProperty(obj: Record<string, unknown>, path: string, value: unknown): void;
21
+ /**
22
+ * Check if a value is defined and not null
23
+ * @param value - The value to check
24
+ * @returns True if the value is defined and not null
25
+ */
26
+ export declare function isDefined<T>(value: T | null | undefined): value is T;
27
+ /**
28
+ * Safely parse JSON string
29
+ * @param str - The string to parse
30
+ * @param fallback - The fallback value if parsing fails
31
+ * @returns The parsed value or fallback
32
+ */
33
+ export declare function safeJsonParse<T = unknown>(str: string, fallback?: T | null): T | null;
@@ -0,0 +1,37 @@
1
+ export function isObject(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ export function getNestedProperty(obj, path) {
5
+ if (!isObject(obj)) {
6
+ return void 0;
7
+ }
8
+ return path.split(".").reduce((current, key) => {
9
+ if (isObject(current)) {
10
+ return current[key];
11
+ }
12
+ return void 0;
13
+ }, obj);
14
+ }
15
+ export function setNestedProperty(obj, path, value) {
16
+ const keys = path.split(".");
17
+ const lastKey = keys.pop();
18
+ if (!lastKey)
19
+ return;
20
+ const target = keys.reduce((current, key) => {
21
+ if (!current[key] || !isObject(current[key])) {
22
+ current[key] = {};
23
+ }
24
+ return current[key];
25
+ }, obj);
26
+ target[lastKey] = value;
27
+ }
28
+ export function isDefined(value) {
29
+ return value !== null && value !== void 0;
30
+ }
31
+ export function safeJsonParse(str, fallback = null) {
32
+ try {
33
+ return JSON.parse(str);
34
+ } catch {
35
+ return fallback;
36
+ }
37
+ }
@@ -0,0 +1,7 @@
1
+ import type { RedirectTarget } from '../types.js';
2
+ /**
3
+ * Handle redirect with support for both string URLs and RedirectTarget objects
4
+ * @param redirectConfig - URL string or RedirectTarget with external option
5
+ * @returns Navigation result or undefined if no redirect config provided
6
+ */
7
+ export declare function handleRedirect(redirectConfig?: string | RedirectTarget): string | false | void | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric | Promise<false | void | import("vue-router").NavigationFailure>;
@@ -0,0 +1,6 @@
1
+ import { navigateTo } from "#imports";
2
+ export function handleRedirect(redirectConfig) {
3
+ if (!redirectConfig) return;
4
+ const target = typeof redirectConfig === "string" ? { url: redirectConfig, external: false } : redirectConfig;
5
+ return navigateTo(target.url, { external: target.external });
6
+ }
@@ -0,0 +1,3 @@
1
+ export { type ModuleOptions } from '../dist/runtime/types.js'
2
+
3
+ export { default } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@icecat-studio/nuxt-auth",
3
+ "version": "0.1.0",
4
+ "description": "Modern authentication module for Nuxt 3+ with token-based auth, auto-refresh, SSR support, and smart tab coordination",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/icecatstudio/nuxt-auth"
8
+ },
9
+ "keywords": [
10
+ "nuxt",
11
+ "nuxt3",
12
+ "nuxt-module",
13
+ "authentication",
14
+ "auth",
15
+ "jwt",
16
+ "token",
17
+ "refresh-token",
18
+ "ssr",
19
+ "session",
20
+ "login",
21
+ "authorization"
22
+ ],
23
+ "license": "MIT",
24
+ "type": "module",
25
+ "author": {
26
+ "name": "Icecat Studio",
27
+ "url": "https://github.com/icecatstudio"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/types.d.mts",
32
+ "import": "./dist/module.mjs"
33
+ }
34
+ },
35
+ "main": "./dist/module.mjs",
36
+ "typesVersions": {
37
+ "*": {
38
+ ".": [
39
+ "./dist/types.d.mts"
40
+ ]
41
+ }
42
+ },
43
+ "files": [
44
+ "dist"
45
+ ],
46
+ "scripts": {
47
+ "prepack": "nuxt-module-build build",
48
+ "dev": "npm run dev:prepare && nuxi dev playground",
49
+ "dev:client": "npm run dev:prepare && AUTH_MODE=client-managed nuxi dev playground",
50
+ "dev:server": "npm run dev:prepare && AUTH_MODE=server-managed nuxi dev playground",
51
+ "dev:no-refresh": "npm run dev:prepare && AUTH_MODE=no-refresh nuxi dev playground",
52
+ "dev:build": "nuxi build playground",
53
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
54
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
55
+ "lint": "eslint .",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest watch",
58
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
59
+ },
60
+ "dependencies": {
61
+ "@nuxt/kit": "^4.1.3",
62
+ "defu": "^6.1.4"
63
+ },
64
+ "devDependencies": {
65
+ "@nuxt/devtools": "^2.6.5",
66
+ "@nuxt/eslint-config": "^1.9.0",
67
+ "@nuxt/module-builder": "^1.0.2",
68
+ "@nuxt/schema": "^4.1.3",
69
+ "@nuxt/test-utils": "^3.19.2",
70
+ "@types/node": "latest",
71
+ "changelogen": "^0.6.2",
72
+ "eslint": "^9.37.0",
73
+ "nuxt": "^4.1.3",
74
+ "type-fest": "^5.0.1",
75
+ "typescript": "~5.9.3",
76
+ "vitest": "^3.2.4",
77
+ "vue-tsc": "^3.1.0"
78
+ }
79
+ }