@payez/next-mvp 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/callbacks/jwt.js +305 -305
- package/dist/auth/providers/credentials.js +223 -223
- package/dist/config/vibe-log-transport.d.ts +5 -3
- package/dist/config/vibe-log-transport.js +14 -5
- package/dist/server/auth-guard.d.ts +46 -0
- package/dist/server/auth-guard.js +128 -0
- package/dist/server/decode-session.d.ts +30 -0
- package/dist/server/decode-session.js +78 -0
- package/dist/server/slim-middleware.d.ts +23 -0
- package/dist/server/slim-middleware.js +89 -0
- package/dist/server/with-auth.d.ts +33 -0
- package/dist/server/with-auth.js +59 -0
- package/package.json +21 -1
- package/src/config/vibe-log-transport.ts +17 -3
- package/src/server/auth-guard.ts +170 -0
- package/src/server/decode-session.ts +91 -0
- package/src/server/slim-middleware.ts +117 -0
- package/src/server/with-auth.ts +93 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Server-Side Auth Guard for Layouts
|
|
4
|
+
*
|
|
5
|
+
* Replaces middleware's self-fetch auth checks with direct Redis/function calls.
|
|
6
|
+
* Call from server-component layouts to protect routes.
|
|
7
|
+
*
|
|
8
|
+
* Zero HTTP self-fetches. ~8ms total (Redis + in-memory checks).
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.authGuard = authGuard;
|
|
12
|
+
require("server-only");
|
|
13
|
+
const navigation_1 = require("next/navigation");
|
|
14
|
+
const headers_1 = require("next/headers");
|
|
15
|
+
const decode_session_1 = require("./decode-session");
|
|
16
|
+
const idp_client_config_1 = require("../lib/idp-client-config");
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// CONSTANTS
|
|
19
|
+
// =============================================================================
|
|
20
|
+
const LOGIN_PAGE = '/account-auth/login';
|
|
21
|
+
const VERIFY_CODE_PAGE = '/account-auth/verify-code';
|
|
22
|
+
const SERVICE_UNAVAILABLE_PAGE = '/service-unavailable';
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// MAIN
|
|
25
|
+
// =============================================================================
|
|
26
|
+
/**
|
|
27
|
+
* Server-side auth guard. Call from async server layouts.
|
|
28
|
+
*
|
|
29
|
+
* Redirects (via next/navigation redirect()) if:
|
|
30
|
+
* - No session cookie / invalid JWT
|
|
31
|
+
* - Session not in Redis (stale)
|
|
32
|
+
* - Session force-invalidated
|
|
33
|
+
* - 2FA required but not completed / expired
|
|
34
|
+
* - Any custom check fails
|
|
35
|
+
*
|
|
36
|
+
* Returns the authenticated user's session data on success.
|
|
37
|
+
*/
|
|
38
|
+
async function authGuard(options) {
|
|
39
|
+
const loginUrl = options?.loginUrl || LOGIN_PAGE;
|
|
40
|
+
const verifyCodeUrl = options?.verifyCodeUrl || VERIFY_CODE_PAGE;
|
|
41
|
+
const serviceUnavailableUrl = options?.serviceUnavailableUrl || SERVICE_UNAVAILABLE_PAGE;
|
|
42
|
+
// Get current pathname from headers (set by Next.js)
|
|
43
|
+
const headerStore = await (0, headers_1.headers)();
|
|
44
|
+
const pathname = headerStore.get('x-next-pathname') ||
|
|
45
|
+
headerStore.get('x-invoke-path') ||
|
|
46
|
+
headerStore.get('x-matched-path') ||
|
|
47
|
+
'/';
|
|
48
|
+
const callbackUrl = encodeURIComponent(pathname);
|
|
49
|
+
// --- Decode session (cookie → JWT → Redis) ---
|
|
50
|
+
let decoded;
|
|
51
|
+
try {
|
|
52
|
+
decoded = await (0, decode_session_1.decodeSession)();
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// Redis unreachable or startup failure → fail closed
|
|
56
|
+
console.error('[AUTH-GUARD] Session decode failed (service error):', error instanceof Error ? error.message : String(error));
|
|
57
|
+
(0, navigation_1.redirect)(serviceUnavailableUrl);
|
|
58
|
+
}
|
|
59
|
+
// No session at all → redirect to login
|
|
60
|
+
if (!decoded) {
|
|
61
|
+
(0, navigation_1.redirect)(`${loginUrl}?callbackUrl=${callbackUrl}`);
|
|
62
|
+
}
|
|
63
|
+
const { sessionData } = decoded;
|
|
64
|
+
// --- Force-invalidated session (admin action, password change) ---
|
|
65
|
+
if (sessionData.forceInvalidated) {
|
|
66
|
+
console.warn('[AUTH-GUARD] Session force-invalidated', {
|
|
67
|
+
userId: sessionData.userId,
|
|
68
|
+
pathname,
|
|
69
|
+
});
|
|
70
|
+
(0, navigation_1.redirect)(`${loginUrl}?callbackUrl=${callbackUrl}&reason=invalidated`);
|
|
71
|
+
}
|
|
72
|
+
// --- 2FA check ---
|
|
73
|
+
try {
|
|
74
|
+
const config = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
75
|
+
const requires2FA = config.authSettings?.require2FA ?? true;
|
|
76
|
+
if (requires2FA) {
|
|
77
|
+
const mfaVerified = sessionData.mfaVerified ?? sessionData.twoFactorComplete ?? false;
|
|
78
|
+
const mfaExpiresAt = sessionData.mfaExpiresAt || 0;
|
|
79
|
+
const mfaExpired = mfaExpiresAt > 0 && mfaExpiresAt < Date.now();
|
|
80
|
+
if (!mfaVerified || mfaExpired) {
|
|
81
|
+
console.log('[AUTH-GUARD] 2FA required', {
|
|
82
|
+
mfaVerified,
|
|
83
|
+
mfaExpired,
|
|
84
|
+
userId: sessionData.userId,
|
|
85
|
+
pathname,
|
|
86
|
+
});
|
|
87
|
+
(0, navigation_1.redirect)(`${verifyCodeUrl}?callbackUrl=${callbackUrl}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// If we can't check 2FA config, fail closed
|
|
93
|
+
console.error('[AUTH-GUARD] 2FA config check failed:', error instanceof Error ? error.message : String(error));
|
|
94
|
+
(0, navigation_1.redirect)(serviceUnavailableUrl);
|
|
95
|
+
}
|
|
96
|
+
// --- Custom checks (beta, admin, etc.) ---
|
|
97
|
+
if (options?.checks) {
|
|
98
|
+
for (const check of options.checks) {
|
|
99
|
+
try {
|
|
100
|
+
const redirectUrl = await check.check(sessionData, pathname);
|
|
101
|
+
if (redirectUrl) {
|
|
102
|
+
console.log(`[AUTH-GUARD] Custom check "${check.name}" failed`, {
|
|
103
|
+
userId: sessionData.userId,
|
|
104
|
+
pathname,
|
|
105
|
+
redirectUrl,
|
|
106
|
+
});
|
|
107
|
+
(0, navigation_1.redirect)(redirectUrl);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
// If the error is a redirect (from next/navigation), re-throw it
|
|
112
|
+
if (error && typeof error === 'object' && 'digest' in error) {
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
console.error(`[AUTH-GUARD] Custom check "${check.name}" error:`, error instanceof Error ? error.message : String(error));
|
|
116
|
+
(0, navigation_1.redirect)(serviceUnavailableUrl);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// --- All checks passed ---
|
|
121
|
+
return {
|
|
122
|
+
userId: sessionData.userId,
|
|
123
|
+
email: sessionData.email,
|
|
124
|
+
roles: sessionData.roles || [],
|
|
125
|
+
sessionData,
|
|
126
|
+
accessToken: sessionData.idpAccessToken,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Side Session Decoder
|
|
3
|
+
*
|
|
4
|
+
* Reads the JWT session cookie, decodes it with jose, and fetches the
|
|
5
|
+
* full session from Redis. Used by authGuard (layouts) and withAuth (API routes).
|
|
6
|
+
*
|
|
7
|
+
* Zero HTTP self-fetches. Direct Redis reads only.
|
|
8
|
+
*/
|
|
9
|
+
import 'server-only';
|
|
10
|
+
import { type JWTPayload } from 'jose';
|
|
11
|
+
import { type SessionData } from '../lib/session-store';
|
|
12
|
+
export interface DecodedSession {
|
|
13
|
+
sessionData: SessionData;
|
|
14
|
+
jwtPayload: JWTPayload & {
|
|
15
|
+
sessionToken?: string;
|
|
16
|
+
redisSessionId?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Decode the session from cookies and Redis.
|
|
21
|
+
* Returns null if no valid session exists.
|
|
22
|
+
*
|
|
23
|
+
* @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
|
|
24
|
+
* If omitted, uses next/headers cookies() for server components.
|
|
25
|
+
*/
|
|
26
|
+
export declare function decodeSession(requestCookies?: {
|
|
27
|
+
get: (name: string) => {
|
|
28
|
+
value: string;
|
|
29
|
+
} | undefined;
|
|
30
|
+
}): Promise<DecodedSession | null>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Server-Side Session Decoder
|
|
4
|
+
*
|
|
5
|
+
* Reads the JWT session cookie, decodes it with jose, and fetches the
|
|
6
|
+
* full session from Redis. Used by authGuard (layouts) and withAuth (API routes).
|
|
7
|
+
*
|
|
8
|
+
* Zero HTTP self-fetches. Direct Redis reads only.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.decodeSession = decodeSession;
|
|
12
|
+
require("server-only");
|
|
13
|
+
const headers_1 = require("next/headers");
|
|
14
|
+
const jose_1 = require("jose");
|
|
15
|
+
const session_store_1 = require("../lib/session-store");
|
|
16
|
+
const idp_client_config_1 = require("../lib/idp-client-config");
|
|
17
|
+
const app_slug_1 = require("../lib/app-slug");
|
|
18
|
+
const startup_init_1 = require("../lib/startup-init");
|
|
19
|
+
/**
|
|
20
|
+
* Decode the session from cookies and Redis.
|
|
21
|
+
* Returns null if no valid session exists.
|
|
22
|
+
*
|
|
23
|
+
* @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
|
|
24
|
+
* If omitted, uses next/headers cookies() for server components.
|
|
25
|
+
*/
|
|
26
|
+
async function decodeSession(requestCookies) {
|
|
27
|
+
try {
|
|
28
|
+
// Ensure startup initialization is complete (Redis, IDP config, etc.)
|
|
29
|
+
await (0, startup_init_1.ensureInitialized)();
|
|
30
|
+
// Get the JWT cookie value
|
|
31
|
+
const cookieStore = requestCookies || (await (0, headers_1.cookies)());
|
|
32
|
+
const sessionCookieName = (0, app_slug_1.getSessionCookieName)();
|
|
33
|
+
const secureCookieName = (0, app_slug_1.getSecureSessionCookieName)();
|
|
34
|
+
const cookieValue = cookieStore.get(secureCookieName)?.value ||
|
|
35
|
+
cookieStore.get(sessionCookieName)?.value;
|
|
36
|
+
if (!cookieValue) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
// Get the NextAuth secret from IDP config
|
|
40
|
+
const config = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
41
|
+
const secret = config.nextAuthSecret;
|
|
42
|
+
if (!secret) {
|
|
43
|
+
console.error('[DECODE-SESSION] No nextAuthSecret available from IDP config');
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
// Decode the JWT (same pattern as test-aware-get-token.ts)
|
|
47
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
48
|
+
let payload;
|
|
49
|
+
try {
|
|
50
|
+
const result = await (0, jose_1.jwtVerify)(cookieValue, secretKey);
|
|
51
|
+
payload = result.payload;
|
|
52
|
+
}
|
|
53
|
+
catch (jwtError) {
|
|
54
|
+
// JWT decode failed - cookie may be corrupted or secret rotated
|
|
55
|
+
console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// Extract the Redis session ID from JWT payload
|
|
59
|
+
const sessionToken = payload.sessionToken || payload.redisSessionId;
|
|
60
|
+
if (!sessionToken) {
|
|
61
|
+
console.warn('[DECODE-SESSION] JWT payload missing sessionToken/redisSessionId');
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// Fetch session from Redis (direct, no HTTP)
|
|
65
|
+
const sessionData = await (0, session_store_1.getSession)(sessionToken);
|
|
66
|
+
if (!sessionData) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
sessionData,
|
|
71
|
+
jwtPayload: payload,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error('[DECODE-SESSION] Error:', error instanceof Error ? error.message : String(error));
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slim Middleware — Cookie-Only Auth Check
|
|
3
|
+
*
|
|
4
|
+
* Replaces the self-fetching middleware with a cookie existence check.
|
|
5
|
+
* All real auth validation happens in server-side layouts (authGuard).
|
|
6
|
+
*
|
|
7
|
+
* Zero self-fetches. Zero Redis calls. Zero JWT decoding.
|
|
8
|
+
* Just: does the session cookie exist? Yes → pass through. No → redirect to login.
|
|
9
|
+
*/
|
|
10
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
11
|
+
export interface SlimMiddlewareOptions {
|
|
12
|
+
/** Routes that don't require authentication (glob-style patterns) */
|
|
13
|
+
publicRoutes?: string[];
|
|
14
|
+
/** Login page URL (default: /account-auth/login) */
|
|
15
|
+
loginUrl?: string;
|
|
16
|
+
/** Additional paths to always bypass (e.g., /api/auth/, /api/session/) */
|
|
17
|
+
bypassPrefixes?: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create a slim middleware that only checks cookie existence.
|
|
21
|
+
* Auth validation is deferred to server-side layouts (authGuard).
|
|
22
|
+
*/
|
|
23
|
+
export declare function createSlimMiddleware(options?: SlimMiddlewareOptions): (request: NextRequest) => NextResponse;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Slim Middleware — Cookie-Only Auth Check
|
|
4
|
+
*
|
|
5
|
+
* Replaces the self-fetching middleware with a cookie existence check.
|
|
6
|
+
* All real auth validation happens in server-side layouts (authGuard).
|
|
7
|
+
*
|
|
8
|
+
* Zero self-fetches. Zero Redis calls. Zero JWT decoding.
|
|
9
|
+
* Just: does the session cookie exist? Yes → pass through. No → redirect to login.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createSlimMiddleware = createSlimMiddleware;
|
|
13
|
+
const server_1 = require("next/server");
|
|
14
|
+
const app_slug_1 = require("../lib/app-slug");
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// DEFAULT BYPASS PATHS
|
|
17
|
+
// =============================================================================
|
|
18
|
+
/** Routes that must always bypass middleware (prevent infinite loops) */
|
|
19
|
+
const DEFAULT_BYPASS_PREFIXES = [
|
|
20
|
+
'/api/auth/',
|
|
21
|
+
'/api/session/',
|
|
22
|
+
'/_next/',
|
|
23
|
+
'/favicon.ico',
|
|
24
|
+
];
|
|
25
|
+
/** Static file extensions to bypass */
|
|
26
|
+
const STATIC_EXTENSIONS = /\.(svg|png|jpg|jpeg|gif|webp|ico|css|js|woff|woff2|ttf|eot|map)$/i;
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// MAIN
|
|
29
|
+
// =============================================================================
|
|
30
|
+
/**
|
|
31
|
+
* Create a slim middleware that only checks cookie existence.
|
|
32
|
+
* Auth validation is deferred to server-side layouts (authGuard).
|
|
33
|
+
*/
|
|
34
|
+
function createSlimMiddleware(options) {
|
|
35
|
+
const publicRoutes = options?.publicRoutes || [];
|
|
36
|
+
const loginUrl = options?.loginUrl || '/account-auth/login';
|
|
37
|
+
const extraBypass = options?.bypassPrefixes || [];
|
|
38
|
+
const allBypass = [...DEFAULT_BYPASS_PREFIXES, ...extraBypass];
|
|
39
|
+
// Pre-compile public route patterns for fast matching
|
|
40
|
+
const publicMatchers = publicRoutes.map(pattern => {
|
|
41
|
+
if (pattern.endsWith('/*')) {
|
|
42
|
+
const prefix = pattern.slice(0, -2);
|
|
43
|
+
return (p) => p === prefix || p.startsWith(prefix + '/');
|
|
44
|
+
}
|
|
45
|
+
if (pattern.endsWith('*')) {
|
|
46
|
+
const prefix = pattern.slice(0, -1);
|
|
47
|
+
return (p) => p.startsWith(prefix);
|
|
48
|
+
}
|
|
49
|
+
if (pattern.startsWith('/*.')) {
|
|
50
|
+
const ext = pattern.slice(2);
|
|
51
|
+
return (p) => p.endsWith(ext);
|
|
52
|
+
}
|
|
53
|
+
return (p) => p === pattern;
|
|
54
|
+
});
|
|
55
|
+
return function middleware(request) {
|
|
56
|
+
const { pathname } = request.nextUrl;
|
|
57
|
+
// 1. Always bypass static/internal routes
|
|
58
|
+
if (STATIC_EXTENSIONS.test(pathname)) {
|
|
59
|
+
return server_1.NextResponse.next();
|
|
60
|
+
}
|
|
61
|
+
for (const prefix of allBypass) {
|
|
62
|
+
if (pathname.startsWith(prefix)) {
|
|
63
|
+
return server_1.NextResponse.next();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 2. Check if it's a public route → pass through
|
|
67
|
+
for (const matcher of publicMatchers) {
|
|
68
|
+
if (matcher(pathname)) {
|
|
69
|
+
return server_1.NextResponse.next();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 3. THE ONLY AUTH CHECK: Does a session cookie exist?
|
|
73
|
+
const sessionCookieName = (0, app_slug_1.getSessionCookieName)();
|
|
74
|
+
const secureCookieName = (0, app_slug_1.getSecureSessionCookieName)();
|
|
75
|
+
const hasCookie = request.cookies.has(sessionCookieName) ||
|
|
76
|
+
request.cookies.has(secureCookieName);
|
|
77
|
+
if (!hasCookie) {
|
|
78
|
+
// No cookie on a protected route → redirect to login
|
|
79
|
+
// API routes get 401 instead of redirect
|
|
80
|
+
if (pathname.startsWith('/api/')) {
|
|
81
|
+
return server_1.NextResponse.json({ error: 'Unauthorized', message: 'No session' }, { status: 401 });
|
|
82
|
+
}
|
|
83
|
+
const callbackUrl = encodeURIComponent(pathname);
|
|
84
|
+
return server_1.NextResponse.redirect(new URL(`${loginUrl}?callbackUrl=${callbackUrl}`, request.url));
|
|
85
|
+
}
|
|
86
|
+
// Cookie exists → pass through, layout authGuard does the real validation
|
|
87
|
+
return server_1.NextResponse.next();
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Side Auth Wrapper for API Routes & Server Actions
|
|
3
|
+
*
|
|
4
|
+
* Wraps route handlers with session validation. Uses direct Redis reads.
|
|
5
|
+
* Zero HTTP self-fetches.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* export const GET = withAuth(async (req, auth) => {
|
|
9
|
+
* return NextResponse.json({ userId: auth.userId });
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* // With role requirement:
|
|
13
|
+
* export const POST = withAuth(async (req, auth) => { ... }, { requiredRoles: ['admin'] });
|
|
14
|
+
*/
|
|
15
|
+
import 'server-only';
|
|
16
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
17
|
+
import type { SessionData } from '../lib/session-store';
|
|
18
|
+
export interface ApiAuthResult {
|
|
19
|
+
userId: string;
|
|
20
|
+
email: string;
|
|
21
|
+
roles: string[];
|
|
22
|
+
sessionData: SessionData;
|
|
23
|
+
accessToken?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface WithAuthOptions {
|
|
26
|
+
/** Roles required to access the route (any match = allowed) */
|
|
27
|
+
requiredRoles?: string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Wrap an API route handler with auth validation.
|
|
31
|
+
* Returns 401 if not authenticated, 403 if missing required roles.
|
|
32
|
+
*/
|
|
33
|
+
export declare function withAuth(handler: (req: NextRequest, auth: ApiAuthResult) => Promise<NextResponse>, options?: WithAuthOptions): (req: NextRequest) => Promise<NextResponse>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Server-Side Auth Wrapper for API Routes & Server Actions
|
|
4
|
+
*
|
|
5
|
+
* Wraps route handlers with session validation. Uses direct Redis reads.
|
|
6
|
+
* Zero HTTP self-fetches.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* export const GET = withAuth(async (req, auth) => {
|
|
10
|
+
* return NextResponse.json({ userId: auth.userId });
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* // With role requirement:
|
|
14
|
+
* export const POST = withAuth(async (req, auth) => { ... }, { requiredRoles: ['admin'] });
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.withAuth = withAuth;
|
|
18
|
+
require("server-only");
|
|
19
|
+
const server_1 = require("next/server");
|
|
20
|
+
const decode_session_1 = require("./decode-session");
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// MAIN
|
|
23
|
+
// =============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Wrap an API route handler with auth validation.
|
|
26
|
+
* Returns 401 if not authenticated, 403 if missing required roles.
|
|
27
|
+
*/
|
|
28
|
+
function withAuth(handler, options) {
|
|
29
|
+
return async (req) => {
|
|
30
|
+
try {
|
|
31
|
+
// Decode session from request cookies (direct Redis, no self-fetch)
|
|
32
|
+
const decoded = await (0, decode_session_1.decodeSession)(req.cookies);
|
|
33
|
+
if (!decoded) {
|
|
34
|
+
return server_1.NextResponse.json({ error: 'Unauthorized', message: 'No valid session' }, { status: 401 });
|
|
35
|
+
}
|
|
36
|
+
const { sessionData } = decoded;
|
|
37
|
+
// Check required roles
|
|
38
|
+
if (options?.requiredRoles && options.requiredRoles.length > 0) {
|
|
39
|
+
const userRoles = sessionData.roles || [];
|
|
40
|
+
const hasRole = options.requiredRoles.some(r => userRoles.includes(r));
|
|
41
|
+
if (!hasRole) {
|
|
42
|
+
return server_1.NextResponse.json({ error: 'Forbidden', message: 'Insufficient permissions' }, { status: 403 });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const auth = {
|
|
46
|
+
userId: sessionData.userId,
|
|
47
|
+
email: sessionData.email,
|
|
48
|
+
roles: sessionData.roles || [],
|
|
49
|
+
sessionData,
|
|
50
|
+
accessToken: sessionData.idpAccessToken,
|
|
51
|
+
};
|
|
52
|
+
return handler(req, auth);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error('[WITH-AUTH] Error:', error instanceof Error ? error.message : String(error));
|
|
56
|
+
return server_1.NextResponse.json({ error: 'Internal Server Error', message: 'Auth check failed' }, { status: 500 });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payez/next-mvp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -821,6 +821,26 @@
|
|
|
821
821
|
"types": "./dist/pages/coming-soon/page.d.ts",
|
|
822
822
|
"require": "./dist/pages/coming-soon/page.js",
|
|
823
823
|
"default": "./dist/pages/coming-soon/page.js"
|
|
824
|
+
},
|
|
825
|
+
"./server/auth-guard": {
|
|
826
|
+
"types": "./dist/server/auth-guard.d.ts",
|
|
827
|
+
"require": "./dist/server/auth-guard.js",
|
|
828
|
+
"default": "./dist/server/auth-guard.js"
|
|
829
|
+
},
|
|
830
|
+
"./server/with-auth": {
|
|
831
|
+
"types": "./dist/server/with-auth.d.ts",
|
|
832
|
+
"require": "./dist/server/with-auth.js",
|
|
833
|
+
"default": "./dist/server/with-auth.js"
|
|
834
|
+
},
|
|
835
|
+
"./server/slim-middleware": {
|
|
836
|
+
"types": "./dist/server/slim-middleware.d.ts",
|
|
837
|
+
"require": "./dist/server/slim-middleware.js",
|
|
838
|
+
"default": "./dist/server/slim-middleware.js"
|
|
839
|
+
},
|
|
840
|
+
"./server/decode-session": {
|
|
841
|
+
"types": "./dist/server/decode-session.d.ts",
|
|
842
|
+
"require": "./dist/server/decode-session.js",
|
|
843
|
+
"default": "./dist/server/decode-session.js"
|
|
824
844
|
}
|
|
825
845
|
},
|
|
826
846
|
"peerDependencies": {
|
|
@@ -11,15 +11,29 @@
|
|
|
11
11
|
* - Graceful degradation on failure
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import Transport from 'winston-transport';
|
|
15
14
|
import { getRedis } from '../lib/redis';
|
|
16
15
|
|
|
16
|
+
// Dynamic import — winston is a peerDependency and may not be installed.
|
|
17
|
+
// We resolve the base class lazily so builds don't break without it.
|
|
18
|
+
let TransportBase: any;
|
|
19
|
+
try {
|
|
20
|
+
TransportBase = require('winston-transport');
|
|
21
|
+
} catch {
|
|
22
|
+
// Fallback: minimal base class for when winston is not installed
|
|
23
|
+
TransportBase = class {
|
|
24
|
+
constructor(_opts?: any) {}
|
|
25
|
+
emit(_event: string, _info: any) {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
/** Redis key for pending log entries */
|
|
18
30
|
const REDIS_LOG_KEY = 'vibe:logs:pending';
|
|
19
31
|
/** TTL in seconds: 1 week */
|
|
20
32
|
const REDIS_LOG_TTL = 7 * 24 * 60 * 60;
|
|
21
33
|
|
|
22
|
-
export interface VibeLogTransportOptions
|
|
34
|
+
export interface VibeLogTransportOptions {
|
|
35
|
+
/** Winston transport level */
|
|
36
|
+
level?: string;
|
|
23
37
|
/** Redis URL (optional, uses REDIS_URL env var by default) */
|
|
24
38
|
redisUrl?: string;
|
|
25
39
|
/** Vibe client ID (for log metadata) */
|
|
@@ -55,7 +69,7 @@ const LEVEL_ORDER: Record<string, number> = {
|
|
|
55
69
|
/**
|
|
56
70
|
* Winston transport that buffers logs to Redis for Vibe drain processing
|
|
57
71
|
*/
|
|
58
|
-
export class VibeLogTransport extends
|
|
72
|
+
export class VibeLogTransport extends TransportBase {
|
|
59
73
|
private vibeClientId: string;
|
|
60
74
|
private appSlug: string;
|
|
61
75
|
private minLevelNum: number;
|