najm-auth 1.1.19 → 1.1.21
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 +63 -3
- package/dist/index.js +126 -11
- 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'> {
|
|
@@ -683,8 +730,10 @@ declare class TokenService {
|
|
|
683
730
|
validateRefreshSession(refreshToken: string): Promise<string>;
|
|
684
731
|
/**
|
|
685
732
|
* Read the refresh cookie and return the userId it belongs to.
|
|
686
|
-
*
|
|
687
|
-
*
|
|
733
|
+
* Lightweight check: verifies JWT signature and ensures the user
|
|
734
|
+
* has an active session in the DB. Does NOT compare token hashes,
|
|
735
|
+
* so it is safe to call concurrently with token rotation.
|
|
736
|
+
* Throws if the cookie is missing, invalid, or the session was revoked.
|
|
688
737
|
*/
|
|
689
738
|
resolveUserFromCookie(): Promise<string>;
|
|
690
739
|
getUser(auth: string): Promise<any>;
|
|
@@ -845,6 +894,11 @@ declare class AuthService {
|
|
|
845
894
|
constructor(tokenService: TokenService, userService: UserService, userValidator: UserValidator, cookieManager: CookieManager, i18nService: I18nService, emailService: EmailService);
|
|
846
895
|
private isLockoutActive;
|
|
847
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;
|
|
848
902
|
registerUser(body: CreateUserDto): Promise<SanitizedUser>;
|
|
849
903
|
loginUser(body: LoginDto): Promise<TokenPair & {
|
|
850
904
|
user: SanitizedUser;
|
|
@@ -863,6 +917,12 @@ declare class AuthService {
|
|
|
863
917
|
/**
|
|
864
918
|
* Get current user — prefer access token (no cookie rotation risk),
|
|
865
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.
|
|
866
926
|
*/
|
|
867
927
|
getMe(authorization?: string): Promise<SanitizedUser & {
|
|
868
928
|
language: string;
|
|
@@ -1834,4 +1894,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
|
|
|
1834
1894
|
*/
|
|
1835
1895
|
declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
|
|
1836
1896
|
|
|
1837
|
-
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),
|
|
@@ -1408,24 +1461,28 @@ var TokenService = class TokenService2 {
|
|
|
1408
1461
|
}
|
|
1409
1462
|
const isValid = this.hashToken(refreshToken) === stored.token;
|
|
1410
1463
|
if (!isValid) {
|
|
1411
|
-
if (stored.tokenFamily) {
|
|
1412
|
-
await this.tokenRepository.revokeByFamily(stored.tokenFamily);
|
|
1413
|
-
}
|
|
1414
1464
|
Err5(this.t("errors.refreshTokenInvalid"));
|
|
1415
1465
|
}
|
|
1416
1466
|
return userId;
|
|
1417
1467
|
}
|
|
1418
1468
|
/**
|
|
1419
1469
|
* Read the refresh cookie and return the userId it belongs to.
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1470
|
+
* Lightweight check: verifies JWT signature and ensures the user
|
|
1471
|
+
* has an active session in the DB. Does NOT compare token hashes,
|
|
1472
|
+
* so it is safe to call concurrently with token rotation.
|
|
1473
|
+
* Throws if the cookie is missing, invalid, or the session was revoked.
|
|
1422
1474
|
*/
|
|
1423
1475
|
async resolveUserFromCookie() {
|
|
1424
1476
|
const refreshToken = this.cookieManager.getRefreshToken();
|
|
1425
1477
|
if (!refreshToken) {
|
|
1426
1478
|
Err5(this.t("errors.refreshTokenMissing"));
|
|
1427
1479
|
}
|
|
1428
|
-
|
|
1480
|
+
const userId = this.verifyRefreshToken(refreshToken);
|
|
1481
|
+
const stored = await this.tokenRepository.getRefreshTokenWithFamily(userId);
|
|
1482
|
+
if (!stored) {
|
|
1483
|
+
Err5(this.t("errors.refreshTokenInvalid"));
|
|
1484
|
+
}
|
|
1485
|
+
return userId;
|
|
1429
1486
|
}
|
|
1430
1487
|
// ============ USER RETRIEVAL (MAIN METHOD) ============
|
|
1431
1488
|
async getUser(auth2) {
|
|
@@ -1552,9 +1609,6 @@ var TokenService = class TokenService2 {
|
|
|
1552
1609
|
}
|
|
1553
1610
|
const isValid = this.hashToken(refreshToken) === stored.token;
|
|
1554
1611
|
if (!isValid) {
|
|
1555
|
-
if (stored.tokenFamily) {
|
|
1556
|
-
await this.tokenRepository.revokeByFamily(stored.tokenFamily);
|
|
1557
|
-
}
|
|
1558
1612
|
Err5(this.t("errors.refreshTokenInvalid"));
|
|
1559
1613
|
}
|
|
1560
1614
|
return this.generateTokens(userId, stored.tokenFamily ?? void 0);
|
|
@@ -1646,6 +1700,7 @@ TokenService = __decorate10([
|
|
|
1646
1700
|
|
|
1647
1701
|
// src/auth/AuthService.ts
|
|
1648
1702
|
import timestring3 from "timestring";
|
|
1703
|
+
import jwt2 from "jsonwebtoken";
|
|
1649
1704
|
var __decorate11 = function(decorators, target, key, desc) {
|
|
1650
1705
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1651
1706
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1696,6 +1751,21 @@ var AuthService = class AuthService2 {
|
|
|
1696
1751
|
const durationMs = timestring3(this.config.lockout.duration, "ms");
|
|
1697
1752
|
return new Date(Date.now() + durationMs).toISOString();
|
|
1698
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
|
+
}
|
|
1699
1769
|
async registerUser(body) {
|
|
1700
1770
|
return await this.userService.create(body);
|
|
1701
1771
|
}
|
|
@@ -1732,17 +1802,33 @@ var AuthService = class AuthService2 {
|
|
|
1732
1802
|
this.cookieManager.setRefreshToken(data.refreshToken);
|
|
1733
1803
|
await this.userService.updateLastLogin(user.id);
|
|
1734
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
|
+
});
|
|
1735
1811
|
return { ...data, user: sanitized };
|
|
1736
1812
|
}
|
|
1737
1813
|
async refreshTokens() {
|
|
1738
1814
|
const data = await this.tokenService.refreshTokens();
|
|
1739
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
|
+
}
|
|
1740
1825
|
return data;
|
|
1741
1826
|
}
|
|
1742
1827
|
async logoutUser(userId, authorization) {
|
|
1743
1828
|
await this.userValidator.checkUserExists(userId);
|
|
1744
1829
|
await this.tokenService.logout(userId, authorization);
|
|
1745
1830
|
this.cookieManager.clearRefreshToken();
|
|
1831
|
+
this.cookieManager.clearSessionCookie();
|
|
1746
1832
|
return { data: null, message: this.t("auth.success.logout") };
|
|
1747
1833
|
}
|
|
1748
1834
|
async getUserProfile(userData) {
|
|
@@ -1761,16 +1847,37 @@ var AuthService = class AuthService2 {
|
|
|
1761
1847
|
/**
|
|
1762
1848
|
* Get current user — prefer access token (no cookie rotation risk),
|
|
1763
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.
|
|
1764
1856
|
*/
|
|
1765
1857
|
async getMe(authorization) {
|
|
1858
|
+
let result;
|
|
1859
|
+
let cachePayload = null;
|
|
1766
1860
|
if (authorization) {
|
|
1767
1861
|
const user = await this.tokenService.getUser(authorization);
|
|
1768
1862
|
if (user) {
|
|
1769
1863
|
const lang = this.i18nService.getCurrentLanguage();
|
|
1770
|
-
|
|
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();
|
|
1771
1869
|
}
|
|
1870
|
+
} else {
|
|
1871
|
+
result = await this.getUserFromCookie();
|
|
1772
1872
|
}
|
|
1773
|
-
|
|
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
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
return result;
|
|
1774
1881
|
}
|
|
1775
1882
|
async forgotPassword(email2) {
|
|
1776
1883
|
const user = await this.userService.findByEmail(email2);
|
|
@@ -1802,6 +1909,7 @@ var AuthService = class AuthService2 {
|
|
|
1802
1909
|
await this.tokenService.invalidateUserAccessTokens(userId);
|
|
1803
1910
|
await this.tokenService.revokeToken(userId);
|
|
1804
1911
|
this.cookieManager.clearRefreshToken();
|
|
1912
|
+
this.cookieManager.clearSessionCookie();
|
|
1805
1913
|
return { message: this.t("success.passwordChanged") };
|
|
1806
1914
|
}
|
|
1807
1915
|
async resetPassword(token, newPassword) {
|
|
@@ -1811,6 +1919,7 @@ var AuthService = class AuthService2 {
|
|
|
1811
1919
|
await this.tokenService.invalidateUserAccessTokens(userId);
|
|
1812
1920
|
await this.tokenService.revokeToken(userId);
|
|
1813
1921
|
this.cookieManager.clearRefreshToken();
|
|
1922
|
+
this.cookieManager.clearSessionCookie();
|
|
1814
1923
|
return { message: this.t("success.passwordReset") };
|
|
1815
1924
|
}
|
|
1816
1925
|
};
|
|
@@ -4037,6 +4146,12 @@ var mergeConfig = /* @__PURE__ */ __name((config) => {
|
|
|
4037
4146
|
lockout: {
|
|
4038
4147
|
maxAttempts: config?.lockout?.maxAttempts ?? 5,
|
|
4039
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
|
|
4040
4155
|
}
|
|
4041
4156
|
};
|
|
4042
4157
|
if (!finalConfig.jwt.accessSecret) {
|