najm-auth 1.1.20 → 1.1.23
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/dist/{FetchClient-aatsED1a.d.ts → FetchClient-DadnuVF7.d.ts} +2 -0
- package/dist/{NajmAuthClient-FCwA8LIg.d.ts → NajmAuthClient-CZHKl4ri.d.ts} +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.js +5 -4
- package/dist/client/react/index.d.ts +37 -5
- package/dist/client/react/index.js +75 -22
- package/dist/client/server/index.d.ts +88 -5
- package/dist/client/server/index.js +347 -55
- package/dist/index.d.ts +59 -1
- package/dist/index.js +116 -2
- package/package.json +1 -1
|
@@ -83,6 +83,8 @@ interface AuthClientConfig {
|
|
|
83
83
|
interface AuthEventMap {
|
|
84
84
|
login: AuthUser;
|
|
85
85
|
logout: null;
|
|
86
|
+
/** Emitted when server-side logout invalidation fails (state was already cleared) */
|
|
87
|
+
logoutError: unknown;
|
|
86
88
|
tokenRefresh: null;
|
|
87
89
|
sessionExpired: null;
|
|
88
90
|
stateChange: AuthState;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as FetchClient, a as AuthClientConfig, d as AuthUser, c as AuthState, e as AuthEvent, f as AuthEventHandler } from './FetchClient-
|
|
1
|
+
import { F as FetchClient, a as AuthClientConfig, d as AuthUser, c as AuthState, e as AuthEvent, f as AuthEventHandler } from './FetchClient-DadnuVF7.js';
|
|
2
2
|
|
|
3
3
|
interface HydrateSession {
|
|
4
4
|
user: AuthUser | null;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-
|
|
2
|
-
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../FetchClient-
|
|
3
|
-
export { a as AuthClientConfig, A as AuthError, e as AuthEvent, f as AuthEventHandler, b as AuthEventMap, c as AuthState, d as AuthUser, F as FetchClient, i as RequestOptions, R as RetryConfig, g as ServerResponse, h as TokenPair } from '../FetchClient-
|
|
1
|
+
export { H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-CZHKl4ri.js';
|
|
2
|
+
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../FetchClient-DadnuVF7.js';
|
|
3
|
+
export { a as AuthClientConfig, A as AuthError, e as AuthEvent, f as AuthEventHandler, b as AuthEventMap, c as AuthState, d as AuthUser, F as FetchClient, i as RequestOptions, R as RetryConfig, g as ServerResponse, h as TokenPair } from '../FetchClient-DadnuVF7.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Decode a JWT token payload without verification.
|
package/dist/client/index.js
CHANGED
|
@@ -265,13 +265,14 @@ var NajmAuthClient = class _NajmAuthClient {
|
|
|
265
265
|
return res.data;
|
|
266
266
|
}
|
|
267
267
|
async logout() {
|
|
268
|
-
try {
|
|
269
|
-
await this.api.post(`${this.prefix}/logout`);
|
|
270
|
-
} catch {
|
|
271
|
-
}
|
|
272
268
|
this.resetState();
|
|
273
269
|
this.tabSync?.broadcastLogout();
|
|
274
270
|
this.emit("logout", null);
|
|
271
|
+
try {
|
|
272
|
+
await this.api.post(`${this.prefix}/logout`);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
this.emit("logoutError", err);
|
|
275
|
+
}
|
|
275
276
|
}
|
|
276
277
|
async refresh() {
|
|
277
278
|
if (this.refreshFailures >= _NajmAuthClient.MAX_REFRESH_FAILURES) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
import { ReactNode, CSSProperties, ReactElement } from 'react';
|
|
4
|
-
import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-
|
|
5
|
-
import { c as AuthState, d as AuthUser, A as AuthError, e as AuthEvent, b as AuthEventMap } from '../../FetchClient-
|
|
4
|
+
import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-CZHKl4ri.js';
|
|
5
|
+
import { c as AuthState, d as AuthUser, A as AuthError, e as AuthEvent, b as AuthEventMap } from '../../FetchClient-DadnuVF7.js';
|
|
6
6
|
|
|
7
7
|
interface AuthProviderProps {
|
|
8
8
|
client: NajmAuthClient;
|
|
@@ -91,6 +91,8 @@ interface UseRegisterReturn {
|
|
|
91
91
|
declare function useRegister(opts?: UseRegisterOptions): UseRegisterReturn;
|
|
92
92
|
|
|
93
93
|
interface UseLogoutOptions {
|
|
94
|
+
/** URL to redirect to after logout (uses window.location for full page reload) */
|
|
95
|
+
redirectTo?: string;
|
|
94
96
|
onSuccess?: () => void;
|
|
95
97
|
onError?: (error: Error) => void;
|
|
96
98
|
}
|
|
@@ -317,9 +319,9 @@ interface ProtectedProps {
|
|
|
317
319
|
role?: string;
|
|
318
320
|
/** Require a specific permission */
|
|
319
321
|
permission?: string;
|
|
320
|
-
/** Shown while session is loading */
|
|
322
|
+
/** Shown while session is loading (defaults to fallback) */
|
|
321
323
|
loadingFallback?: ReactNode;
|
|
322
|
-
/** Shown when access is denied */
|
|
324
|
+
/** Shown when access is denied (defaults to loadingFallback) */
|
|
323
325
|
fallback?: ReactNode;
|
|
324
326
|
}
|
|
325
327
|
/**
|
|
@@ -338,6 +340,36 @@ interface ProtectedProps {
|
|
|
338
340
|
*/
|
|
339
341
|
declare function Protected({ children, redirectTo, onUnauthenticated, role, permission, loadingFallback, fallback, }: ProtectedProps): react_jsx_runtime.JSX.Element;
|
|
340
342
|
|
|
343
|
+
interface AuthGateProps {
|
|
344
|
+
children: ReactNode;
|
|
345
|
+
/** Full-page loader shown while resolving auth on hard reload */
|
|
346
|
+
loader?: ReactNode;
|
|
347
|
+
/** Where to redirect when unauthenticated (uses window.location for full reload) */
|
|
348
|
+
redirectTo?: string;
|
|
349
|
+
/** Callback when unauthenticated (overrides redirectTo) */
|
|
350
|
+
onUnauthenticated?: () => void;
|
|
351
|
+
/** Required role — renders `denied` if user lacks it */
|
|
352
|
+
role?: string;
|
|
353
|
+
/** Required permission — renders `denied` if user lacks it */
|
|
354
|
+
permission?: string;
|
|
355
|
+
/** Shown when role/permission check fails (defaults to null) */
|
|
356
|
+
denied?: ReactNode;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Full-page authentication gate.
|
|
360
|
+
*
|
|
361
|
+
* **Key difference from hooks**: AuthGate never renders `children` until
|
|
362
|
+
* auth is confirmed. No conditional rendering bugs, no layout shift,
|
|
363
|
+
* no flash of protected content.
|
|
364
|
+
*
|
|
365
|
+
* **With SSR hydration**: If `AuthProvider` was hydrated with `initialSession`,
|
|
366
|
+
* the gate opens immediately on first render — no loader flash.
|
|
367
|
+
*
|
|
368
|
+
* **Without SSR hydration** (client-side nav, hard reload without initialSession):
|
|
369
|
+
* Shows `loader` while attempting a token refresh, then either opens or redirects.
|
|
370
|
+
*/
|
|
371
|
+
declare function AuthGate({ children, loader, redirectTo, onUnauthenticated, role, permission, denied, }: AuthGateProps): react_jsx_runtime.JSX.Element;
|
|
372
|
+
|
|
341
373
|
interface UserNameProps {
|
|
342
374
|
/** Optional fallback shown when no user is loaded */
|
|
343
375
|
fallback?: string;
|
|
@@ -492,4 +524,4 @@ interface RedirectToLoginProps {
|
|
|
492
524
|
*/
|
|
493
525
|
declare function RedirectToLogin({ to, preserveFrom }: RedirectToLoginProps): any;
|
|
494
526
|
|
|
495
|
-
export { type AuthEventEntry, AuthLoading, AuthProvider, Can, IfAuth, LoginButton, PermissionList, Protected, RedirectToLogin, Role, type SessionStatus, SignOutButton, SignedIn, SignedOut, UserAvatar, UserEmail, UserName, UserRole, useAuth, useAuthClient, useAuthEvent, useAuthEvents, useChangePassword, useForgotPassword, useLogin, useLogout, usePermissions, useRegister, useResetPassword, useSession, useUser };
|
|
527
|
+
export { type AuthEventEntry, AuthGate, AuthLoading, AuthProvider, Can, IfAuth, LoginButton, PermissionList, Protected, RedirectToLogin, Role, type SessionStatus, SignOutButton, SignedIn, SignedOut, UserAvatar, UserEmail, UserName, UserRole, useAuth, useAuthClient, useAuthEvent, useAuthEvents, useChangePassword, useForgotPassword, useLogin, useLogout, usePermissions, useRegister, useResetPassword, useSession, useUser };
|
|
@@ -152,6 +152,10 @@ function useLogout(opts) {
|
|
|
152
152
|
setIsLoading(true);
|
|
153
153
|
try {
|
|
154
154
|
await client.logout();
|
|
155
|
+
if (opts?.redirectTo && typeof window !== "undefined") {
|
|
156
|
+
window.location.href = opts.redirectTo;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
155
159
|
opts?.onSuccess?.();
|
|
156
160
|
} catch (err) {
|
|
157
161
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
@@ -159,7 +163,7 @@ function useLogout(opts) {
|
|
|
159
163
|
} finally {
|
|
160
164
|
setIsLoading(false);
|
|
161
165
|
}
|
|
162
|
-
}, [client, opts?.onSuccess, opts?.onError]);
|
|
166
|
+
}, [client, opts?.onSuccess, opts?.onError, opts?.redirectTo]);
|
|
163
167
|
return { logout, isLoading };
|
|
164
168
|
}
|
|
165
169
|
__name(useLogout, "useLogout");
|
|
@@ -276,6 +280,7 @@ import { useEffect as useEffect3, useRef as useRef4, useState as useState8 } fro
|
|
|
276
280
|
var ALL_EVENTS = [
|
|
277
281
|
"login",
|
|
278
282
|
"logout",
|
|
283
|
+
"logoutError",
|
|
279
284
|
"tokenRefresh",
|
|
280
285
|
"sessionExpired",
|
|
281
286
|
"stateChange",
|
|
@@ -372,51 +377,98 @@ function Protected({
|
|
|
372
377
|
onUnauthenticated,
|
|
373
378
|
role,
|
|
374
379
|
permission,
|
|
375
|
-
loadingFallback
|
|
376
|
-
fallback
|
|
380
|
+
loadingFallback,
|
|
381
|
+
fallback
|
|
377
382
|
}) {
|
|
383
|
+
const resolvedLoading = loadingFallback ?? fallback ?? null;
|
|
384
|
+
const resolvedFallback = fallback ?? loadingFallback ?? null;
|
|
378
385
|
const { isLoading, isAuthenticated } = useSession({
|
|
379
386
|
required: true,
|
|
380
387
|
redirectTo,
|
|
381
388
|
onUnauthenticated
|
|
382
389
|
});
|
|
383
390
|
const { can, hasRole } = usePermissions();
|
|
384
|
-
if (isLoading) return /* @__PURE__ */ jsx8(Fragment7, { children:
|
|
385
|
-
if (!isAuthenticated) return /* @__PURE__ */ jsx8(Fragment7, { children:
|
|
386
|
-
if (role && !hasRole(role)) return /* @__PURE__ */ jsx8(Fragment7, { children:
|
|
387
|
-
if (permission && !can(permission)) return /* @__PURE__ */ jsx8(Fragment7, { children:
|
|
391
|
+
if (isLoading) return /* @__PURE__ */ jsx8(Fragment7, { children: resolvedLoading });
|
|
392
|
+
if (!isAuthenticated) return /* @__PURE__ */ jsx8(Fragment7, { children: resolvedFallback });
|
|
393
|
+
if (role && !hasRole(role)) return /* @__PURE__ */ jsx8(Fragment7, { children: resolvedFallback });
|
|
394
|
+
if (permission && !can(permission)) return /* @__PURE__ */ jsx8(Fragment7, { children: resolvedFallback });
|
|
388
395
|
return /* @__PURE__ */ jsx8(Fragment7, { children });
|
|
389
396
|
}
|
|
390
397
|
__name(Protected, "Protected");
|
|
391
398
|
|
|
392
|
-
// src/client/react/
|
|
399
|
+
// src/client/react/AuthGate.tsx
|
|
400
|
+
import { useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
393
401
|
import { Fragment as Fragment8, jsx as jsx9 } from "react/jsx-runtime";
|
|
402
|
+
function AuthGate({
|
|
403
|
+
children,
|
|
404
|
+
loader = null,
|
|
405
|
+
redirectTo,
|
|
406
|
+
onUnauthenticated,
|
|
407
|
+
role,
|
|
408
|
+
permission,
|
|
409
|
+
denied = null
|
|
410
|
+
}) {
|
|
411
|
+
const client = useAuthClient();
|
|
412
|
+
const state = useSyncExternalStore2(
|
|
413
|
+
(cb) => client.subscribe(cb),
|
|
414
|
+
() => client.getState(),
|
|
415
|
+
() => client.getState()
|
|
416
|
+
);
|
|
417
|
+
const hydrated = client.isHydrated();
|
|
418
|
+
useEffect4(() => {
|
|
419
|
+
if (hydrated) return;
|
|
420
|
+
client.refresh().then(() => client.fetchUser()).catch(() => {
|
|
421
|
+
});
|
|
422
|
+
}, [client, hydrated]);
|
|
423
|
+
if (!hydrated && !state.isAuthenticated) {
|
|
424
|
+
return /* @__PURE__ */ jsx9(Fragment8, { children: loader });
|
|
425
|
+
}
|
|
426
|
+
if (!state.isAuthenticated) {
|
|
427
|
+
if (onUnauthenticated) {
|
|
428
|
+
onUnauthenticated();
|
|
429
|
+
} else if (redirectTo && typeof window !== "undefined") {
|
|
430
|
+
window.location.href = redirectTo;
|
|
431
|
+
}
|
|
432
|
+
return /* @__PURE__ */ jsx9(Fragment8, { children: loader });
|
|
433
|
+
}
|
|
434
|
+
if (role && !client.hasRole(role)) {
|
|
435
|
+
return /* @__PURE__ */ jsx9(Fragment8, { children: denied });
|
|
436
|
+
}
|
|
437
|
+
if (permission && !client.can(permission)) {
|
|
438
|
+
return /* @__PURE__ */ jsx9(Fragment8, { children: denied });
|
|
439
|
+
}
|
|
440
|
+
return /* @__PURE__ */ jsx9(Fragment8, { children });
|
|
441
|
+
}
|
|
442
|
+
__name(AuthGate, "AuthGate");
|
|
443
|
+
|
|
444
|
+
// src/client/react/UserName.tsx
|
|
445
|
+
import { Fragment as Fragment9, jsx as jsx10 } from "react/jsx-runtime";
|
|
394
446
|
function UserName({ fallback = "" }) {
|
|
395
447
|
const user = useUser();
|
|
396
|
-
if (!user) return /* @__PURE__ */
|
|
448
|
+
if (!user) return /* @__PURE__ */ jsx10(Fragment9, { children: fallback });
|
|
397
449
|
const name = user.name ?? user.email ?? fallback;
|
|
398
|
-
return /* @__PURE__ */
|
|
450
|
+
return /* @__PURE__ */ jsx10(Fragment9, { children: name });
|
|
399
451
|
}
|
|
400
452
|
__name(UserName, "UserName");
|
|
401
453
|
|
|
402
454
|
// src/client/react/UserEmail.tsx
|
|
403
|
-
import { Fragment as
|
|
455
|
+
import { Fragment as Fragment10, jsx as jsx11 } from "react/jsx-runtime";
|
|
404
456
|
function UserEmail({ fallback = "" }) {
|
|
405
457
|
const user = useUser();
|
|
406
|
-
return /* @__PURE__ */
|
|
458
|
+
return /* @__PURE__ */ jsx11(Fragment10, { children: user?.email ?? fallback });
|
|
407
459
|
}
|
|
408
460
|
__name(UserEmail, "UserEmail");
|
|
409
461
|
|
|
410
462
|
// src/client/react/UserRole.tsx
|
|
411
|
-
import { Fragment as
|
|
463
|
+
import { Fragment as Fragment11, jsx as jsx12 } from "react/jsx-runtime";
|
|
412
464
|
function UserRole({ fallback = "" }) {
|
|
413
465
|
const { roles } = usePermissions();
|
|
414
|
-
return /* @__PURE__ */
|
|
466
|
+
return /* @__PURE__ */ jsx12(Fragment11, { children: roles[0] ?? fallback });
|
|
415
467
|
}
|
|
416
468
|
__name(UserRole, "UserRole");
|
|
417
469
|
|
|
418
470
|
// src/client/react/UserAvatar.tsx
|
|
419
|
-
import { jsx as
|
|
471
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
420
472
|
function UserAvatar({ size = 32, className, style, alt }) {
|
|
421
473
|
const user = useUser();
|
|
422
474
|
const baseStyle = {
|
|
@@ -439,7 +491,7 @@ function UserAvatar({ size = 32, className, style, alt }) {
|
|
|
439
491
|
const initial = label ? label[0].toUpperCase() : "?";
|
|
440
492
|
const altText = alt ?? label ?? "User avatar";
|
|
441
493
|
if (src) {
|
|
442
|
-
return /* @__PURE__ */
|
|
494
|
+
return /* @__PURE__ */ jsx13(
|
|
443
495
|
"img",
|
|
444
496
|
{
|
|
445
497
|
src,
|
|
@@ -451,7 +503,7 @@ function UserAvatar({ size = 32, className, style, alt }) {
|
|
|
451
503
|
}
|
|
452
504
|
);
|
|
453
505
|
}
|
|
454
|
-
return /* @__PURE__ */
|
|
506
|
+
return /* @__PURE__ */ jsx13(
|
|
455
507
|
"span",
|
|
456
508
|
{
|
|
457
509
|
role: "img",
|
|
@@ -465,11 +517,11 @@ function UserAvatar({ size = 32, className, style, alt }) {
|
|
|
465
517
|
__name(UserAvatar, "UserAvatar");
|
|
466
518
|
|
|
467
519
|
// src/client/react/PermissionList.tsx
|
|
468
|
-
import { Fragment as
|
|
520
|
+
import { Fragment as Fragment12, jsx as jsx14 } from "react/jsx-runtime";
|
|
469
521
|
function PermissionList({ children, fallback = null }) {
|
|
470
522
|
const { permissions } = usePermissions();
|
|
471
|
-
if (permissions.length === 0) return /* @__PURE__ */
|
|
472
|
-
return /* @__PURE__ */
|
|
523
|
+
if (permissions.length === 0) return /* @__PURE__ */ jsx14(Fragment12, { children: fallback });
|
|
524
|
+
return /* @__PURE__ */ jsx14(Fragment12, { children: permissions.map((perm, i) => children(perm, i)) });
|
|
473
525
|
}
|
|
474
526
|
__name(PermissionList, "PermissionList");
|
|
475
527
|
|
|
@@ -508,9 +560,9 @@ function LoginButton({ href = "/login", children }) {
|
|
|
508
560
|
__name(LoginButton, "LoginButton");
|
|
509
561
|
|
|
510
562
|
// src/client/react/RedirectToLogin.tsx
|
|
511
|
-
import { useEffect as
|
|
563
|
+
import { useEffect as useEffect5 } from "react";
|
|
512
564
|
function RedirectToLogin({ to = "/login", preserveFrom = true }) {
|
|
513
|
-
|
|
565
|
+
useEffect5(() => {
|
|
514
566
|
if (typeof window === "undefined") return;
|
|
515
567
|
const target = preserveFrom ? `${to}?from=${encodeURIComponent(window.location.pathname + window.location.search)}` : to;
|
|
516
568
|
window.location.href = target;
|
|
@@ -519,6 +571,7 @@ function RedirectToLogin({ to = "/login", preserveFrom = true }) {
|
|
|
519
571
|
}
|
|
520
572
|
__name(RedirectToLogin, "RedirectToLogin");
|
|
521
573
|
export {
|
|
574
|
+
AuthGate,
|
|
522
575
|
AuthLoading,
|
|
523
576
|
AuthProvider,
|
|
524
577
|
Can,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as AuthUser, F as FetchClient } from '../../FetchClient-
|
|
1
|
+
import { d as AuthUser, F as FetchClient } from '../../FetchClient-DadnuVF7.js';
|
|
2
2
|
import * as next_server from 'next/server';
|
|
3
3
|
|
|
4
4
|
interface GetServerSessionOptions {
|
|
@@ -115,19 +115,53 @@ interface GetSessionConfig {
|
|
|
115
115
|
*/
|
|
116
116
|
authPrefix?: string;
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
118
|
+
* Refresh token cookie name to check before making the network call.
|
|
119
119
|
* If absent we skip and return null immediately.
|
|
120
120
|
* Default: 'refreshToken'
|
|
121
121
|
*/
|
|
122
122
|
cookieName?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Session cookie name (default: 'najm.session').
|
|
125
|
+
* When present and valid, skips the /auth/me fetch entirely.
|
|
126
|
+
*/
|
|
127
|
+
sessionCookieName?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Secret used to verify the session cookie HMAC signature.
|
|
130
|
+
* Must match the `jwt.accessSecret` used by the auth plugin.
|
|
131
|
+
* Falls back to NAJM_SESSION_SECRET or JWT_ACCESS_SECRET env vars.
|
|
132
|
+
*/
|
|
133
|
+
sessionSecret?: string;
|
|
134
|
+
/**
|
|
135
|
+
* Error handling mode:
|
|
136
|
+
* - 'nullable' (default): returns null on any failure
|
|
137
|
+
* - 'strict': throws typed errors for debugging
|
|
138
|
+
*/
|
|
139
|
+
mode?: 'nullable' | 'strict';
|
|
140
|
+
}
|
|
141
|
+
declare class NoSessionError extends Error {
|
|
142
|
+
readonly code = "NO_SESSION";
|
|
143
|
+
constructor(message?: string);
|
|
144
|
+
}
|
|
145
|
+
declare class AuthConfigError extends Error {
|
|
146
|
+
readonly code = "AUTH_CONFIG_ERROR";
|
|
147
|
+
constructor(message: string);
|
|
148
|
+
}
|
|
149
|
+
declare class AuthTransportError extends Error {
|
|
150
|
+
readonly status?: number;
|
|
151
|
+
readonly code = "AUTH_TRANSPORT_ERROR";
|
|
152
|
+
constructor(message: string, status?: number);
|
|
123
153
|
}
|
|
124
154
|
/**
|
|
125
155
|
* Resolve the current session inside a Next.js Server Component, Route Handler,
|
|
126
|
-
* or Server Action.
|
|
156
|
+
* or Server Action.
|
|
157
|
+
*
|
|
158
|
+
* **Fast path**: If a signed `najm.session` cookie exists and is valid,
|
|
159
|
+
* returns the session instantly with zero network calls.
|
|
160
|
+
*
|
|
161
|
+
* **Fallback**: Checks the refresh token cookie and calls `/auth/me`.
|
|
127
162
|
*
|
|
128
163
|
* @example
|
|
129
164
|
* ```tsx
|
|
130
|
-
* // app/layout.tsx
|
|
131
165
|
* import { getSession } from 'najm-auth/client/server';
|
|
132
166
|
*
|
|
133
167
|
* export default async function RootLayout({ children }) {
|
|
@@ -163,4 +197,53 @@ interface WithAuthProps<P> {
|
|
|
163
197
|
*/
|
|
164
198
|
declare function withAuth<P extends Record<string, unknown> = Record<string, unknown>>(Page: (args: WithAuthProps<P>) => Promise<unknown> | unknown, options?: WithAuthOptions): (props: P) => Promise<unknown>;
|
|
165
199
|
|
|
166
|
-
|
|
200
|
+
interface DefineAuthConfig {
|
|
201
|
+
/** API base URL (default: '/api') */
|
|
202
|
+
apiBaseURL?: string;
|
|
203
|
+
/** Auth prefix appended to apiBaseURL (default: '/auth') */
|
|
204
|
+
authPrefix?: string;
|
|
205
|
+
/** Route to redirect unauthenticated users (default: '/login') */
|
|
206
|
+
loginRoute?: string;
|
|
207
|
+
/** Route to redirect after login (default: '/dashboard') */
|
|
208
|
+
afterLoginRoute?: string;
|
|
209
|
+
/** Routes that are always public (glob patterns) */
|
|
210
|
+
publicRoutes?: string[];
|
|
211
|
+
/** Routes that require authentication (glob patterns) */
|
|
212
|
+
protectedRoutes?: string[];
|
|
213
|
+
/** Routes restricted to specific roles: { '/admin/:path*': ['admin'] } */
|
|
214
|
+
roleRoutes?: Record<string, string[]>;
|
|
215
|
+
/** Refresh token cookie name (default: 'refreshToken') */
|
|
216
|
+
cookieName?: string;
|
|
217
|
+
/** Session cookie name (default: 'najm.session') */
|
|
218
|
+
sessionCookieName?: string;
|
|
219
|
+
/** Secret for verifying session cookie HMAC. Falls back to env vars. */
|
|
220
|
+
sessionSecret?: string;
|
|
221
|
+
/** Next.js middleware matcher (default: exclude _next, favicon, api) */
|
|
222
|
+
matcher?: string[];
|
|
223
|
+
}
|
|
224
|
+
interface AuthKit {
|
|
225
|
+
/** Resolve session — reads signed cookie first (instant), falls back to /auth/me */
|
|
226
|
+
getSession: (opts?: Pick<GetSessionConfig, 'mode'>) => Promise<ServerSession | null>;
|
|
227
|
+
/** Require session — throws if unauthenticated */
|
|
228
|
+
requireSession: () => Promise<ServerSession>;
|
|
229
|
+
/** Generated Next.js middleware function */
|
|
230
|
+
middleware: (request: Request) => Promise<Response>;
|
|
231
|
+
/** Next.js middleware config with matcher */
|
|
232
|
+
config: {
|
|
233
|
+
matcher: string[];
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Protect a server component — redirects to loginRoute if unauthenticated.
|
|
237
|
+
* Passes session to the wrapped component.
|
|
238
|
+
*/
|
|
239
|
+
protect: <P extends Record<string, unknown> = Record<string, unknown>>(Page: (args: {
|
|
240
|
+
session: ServerSession;
|
|
241
|
+
children?: unknown;
|
|
242
|
+
} & P) => Promise<unknown> | unknown, options?: {
|
|
243
|
+
role?: string;
|
|
244
|
+
permission?: string;
|
|
245
|
+
}) => (props: P) => Promise<unknown>;
|
|
246
|
+
}
|
|
247
|
+
declare function defineAuth(authConfig?: DefineAuthConfig): AuthKit;
|
|
248
|
+
|
|
249
|
+
export { AuthConfigError, type AuthKit, AuthTransportError, type DefineAuthConfig, type GetSessionConfig, NoSessionError, type ServerSession, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getServerSession, getSession, withAuth, withAuthMiddleware };
|
|
@@ -1,5 +1,199 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
3
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/client/server/getSession.ts
|
|
13
|
+
var getSession_exports = {};
|
|
14
|
+
__export(getSession_exports, {
|
|
15
|
+
AuthConfigError: () => AuthConfigError,
|
|
16
|
+
AuthTransportError: () => AuthTransportError,
|
|
17
|
+
NoSessionError: () => NoSessionError,
|
|
18
|
+
getSession: () => getSession
|
|
19
|
+
});
|
|
20
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
21
|
+
function defaultBaseURL() {
|
|
22
|
+
const explicit = typeof process !== "undefined" ? process.env.NAJM_AUTH_BASE_URL : void 0;
|
|
23
|
+
if (explicit) return explicit;
|
|
24
|
+
const origin = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL ?? `http://localhost:${process.env.PORT ?? 3e3}` : "http://localhost:3000";
|
|
25
|
+
return `${origin.replace(/\/$/, "")}/api`;
|
|
26
|
+
}
|
|
27
|
+
function getSessionSecret(config) {
|
|
28
|
+
return config.sessionSecret ?? (typeof process !== "undefined" ? process.env.NAJM_SESSION_SECRET : void 0) ?? (typeof process !== "undefined" ? process.env.JWT_ACCESS_SECRET : void 0);
|
|
29
|
+
}
|
|
30
|
+
function verifyHmac(payload, signature, secret) {
|
|
31
|
+
const expected = createHmac("sha256", secret).update(payload).digest("base64url");
|
|
32
|
+
if (expected.length !== signature.length) return false;
|
|
33
|
+
try {
|
|
34
|
+
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function parseSessionCookie(raw, secret) {
|
|
40
|
+
const lastDot = raw.lastIndexOf(".");
|
|
41
|
+
if (lastDot === -1) return null;
|
|
42
|
+
const payload = raw.substring(0, lastDot);
|
|
43
|
+
const signature = raw.substring(lastDot + 1);
|
|
44
|
+
if (!verifyHmac(payload, signature, secret)) return null;
|
|
45
|
+
try {
|
|
46
|
+
const decoded = decodeURIComponent(payload);
|
|
47
|
+
const data = JSON.parse(decoded);
|
|
48
|
+
if (Date.now() - data.iat > SESSION_COOKIE_MAX_AGE_MS) return null;
|
|
49
|
+
const { user, roles, permissions } = data;
|
|
50
|
+
return {
|
|
51
|
+
user,
|
|
52
|
+
roles: roles ?? (user.role ? [user.role] : void 0),
|
|
53
|
+
permissions: permissions ?? void 0
|
|
54
|
+
};
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function buildSession(body) {
|
|
60
|
+
if (!body?.data) return null;
|
|
61
|
+
const { roles, permissions, ...user } = body.data;
|
|
62
|
+
return {
|
|
63
|
+
user,
|
|
64
|
+
roles: roles ?? (user.role ? [user.role] : void 0),
|
|
65
|
+
permissions: permissions ?? user.permissions
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function getSession(config = {}) {
|
|
69
|
+
const cookieName = config.cookieName ?? "refreshToken";
|
|
70
|
+
const sessionCookieName = config.sessionCookieName ?? "najm.session";
|
|
71
|
+
const baseURL = config.baseURL ?? defaultBaseURL();
|
|
72
|
+
const prefix = config.authPrefix ?? "/auth";
|
|
73
|
+
const strict = config.mode === "strict";
|
|
74
|
+
let cookieHeader = "";
|
|
75
|
+
let sessionCookieValue;
|
|
76
|
+
let hasRefreshCookie = false;
|
|
77
|
+
try {
|
|
78
|
+
const mod = await import("next/headers");
|
|
79
|
+
const cookieStore = await mod.cookies();
|
|
80
|
+
const sessionCookie = cookieStore.get(sessionCookieName);
|
|
81
|
+
if (sessionCookie) sessionCookieValue = sessionCookie.value;
|
|
82
|
+
if (typeof cookieStore.get === "function") {
|
|
83
|
+
hasRefreshCookie = !!cookieStore.get(cookieName);
|
|
84
|
+
}
|
|
85
|
+
cookieHeader = cookieStore.getAll().map((c) => `${c.name}=${c.value}`).join("; ");
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (strict) throw new AuthConfigError("Failed to read cookies from Next.js headers()");
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (sessionCookieValue) {
|
|
91
|
+
const secret = getSessionSecret(config);
|
|
92
|
+
if (secret) {
|
|
93
|
+
const session = parseSessionCookie(sessionCookieValue, secret);
|
|
94
|
+
if (session) return session;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!hasRefreshCookie) {
|
|
98
|
+
if (strict) throw new NoSessionError("No refresh token cookie");
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (!cookieHeader) {
|
|
102
|
+
if (strict) throw new NoSessionError("Empty cookie header");
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch(`${baseURL}${prefix}/me`, {
|
|
107
|
+
headers: { Cookie: cookieHeader, Accept: "application/json" },
|
|
108
|
+
cache: "no-store"
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
if (strict) throw new AuthTransportError(`/auth/me returned ${res.status}`, res.status);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const body = await res.json();
|
|
115
|
+
const session = buildSession(body);
|
|
116
|
+
if (!session && strict) throw new NoSessionError("No user data in /auth/me response");
|
|
117
|
+
return session;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (err instanceof NoSessionError || err instanceof AuthTransportError) throw err;
|
|
120
|
+
if (strict) throw new AuthTransportError(`Failed to fetch /auth/me: ${err.message}`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
var NoSessionError, AuthConfigError, AuthTransportError, SESSION_COOKIE_MAX_AGE_MS;
|
|
125
|
+
var init_getSession = __esm({
|
|
126
|
+
"src/client/server/getSession.ts"() {
|
|
127
|
+
NoSessionError = class extends Error {
|
|
128
|
+
static {
|
|
129
|
+
__name(this, "NoSessionError");
|
|
130
|
+
}
|
|
131
|
+
code = "NO_SESSION";
|
|
132
|
+
constructor(message = "No active session") {
|
|
133
|
+
super(message);
|
|
134
|
+
this.name = "NoSessionError";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
AuthConfigError = class extends Error {
|
|
138
|
+
static {
|
|
139
|
+
__name(this, "AuthConfigError");
|
|
140
|
+
}
|
|
141
|
+
code = "AUTH_CONFIG_ERROR";
|
|
142
|
+
constructor(message) {
|
|
143
|
+
super(message);
|
|
144
|
+
this.name = "AuthConfigError";
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
AuthTransportError = class extends Error {
|
|
148
|
+
constructor(message, status) {
|
|
149
|
+
super(message);
|
|
150
|
+
this.status = status;
|
|
151
|
+
this.name = "AuthTransportError";
|
|
152
|
+
}
|
|
153
|
+
static {
|
|
154
|
+
__name(this, "AuthTransportError");
|
|
155
|
+
}
|
|
156
|
+
code = "AUTH_TRANSPORT_ERROR";
|
|
157
|
+
};
|
|
158
|
+
__name(defaultBaseURL, "defaultBaseURL");
|
|
159
|
+
__name(getSessionSecret, "getSessionSecret");
|
|
160
|
+
__name(verifyHmac, "verifyHmac");
|
|
161
|
+
SESSION_COOKIE_MAX_AGE_MS = 3e5;
|
|
162
|
+
__name(parseSessionCookie, "parseSessionCookie");
|
|
163
|
+
__name(buildSession, "buildSession");
|
|
164
|
+
__name(getSession, "getSession");
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// src/client/permissions.ts
|
|
169
|
+
var permissions_exports = {};
|
|
170
|
+
__export(permissions_exports, {
|
|
171
|
+
hasAnyRole: () => hasAnyRole,
|
|
172
|
+
hasRole: () => hasRole,
|
|
173
|
+
matchPermission: () => matchPermission
|
|
174
|
+
});
|
|
175
|
+
function matchPermission(userPermissions, required) {
|
|
176
|
+
if (userPermissions.includes("*:*")) return true;
|
|
177
|
+
if (userPermissions.includes(required)) return true;
|
|
178
|
+
const sep = required.indexOf(":");
|
|
179
|
+
if (sep === -1) return false;
|
|
180
|
+
const action = required.slice(0, sep);
|
|
181
|
+
const resource = required.slice(sep + 1);
|
|
182
|
+
return userPermissions.includes(`${action}:*`) || userPermissions.includes(`*:${resource}`);
|
|
183
|
+
}
|
|
184
|
+
function hasRole(userRoles, role) {
|
|
185
|
+
return userRoles.includes(role);
|
|
186
|
+
}
|
|
187
|
+
function hasAnyRole(userRoles, roles) {
|
|
188
|
+
return roles.some((r) => userRoles.includes(r));
|
|
189
|
+
}
|
|
190
|
+
var init_permissions = __esm({
|
|
191
|
+
"src/client/permissions.ts"() {
|
|
192
|
+
__name(matchPermission, "matchPermission");
|
|
193
|
+
__name(hasRole, "hasRole");
|
|
194
|
+
__name(hasAnyRole, "hasAnyRole");
|
|
195
|
+
}
|
|
196
|
+
});
|
|
3
197
|
|
|
4
198
|
// src/client/server/getServerSession.ts
|
|
5
199
|
async function getServerSession(opts) {
|
|
@@ -233,63 +427,12 @@ function findMatchingRoles(pathname, roleRoutes) {
|
|
|
233
427
|
}
|
|
234
428
|
__name(findMatchingRoles, "findMatchingRoles");
|
|
235
429
|
|
|
236
|
-
// src/client/server/
|
|
237
|
-
|
|
238
|
-
const explicit = typeof process !== "undefined" ? process.env.NAJM_AUTH_BASE_URL : void 0;
|
|
239
|
-
if (explicit) return explicit;
|
|
240
|
-
const origin = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL ?? `http://localhost:${process.env.PORT ?? 3e3}` : "http://localhost:3000";
|
|
241
|
-
return `${origin.replace(/\/$/, "")}/api`;
|
|
242
|
-
}
|
|
243
|
-
__name(defaultBaseURL, "defaultBaseURL");
|
|
244
|
-
async function getSession(config = {}) {
|
|
245
|
-
const cookieName = config.cookieName ?? "refreshToken";
|
|
246
|
-
const baseURL = config.baseURL ?? defaultBaseURL();
|
|
247
|
-
const prefix = config.authPrefix ?? "/auth";
|
|
248
|
-
let cookieHeader = "";
|
|
249
|
-
try {
|
|
250
|
-
const mod = await import("next/headers");
|
|
251
|
-
const cookieStore = await mod.cookies();
|
|
252
|
-
if (typeof cookieStore.get === "function" && !cookieStore.get(cookieName)) {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
cookieHeader = cookieStore.getAll().map((c) => `${c.name}=${c.value}`).join("; ");
|
|
256
|
-
} catch {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
if (!cookieHeader) return null;
|
|
260
|
-
try {
|
|
261
|
-
const res = await fetch(`${baseURL}${prefix}/me`, {
|
|
262
|
-
headers: { Cookie: cookieHeader, Accept: "application/json" },
|
|
263
|
-
cache: "no-store"
|
|
264
|
-
});
|
|
265
|
-
if (!res.ok) return null;
|
|
266
|
-
const body = await res.json();
|
|
267
|
-
if (!body?.data) return null;
|
|
268
|
-
const { roles, permissions, ...user } = body.data;
|
|
269
|
-
return {
|
|
270
|
-
user,
|
|
271
|
-
roles: roles ?? (user.role ? [user.role] : void 0),
|
|
272
|
-
permissions: permissions ?? user.permissions
|
|
273
|
-
};
|
|
274
|
-
} catch {
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
__name(getSession, "getSession");
|
|
279
|
-
|
|
280
|
-
// src/client/permissions.ts
|
|
281
|
-
function matchPermission(userPermissions, required) {
|
|
282
|
-
if (userPermissions.includes("*:*")) return true;
|
|
283
|
-
if (userPermissions.includes(required)) return true;
|
|
284
|
-
const sep = required.indexOf(":");
|
|
285
|
-
if (sep === -1) return false;
|
|
286
|
-
const action = required.slice(0, sep);
|
|
287
|
-
const resource = required.slice(sep + 1);
|
|
288
|
-
return userPermissions.includes(`${action}:*`) || userPermissions.includes(`*:${resource}`);
|
|
289
|
-
}
|
|
290
|
-
__name(matchPermission, "matchPermission");
|
|
430
|
+
// src/client/server/index.ts
|
|
431
|
+
init_getSession();
|
|
291
432
|
|
|
292
433
|
// src/client/server/withAuth.ts
|
|
434
|
+
init_getSession();
|
|
435
|
+
init_permissions();
|
|
293
436
|
function withAuth(Page, options = {}) {
|
|
294
437
|
const { redirectTo = "/login", role, permission, ...sessionConfig } = options;
|
|
295
438
|
return /* @__PURE__ */ __name(async function ProtectedPage(props) {
|
|
@@ -316,8 +459,157 @@ function withAuth(Page, options = {}) {
|
|
|
316
459
|
}, "ProtectedPage");
|
|
317
460
|
}
|
|
318
461
|
__name(withAuth, "withAuth");
|
|
462
|
+
|
|
463
|
+
// src/client/server/defineAuth.ts
|
|
464
|
+
function matchesAny2(pathname, patterns) {
|
|
465
|
+
return patterns.some((p) => matchPattern2(pathname, p));
|
|
466
|
+
}
|
|
467
|
+
__name(matchesAny2, "matchesAny");
|
|
468
|
+
function matchPattern2(pathname, pattern) {
|
|
469
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
470
|
+
const regex = escaped.replace(/\/:[^/]+\*/g, "(?:/.*)?").replace(/\/\\\*$/g, "(?:/.*)?").replace(/\\\*/g, "(?:/.*)?").replace(/\//g, "\\/");
|
|
471
|
+
return new RegExp(`^${regex}$`).test(pathname);
|
|
472
|
+
}
|
|
473
|
+
__name(matchPattern2, "matchPattern");
|
|
474
|
+
function cookieRegex2(name) {
|
|
475
|
+
return new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=[^;]`);
|
|
476
|
+
}
|
|
477
|
+
__name(cookieRegex2, "cookieRegex");
|
|
478
|
+
function findMatchingRoles2(pathname, roleRoutes) {
|
|
479
|
+
for (const [pattern, roles] of Object.entries(roleRoutes)) {
|
|
480
|
+
if (matchPattern2(pathname, pattern)) return roles;
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
__name(findMatchingRoles2, "findMatchingRoles");
|
|
485
|
+
function defineAuth(authConfig = {}) {
|
|
486
|
+
const {
|
|
487
|
+
apiBaseURL = "/api",
|
|
488
|
+
authPrefix = "/auth",
|
|
489
|
+
loginRoute = "/login",
|
|
490
|
+
publicRoutes = [],
|
|
491
|
+
protectedRoutes = [],
|
|
492
|
+
roleRoutes = {},
|
|
493
|
+
cookieName = "refreshToken",
|
|
494
|
+
sessionCookieName = "najm.session",
|
|
495
|
+
sessionSecret,
|
|
496
|
+
matcher = ["/((?!_next/static|_next/image|favicon.ico|api).*)"]
|
|
497
|
+
} = authConfig;
|
|
498
|
+
const sessionConfig = {
|
|
499
|
+
baseURL: void 0,
|
|
500
|
+
// resolved at call time from env/defaults
|
|
501
|
+
authPrefix,
|
|
502
|
+
cookieName,
|
|
503
|
+
sessionCookieName,
|
|
504
|
+
sessionSecret
|
|
505
|
+
};
|
|
506
|
+
const getSession2 = /* @__PURE__ */ __name(async (opts) => {
|
|
507
|
+
const { getSession: resolveSession } = await Promise.resolve().then(() => (init_getSession(), getSession_exports));
|
|
508
|
+
return resolveSession({ ...sessionConfig, ...opts });
|
|
509
|
+
}, "getSession");
|
|
510
|
+
const requireSession = /* @__PURE__ */ __name(async () => {
|
|
511
|
+
const sessionModule = await Promise.resolve().then(() => (init_getSession(), getSession_exports));
|
|
512
|
+
const {
|
|
513
|
+
getSession: resolveSession,
|
|
514
|
+
NoSessionError: NoSessionError2,
|
|
515
|
+
AuthTransportError: AuthTransportError2
|
|
516
|
+
} = sessionModule;
|
|
517
|
+
try {
|
|
518
|
+
const session = await resolveSession({ ...sessionConfig, mode: "strict" });
|
|
519
|
+
return session;
|
|
520
|
+
} catch (err) {
|
|
521
|
+
if (err instanceof NoSessionError2) {
|
|
522
|
+
const { redirect } = await import("next/navigation");
|
|
523
|
+
redirect(loginRoute);
|
|
524
|
+
}
|
|
525
|
+
if (err instanceof AuthTransportError2 && (err.status === 401 || err.status === 403)) {
|
|
526
|
+
const { redirect } = await import("next/navigation");
|
|
527
|
+
redirect(loginRoute);
|
|
528
|
+
}
|
|
529
|
+
throw err;
|
|
530
|
+
}
|
|
531
|
+
}, "requireSession");
|
|
532
|
+
const middleware = /* @__PURE__ */ __name(async (request) => {
|
|
533
|
+
const { NextResponse } = await import("next/server");
|
|
534
|
+
const url = new URL(request.url);
|
|
535
|
+
const pathname = url.pathname;
|
|
536
|
+
if (matchesAny2(pathname, publicRoutes)) {
|
|
537
|
+
return NextResponse.next();
|
|
538
|
+
}
|
|
539
|
+
const isProtected = protectedRoutes.length === 0 || matchesAny2(pathname, protectedRoutes);
|
|
540
|
+
if (!isProtected) return NextResponse.next();
|
|
541
|
+
const cookie = request.headers.get("cookie") ?? "";
|
|
542
|
+
const hasToken = cookieRegex2(cookieName).test(cookie);
|
|
543
|
+
if (!hasToken) {
|
|
544
|
+
const loginUrl = new URL(loginRoute, request.url);
|
|
545
|
+
loginUrl.searchParams.set("from", pathname);
|
|
546
|
+
return NextResponse.redirect(loginUrl);
|
|
547
|
+
}
|
|
548
|
+
const requiredRoles = findMatchingRoles2(pathname, roleRoutes);
|
|
549
|
+
if (requiredRoles) {
|
|
550
|
+
const verifyURL = `${url.origin}${apiBaseURL}${authPrefix}/me`;
|
|
551
|
+
try {
|
|
552
|
+
const res = await fetch(verifyURL, {
|
|
553
|
+
headers: { Cookie: cookie, Accept: "application/json" }
|
|
554
|
+
});
|
|
555
|
+
if (!res.ok) {
|
|
556
|
+
const loginUrl = new URL(loginRoute, request.url);
|
|
557
|
+
loginUrl.searchParams.set("from", pathname);
|
|
558
|
+
return NextResponse.redirect(loginUrl);
|
|
559
|
+
}
|
|
560
|
+
const body = await res.json();
|
|
561
|
+
const userRole = body?.data?.role;
|
|
562
|
+
if (!userRole || !requiredRoles.includes(userRole)) {
|
|
563
|
+
return new NextResponse(null, { status: 403 });
|
|
564
|
+
}
|
|
565
|
+
} catch {
|
|
566
|
+
const loginUrl = new URL(loginRoute, request.url);
|
|
567
|
+
loginUrl.searchParams.set("from", pathname);
|
|
568
|
+
return NextResponse.redirect(loginUrl);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return NextResponse.next();
|
|
572
|
+
}, "middleware");
|
|
573
|
+
const protect = /* @__PURE__ */ __name((Page, options) => {
|
|
574
|
+
return /* @__PURE__ */ __name(async function ProtectedPage(props) {
|
|
575
|
+
const session = await getSession2();
|
|
576
|
+
if (!session) {
|
|
577
|
+
const { redirect } = await import("next/navigation");
|
|
578
|
+
redirect(loginRoute);
|
|
579
|
+
}
|
|
580
|
+
if (options?.role) {
|
|
581
|
+
const userRoles = session.roles ?? (session.user.role ? [session.user.role] : []);
|
|
582
|
+
if (!userRoles.includes(options.role)) {
|
|
583
|
+
const { redirect } = await import("next/navigation");
|
|
584
|
+
redirect(loginRoute);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (options?.permission) {
|
|
588
|
+
const { matchPermission: matchPermission2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
|
|
589
|
+
const perms = session.permissions ?? session.user.permissions ?? [];
|
|
590
|
+
if (!matchPermission2(perms, options.permission)) {
|
|
591
|
+
const { redirect } = await import("next/navigation");
|
|
592
|
+
redirect(loginRoute);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return Page({ session, ...props });
|
|
596
|
+
}, "ProtectedPage");
|
|
597
|
+
}, "protect");
|
|
598
|
+
return {
|
|
599
|
+
getSession: getSession2,
|
|
600
|
+
requireSession,
|
|
601
|
+
middleware,
|
|
602
|
+
config: { matcher },
|
|
603
|
+
protect
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
__name(defineAuth, "defineAuth");
|
|
319
607
|
export {
|
|
608
|
+
AuthConfigError,
|
|
609
|
+
AuthTransportError,
|
|
610
|
+
NoSessionError,
|
|
320
611
|
createServerClient,
|
|
612
|
+
defineAuth,
|
|
321
613
|
getServerSession,
|
|
322
614
|
getSession,
|
|
323
615
|
withAuth,
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,19 @@ interface LockoutConfig {
|
|
|
31
31
|
/** Lockout duration (e.g. '15m') */
|
|
32
32
|
duration: string;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Session cookie cache configuration.
|
|
36
|
+
* A short-lived, HMAC-signed cookie that caches user/roles/permissions for
|
|
37
|
+
* instant SSR reads (skips the /auth/me round-trip).
|
|
38
|
+
*/
|
|
39
|
+
interface SessionCookieConfig {
|
|
40
|
+
/** Cookie name (default: 'najm.session') */
|
|
41
|
+
name: string;
|
|
42
|
+
/** Max age in seconds (default: 300 = 5 min) */
|
|
43
|
+
maxAge: number;
|
|
44
|
+
/** Secret used to HMAC-sign the cookie. Defaults to jwt.accessSecret. */
|
|
45
|
+
secret?: string;
|
|
46
|
+
}
|
|
34
47
|
/**
|
|
35
48
|
* Complete auth plugin configuration (internal)
|
|
36
49
|
*/
|
|
@@ -51,6 +64,8 @@ interface AuthConfig {
|
|
|
51
64
|
registrationMode: 'active' | 'pending';
|
|
52
65
|
/** Per-account lockout settings */
|
|
53
66
|
lockout: LockoutConfig;
|
|
67
|
+
/** Session cookie cache settings */
|
|
68
|
+
session: SessionCookieConfig;
|
|
54
69
|
}
|
|
55
70
|
/**
|
|
56
71
|
* Auth plugin configuration options
|
|
@@ -88,6 +103,8 @@ type AuthPluginConfig = {
|
|
|
88
103
|
registrationMode?: 'active' | 'pending';
|
|
89
104
|
/** Per-account lockout settings */
|
|
90
105
|
lockout?: Partial<LockoutConfig>;
|
|
106
|
+
/** Session cookie cache settings (optional — sensible defaults applied) */
|
|
107
|
+
session?: Partial<SessionCookieConfig>;
|
|
91
108
|
/** Optional config forwarded to validation() dependency */
|
|
92
109
|
validation?: ValidationPluginConfig;
|
|
93
110
|
/** Optional config forwarded to rateLimit() dependency */
|
|
@@ -332,15 +349,45 @@ declare class EncryptionService {
|
|
|
332
349
|
comparePassword(password: string, hashedPassword: string): Promise<boolean>;
|
|
333
350
|
}
|
|
334
351
|
|
|
352
|
+
interface SessionCookieData {
|
|
353
|
+
user: {
|
|
354
|
+
id: string;
|
|
355
|
+
email: string;
|
|
356
|
+
name?: string | null;
|
|
357
|
+
role?: string;
|
|
358
|
+
};
|
|
359
|
+
roles: string[];
|
|
360
|
+
permissions: string[];
|
|
361
|
+
/** Epoch ms when the cookie was written */
|
|
362
|
+
iat: number;
|
|
363
|
+
}
|
|
335
364
|
declare class CookieManager {
|
|
336
365
|
private config;
|
|
337
366
|
private cookieService;
|
|
338
367
|
private get cookieName();
|
|
368
|
+
private get sessionCookieName();
|
|
369
|
+
private get sessionMaxAge();
|
|
370
|
+
private get sessionSecret();
|
|
339
371
|
setRefreshToken(refreshToken: string): void;
|
|
340
372
|
clearRefreshToken(): void;
|
|
341
373
|
getRefreshToken(): string | undefined;
|
|
342
374
|
hasRefreshToken(): boolean;
|
|
343
375
|
getCookieName(): string;
|
|
376
|
+
/**
|
|
377
|
+
* Write a signed session cookie containing user data, roles, and permissions.
|
|
378
|
+
* The cookie is HMAC-signed with the access secret so it is tamper-proof
|
|
379
|
+
* but readable without a database query. Short TTL (5 min) ensures freshness.
|
|
380
|
+
*/
|
|
381
|
+
setSessionCookie(data: Omit<SessionCookieData, 'iat'>): void;
|
|
382
|
+
/**
|
|
383
|
+
* Read and verify the session cookie. Returns null if missing, expired,
|
|
384
|
+
* or signature verification fails.
|
|
385
|
+
*/
|
|
386
|
+
getSessionCookie(): SessionCookieData | null;
|
|
387
|
+
/**
|
|
388
|
+
* Clear the session cookie (on logout, password change, etc.)
|
|
389
|
+
*/
|
|
390
|
+
clearSessionCookie(): void;
|
|
344
391
|
}
|
|
345
392
|
|
|
346
393
|
interface UserWithPermissions extends Omit<User, 'password'> {
|
|
@@ -847,6 +894,11 @@ declare class AuthService {
|
|
|
847
894
|
constructor(tokenService: TokenService, userService: UserService, userValidator: UserValidator, cookieManager: CookieManager, i18nService: I18nService, emailService: EmailService);
|
|
848
895
|
private isLockoutActive;
|
|
849
896
|
private nextLockoutUntil;
|
|
897
|
+
/**
|
|
898
|
+
* Decode JWT payload without verification (token was just generated by us).
|
|
899
|
+
* Used to extract accurate roles/permissions for the session cookie cache.
|
|
900
|
+
*/
|
|
901
|
+
private decodeAccessToken;
|
|
850
902
|
registerUser(body: CreateUserDto): Promise<SanitizedUser>;
|
|
851
903
|
loginUser(body: LoginDto): Promise<TokenPair & {
|
|
852
904
|
user: SanitizedUser;
|
|
@@ -865,6 +917,12 @@ declare class AuthService {
|
|
|
865
917
|
/**
|
|
866
918
|
* Get current user — prefer access token (no cookie rotation risk),
|
|
867
919
|
* fall back to cookie when no Authorization header is present.
|
|
920
|
+
*
|
|
921
|
+
* Refreshes the session cookie cache only when authoritative roles/permissions
|
|
922
|
+
* are available (from the access token JWT). When called via the refresh-cookie
|
|
923
|
+
* fallback, we cannot reliably populate permissions without a DB round-trip,
|
|
924
|
+
* so we leave the existing cache alone — it will be refreshed by the next
|
|
925
|
+
* login/refresh cycle.
|
|
868
926
|
*/
|
|
869
927
|
getMe(authorization?: string): Promise<SanitizedUser & {
|
|
870
928
|
language: string;
|
|
@@ -1836,4 +1894,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
|
|
|
1836
1894
|
*/
|
|
1837
1895
|
declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
|
|
1838
1896
|
|
|
1839
|
-
export { AUTH_CONFIG, en as AUTH_EN, AUTH_LOCALES, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthPluginConfig, AuthQueries, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, type AuthUser, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type DefineRolesOptions, type EmailParam, EncryptionService, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, ROLES, ROLE_GROUPS, type RefreshTokenDto, type ResetPasswordDto, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, TOKEN_STATUS, TOKEN_TYPE, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, isAdmin, isAdministrator, isAuth, isEmpty, isFile, isPath, join, languageParam, loginDto, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, resetPasswordDto, revokeTokenDto, roleIdParam, seedAuthData, setConfiguredCookieName, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, verifyTokenDto, where };
|
|
1897
|
+
export { AUTH_CONFIG, en as AUTH_EN, AUTH_LOCALES, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthPluginConfig, AuthQueries, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, type AuthUser, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type DefineRolesOptions, type EmailParam, EncryptionService, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, ROLES, ROLE_GROUPS, type RefreshTokenDto, type ResetPasswordDto, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, TOKEN_STATUS, TOKEN_TYPE, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, isAdmin, isAdministrator, isAuth, isEmpty, isFile, isPath, join, languageParam, loginDto, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, resetPasswordDto, revokeTokenDto, roleIdParam, seedAuthData, setConfiguredCookieName, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, verifyTokenDto, where };
|
package/dist/index.js
CHANGED
|
@@ -1183,6 +1183,18 @@ var CookieManager = class CookieManager2 {
|
|
|
1183
1183
|
get cookieName() {
|
|
1184
1184
|
return this.config.refreshCookieName || "refreshToken";
|
|
1185
1185
|
}
|
|
1186
|
+
get sessionCookieName() {
|
|
1187
|
+
return this.config.session.name;
|
|
1188
|
+
}
|
|
1189
|
+
get sessionMaxAge() {
|
|
1190
|
+
return this.config.session.maxAge;
|
|
1191
|
+
}
|
|
1192
|
+
get sessionSecret() {
|
|
1193
|
+
return this.config.session.secret || this.config.jwt.accessSecret;
|
|
1194
|
+
}
|
|
1195
|
+
// =========================================================================
|
|
1196
|
+
// Refresh Token Cookie
|
|
1197
|
+
// =========================================================================
|
|
1186
1198
|
setRefreshToken(refreshToken) {
|
|
1187
1199
|
const maxAge = timestring(this.config.jwt.refreshExpiresIn, "s");
|
|
1188
1200
|
this.cookieService.set(this.cookieName, refreshToken, { maxAge });
|
|
@@ -1199,6 +1211,47 @@ var CookieManager = class CookieManager2 {
|
|
|
1199
1211
|
getCookieName() {
|
|
1200
1212
|
return this.cookieName;
|
|
1201
1213
|
}
|
|
1214
|
+
// =========================================================================
|
|
1215
|
+
// Session Cookie Cache (HMAC-signed, short-lived)
|
|
1216
|
+
// =========================================================================
|
|
1217
|
+
/**
|
|
1218
|
+
* Write a signed session cookie containing user data, roles, and permissions.
|
|
1219
|
+
* The cookie is HMAC-signed with the access secret so it is tamper-proof
|
|
1220
|
+
* but readable without a database query. Short TTL (5 min) ensures freshness.
|
|
1221
|
+
*/
|
|
1222
|
+
setSessionCookie(data) {
|
|
1223
|
+
const payload = { ...data, iat: Date.now() };
|
|
1224
|
+
this.cookieService.setSigned(this.sessionCookieName, JSON.stringify(payload), this.sessionSecret, {
|
|
1225
|
+
maxAge: this.sessionMaxAge,
|
|
1226
|
+
httpOnly: true,
|
|
1227
|
+
sameSite: "Lax",
|
|
1228
|
+
path: "/"
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Read and verify the session cookie. Returns null if missing, expired,
|
|
1233
|
+
* or signature verification fails.
|
|
1234
|
+
*/
|
|
1235
|
+
getSessionCookie() {
|
|
1236
|
+
const raw = this.cookieService.getSigned(this.sessionCookieName, this.sessionSecret);
|
|
1237
|
+
if (!raw)
|
|
1238
|
+
return null;
|
|
1239
|
+
try {
|
|
1240
|
+
const data = JSON.parse(raw);
|
|
1241
|
+
const age = Date.now() - data.iat;
|
|
1242
|
+
if (age > this.sessionMaxAge * 1e3)
|
|
1243
|
+
return null;
|
|
1244
|
+
return data;
|
|
1245
|
+
} catch {
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Clear the session cookie (on logout, password change, etc.)
|
|
1251
|
+
*/
|
|
1252
|
+
clearSessionCookie() {
|
|
1253
|
+
this.cookieService.delete(this.sessionCookieName);
|
|
1254
|
+
}
|
|
1202
1255
|
};
|
|
1203
1256
|
__decorate8([
|
|
1204
1257
|
Inject4(AUTH_CONFIG),
|
|
@@ -1647,6 +1700,7 @@ TokenService = __decorate10([
|
|
|
1647
1700
|
|
|
1648
1701
|
// src/auth/AuthService.ts
|
|
1649
1702
|
import timestring3 from "timestring";
|
|
1703
|
+
import jwt2 from "jsonwebtoken";
|
|
1650
1704
|
var __decorate11 = function(decorators, target, key, desc) {
|
|
1651
1705
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1652
1706
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1697,6 +1751,21 @@ var AuthService = class AuthService2 {
|
|
|
1697
1751
|
const durationMs = timestring3(this.config.lockout.duration, "ms");
|
|
1698
1752
|
return new Date(Date.now() + durationMs).toISOString();
|
|
1699
1753
|
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Decode JWT payload without verification (token was just generated by us).
|
|
1756
|
+
* Used to extract accurate roles/permissions for the session cookie cache.
|
|
1757
|
+
*/
|
|
1758
|
+
decodeAccessToken(accessToken) {
|
|
1759
|
+
try {
|
|
1760
|
+
const decoded = jwt2.decode(accessToken);
|
|
1761
|
+
return {
|
|
1762
|
+
roles: decoded?.roles ?? [],
|
|
1763
|
+
permissions: decoded?.permissions ?? []
|
|
1764
|
+
};
|
|
1765
|
+
} catch {
|
|
1766
|
+
return { roles: [], permissions: [] };
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1700
1769
|
async registerUser(body) {
|
|
1701
1770
|
return await this.userService.create(body);
|
|
1702
1771
|
}
|
|
@@ -1733,17 +1802,33 @@ var AuthService = class AuthService2 {
|
|
|
1733
1802
|
this.cookieManager.setRefreshToken(data.refreshToken);
|
|
1734
1803
|
await this.userService.updateLastLogin(user.id);
|
|
1735
1804
|
const { password: _, failedLoginAttempts: __, lockoutUntil: ___, ...sanitized } = user;
|
|
1805
|
+
const { roles, permissions } = this.decodeAccessToken(data.accessToken);
|
|
1806
|
+
this.cookieManager.setSessionCookie({
|
|
1807
|
+
user: { id: sanitized.id, email: sanitized.email, name: sanitized.name, role: sanitized.role },
|
|
1808
|
+
roles,
|
|
1809
|
+
permissions
|
|
1810
|
+
});
|
|
1736
1811
|
return { ...data, user: sanitized };
|
|
1737
1812
|
}
|
|
1738
1813
|
async refreshTokens() {
|
|
1739
1814
|
const data = await this.tokenService.refreshTokens();
|
|
1740
1815
|
this.cookieManager.setRefreshToken(data.refreshToken);
|
|
1816
|
+
const user = await this.tokenService.getUser(`Bearer ${data.accessToken}`);
|
|
1817
|
+
if (user) {
|
|
1818
|
+
const { roles, permissions } = this.decodeAccessToken(data.accessToken);
|
|
1819
|
+
this.cookieManager.setSessionCookie({
|
|
1820
|
+
user: { id: user.id, email: user.email, name: user.name, role: user.role },
|
|
1821
|
+
roles,
|
|
1822
|
+
permissions
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1741
1825
|
return data;
|
|
1742
1826
|
}
|
|
1743
1827
|
async logoutUser(userId, authorization) {
|
|
1744
1828
|
await this.userValidator.checkUserExists(userId);
|
|
1745
1829
|
await this.tokenService.logout(userId, authorization);
|
|
1746
1830
|
this.cookieManager.clearRefreshToken();
|
|
1831
|
+
this.cookieManager.clearSessionCookie();
|
|
1747
1832
|
return { data: null, message: this.t("auth.success.logout") };
|
|
1748
1833
|
}
|
|
1749
1834
|
async getUserProfile(userData) {
|
|
@@ -1762,16 +1847,37 @@ var AuthService = class AuthService2 {
|
|
|
1762
1847
|
/**
|
|
1763
1848
|
* Get current user — prefer access token (no cookie rotation risk),
|
|
1764
1849
|
* fall back to cookie when no Authorization header is present.
|
|
1850
|
+
*
|
|
1851
|
+
* Refreshes the session cookie cache only when authoritative roles/permissions
|
|
1852
|
+
* are available (from the access token JWT). When called via the refresh-cookie
|
|
1853
|
+
* fallback, we cannot reliably populate permissions without a DB round-trip,
|
|
1854
|
+
* so we leave the existing cache alone — it will be refreshed by the next
|
|
1855
|
+
* login/refresh cycle.
|
|
1765
1856
|
*/
|
|
1766
1857
|
async getMe(authorization) {
|
|
1858
|
+
let result;
|
|
1859
|
+
let cachePayload = null;
|
|
1767
1860
|
if (authorization) {
|
|
1768
1861
|
const user = await this.tokenService.getUser(authorization);
|
|
1769
1862
|
if (user) {
|
|
1770
1863
|
const lang = this.i18nService.getCurrentLanguage();
|
|
1771
|
-
|
|
1864
|
+
result = { ...user, language: lang };
|
|
1865
|
+
const token = authorization.replace(/^Bearer\s+/i, "");
|
|
1866
|
+
cachePayload = this.decodeAccessToken(token);
|
|
1867
|
+
} else {
|
|
1868
|
+
result = await this.getUserFromCookie();
|
|
1772
1869
|
}
|
|
1870
|
+
} else {
|
|
1871
|
+
result = await this.getUserFromCookie();
|
|
1872
|
+
}
|
|
1873
|
+
if (cachePayload) {
|
|
1874
|
+
this.cookieManager.setSessionCookie({
|
|
1875
|
+
user: { id: result.id, email: result.email, name: result.name, role: result.role },
|
|
1876
|
+
roles: cachePayload.roles,
|
|
1877
|
+
permissions: cachePayload.permissions
|
|
1878
|
+
});
|
|
1773
1879
|
}
|
|
1774
|
-
return
|
|
1880
|
+
return result;
|
|
1775
1881
|
}
|
|
1776
1882
|
async forgotPassword(email2) {
|
|
1777
1883
|
const user = await this.userService.findByEmail(email2);
|
|
@@ -1803,6 +1909,7 @@ var AuthService = class AuthService2 {
|
|
|
1803
1909
|
await this.tokenService.invalidateUserAccessTokens(userId);
|
|
1804
1910
|
await this.tokenService.revokeToken(userId);
|
|
1805
1911
|
this.cookieManager.clearRefreshToken();
|
|
1912
|
+
this.cookieManager.clearSessionCookie();
|
|
1806
1913
|
return { message: this.t("success.passwordChanged") };
|
|
1807
1914
|
}
|
|
1808
1915
|
async resetPassword(token, newPassword) {
|
|
@@ -1812,6 +1919,7 @@ var AuthService = class AuthService2 {
|
|
|
1812
1919
|
await this.tokenService.invalidateUserAccessTokens(userId);
|
|
1813
1920
|
await this.tokenService.revokeToken(userId);
|
|
1814
1921
|
this.cookieManager.clearRefreshToken();
|
|
1922
|
+
this.cookieManager.clearSessionCookie();
|
|
1815
1923
|
return { message: this.t("success.passwordReset") };
|
|
1816
1924
|
}
|
|
1817
1925
|
};
|
|
@@ -4038,6 +4146,12 @@ var mergeConfig = /* @__PURE__ */ __name((config) => {
|
|
|
4038
4146
|
lockout: {
|
|
4039
4147
|
maxAttempts: config?.lockout?.maxAttempts ?? 5,
|
|
4040
4148
|
duration: config?.lockout?.duration ?? "15m"
|
|
4149
|
+
},
|
|
4150
|
+
session: {
|
|
4151
|
+
name: config?.session?.name ?? "najm.session",
|
|
4152
|
+
maxAge: config?.session?.maxAge ?? 300,
|
|
4153
|
+
secret: config?.session?.secret
|
|
4154
|
+
// fallback to jwt.accessSecret at use site
|
|
4041
4155
|
}
|
|
4042
4156
|
};
|
|
4043
4157
|
if (!finalConfig.jwt.accessSecret) {
|