create-brainerce-store 1.17.0 → 1.19.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/index.js +31 -9
- package/messages/en.json +366 -359
- package/messages/he.json +366 -359
- package/package.json +45 -45
- package/templates/nextjs/base/next.config.ts +31 -31
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +81 -81
- package/templates/nextjs/base/src/app/.well-known/apple-developer-merchantid-domain-association/route.ts +26 -26
- package/templates/nextjs/base/src/app/account/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/account/page.tsx +122 -122
- package/templates/nextjs/base/src/app/api/auth/logout/route.ts +14 -14
- package/templates/nextjs/base/src/app/api/auth/me/route.ts +56 -56
- package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +59 -59
- package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +41 -41
- package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +77 -77
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +198 -198
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +92 -92
- package/templates/nextjs/base/src/app/cart/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/cart/page.tsx +204 -199
- package/templates/nextjs/base/src/app/checkout/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/checkout/page.tsx +860 -860
- package/templates/nextjs/base/src/app/forgot-password/page.tsx +112 -112
- package/templates/nextjs/base/src/app/layout.tsx.ejs +75 -0
- package/templates/nextjs/base/src/app/login/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/login/page.tsx +59 -59
- package/templates/nextjs/base/src/app/order-confirmation/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +254 -254
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +67 -67
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +486 -486
- package/templates/nextjs/base/src/app/products/layout.tsx +18 -18
- package/templates/nextjs/base/src/app/products/page.tsx +431 -431
- package/templates/nextjs/base/src/app/register/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/register/page.tsx +65 -65
- package/templates/nextjs/base/src/app/reset-password/page.tsx +132 -132
- package/templates/nextjs/base/src/app/robots.ts +14 -14
- package/templates/nextjs/base/src/app/sitemap.ts +25 -25
- package/templates/nextjs/base/src/app/verify-email/page.tsx +258 -258
- package/templates/nextjs/base/src/components/account/address-book.tsx +432 -432
- package/templates/nextjs/base/src/components/account/order-history.tsx +350 -350
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
- package/templates/nextjs/base/src/components/auth/register-form.tsx +232 -232
- package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +247 -111
- package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
- package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +142 -142
- package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +59 -59
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +415 -415
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +243 -83
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +519 -473
- package/templates/nextjs/base/src/components/layout/footer.tsx +41 -41
- package/templates/nextjs/base/src/components/layout/header.tsx +336 -336
- package/templates/nextjs/base/src/components/layout/language-switcher.tsx.ejs +63 -0
- package/templates/nextjs/base/src/components/products/discount-badge.tsx +22 -22
- package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +202 -202
- package/templates/nextjs/base/src/components/products/product-card.tsx +218 -218
- package/templates/nextjs/base/src/components/products/recommendation-section.tsx +107 -107
- package/templates/nextjs/base/src/components/products/stock-badge.tsx +63 -63
- package/templates/nextjs/base/src/components/products/variant-selector.tsx +292 -292
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +72 -72
- package/templates/nextjs/base/src/i18n.ts.ejs +21 -0
- package/templates/nextjs/base/src/lib/auth.ts +149 -149
- package/templates/nextjs/base/src/lib/brainerce.ts.ejs +9 -0
- package/templates/nextjs/base/src/lib/translations.ts.ejs +31 -0
- package/templates/nextjs/base/src/middleware.ts.ejs +81 -0
- package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +41 -0
- package/templates/nextjs/base/src/lib/translations.ts +0 -11
- package/templates/nextjs/base/src/middleware.ts +0 -25
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { cookies, headers } from 'next/headers';
|
|
3
|
-
|
|
4
|
-
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
5
|
-
/\/$/,
|
|
6
|
-
''
|
|
7
|
-
);
|
|
8
|
-
|
|
9
|
-
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
|
|
10
|
-
|
|
11
|
-
const RESET_TOKEN_COOKIE = 'brainerce_reset_token';
|
|
12
|
-
const CSRF_HEADER = 'x-requested-with';
|
|
13
|
-
const CSRF_VALUE = 'brainerce';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* BFF endpoint for password reset.
|
|
17
|
-
* Reads the reset token from the httpOnly cookie (set by /api/auth/reset-callback)
|
|
18
|
-
* and proxies the request to the backend. The token never touches client JS.
|
|
19
|
-
*/
|
|
20
|
-
export async function POST(request: NextRequest) {
|
|
21
|
-
// CSRF check
|
|
22
|
-
const csrfHeader = request.headers.get(CSRF_HEADER);
|
|
23
|
-
if (csrfHeader !== CSRF_VALUE) {
|
|
24
|
-
return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Read reset token from httpOnly cookie
|
|
28
|
-
const cookieStore = await cookies();
|
|
29
|
-
const resetTokenCookie = cookieStore.get(RESET_TOKEN_COOKIE);
|
|
30
|
-
|
|
31
|
-
if (!resetTokenCookie?.value) {
|
|
32
|
-
return NextResponse.json(
|
|
33
|
-
{ error: 'No reset token found. Please request a new password reset link.' },
|
|
34
|
-
{ status: 400 }
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Parse request body
|
|
39
|
-
const body = await request.json();
|
|
40
|
-
const { newPassword } = body;
|
|
41
|
-
|
|
42
|
-
if (!newPassword) {
|
|
43
|
-
return NextResponse.json({ error: 'New password is required' }, { status: 400 });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Derive Origin from the incoming request so the backend's BrowserOriginGuard accepts it
|
|
47
|
-
const requestHeaders = await headers();
|
|
48
|
-
const host = requestHeaders.get('host') || 'localhost:3000';
|
|
49
|
-
const proto = requestHeaders.get('x-forwarded-proto') || 'http';
|
|
50
|
-
const origin = requestHeaders.get('origin') || `${proto}://${host}`;
|
|
51
|
-
|
|
52
|
-
// Proxy to backend
|
|
53
|
-
const backendUrl = `${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/reset-password`;
|
|
54
|
-
|
|
55
|
-
const backendResponse = await fetch(backendUrl, {
|
|
56
|
-
method: 'POST',
|
|
57
|
-
headers: {
|
|
58
|
-
'Content-Type': 'application/json',
|
|
59
|
-
Origin: origin,
|
|
60
|
-
},
|
|
61
|
-
body: JSON.stringify({
|
|
62
|
-
token: resetTokenCookie.value,
|
|
63
|
-
newPassword,
|
|
64
|
-
}),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const data = await backendResponse.json();
|
|
68
|
-
|
|
69
|
-
const response = NextResponse.json(data, {
|
|
70
|
-
status: backendResponse.status,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Always clear the reset token cookie after use (success or failure)
|
|
74
|
-
response.cookies.delete(RESET_TOKEN_COOKIE);
|
|
75
|
-
|
|
76
|
-
return response;
|
|
77
|
-
}
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { cookies, headers } from 'next/headers';
|
|
3
|
+
|
|
4
|
+
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
5
|
+
/\/$/,
|
|
6
|
+
''
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
|
|
10
|
+
|
|
11
|
+
const RESET_TOKEN_COOKIE = 'brainerce_reset_token';
|
|
12
|
+
const CSRF_HEADER = 'x-requested-with';
|
|
13
|
+
const CSRF_VALUE = 'brainerce';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* BFF endpoint for password reset.
|
|
17
|
+
* Reads the reset token from the httpOnly cookie (set by /api/auth/reset-callback)
|
|
18
|
+
* and proxies the request to the backend. The token never touches client JS.
|
|
19
|
+
*/
|
|
20
|
+
export async function POST(request: NextRequest) {
|
|
21
|
+
// CSRF check
|
|
22
|
+
const csrfHeader = request.headers.get(CSRF_HEADER);
|
|
23
|
+
if (csrfHeader !== CSRF_VALUE) {
|
|
24
|
+
return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Read reset token from httpOnly cookie
|
|
28
|
+
const cookieStore = await cookies();
|
|
29
|
+
const resetTokenCookie = cookieStore.get(RESET_TOKEN_COOKIE);
|
|
30
|
+
|
|
31
|
+
if (!resetTokenCookie?.value) {
|
|
32
|
+
return NextResponse.json(
|
|
33
|
+
{ error: 'No reset token found. Please request a new password reset link.' },
|
|
34
|
+
{ status: 400 }
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Parse request body
|
|
39
|
+
const body = await request.json();
|
|
40
|
+
const { newPassword } = body;
|
|
41
|
+
|
|
42
|
+
if (!newPassword) {
|
|
43
|
+
return NextResponse.json({ error: 'New password is required' }, { status: 400 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Derive Origin from the incoming request so the backend's BrowserOriginGuard accepts it
|
|
47
|
+
const requestHeaders = await headers();
|
|
48
|
+
const host = requestHeaders.get('host') || 'localhost:3000';
|
|
49
|
+
const proto = requestHeaders.get('x-forwarded-proto') || 'http';
|
|
50
|
+
const origin = requestHeaders.get('origin') || `${proto}://${host}`;
|
|
51
|
+
|
|
52
|
+
// Proxy to backend
|
|
53
|
+
const backendUrl = `${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/reset-password`;
|
|
54
|
+
|
|
55
|
+
const backendResponse = await fetch(backendUrl, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
Origin: origin,
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
token: resetTokenCookie.value,
|
|
63
|
+
newPassword,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const data = await backendResponse.json();
|
|
68
|
+
|
|
69
|
+
const response = NextResponse.json(data, {
|
|
70
|
+
status: backendResponse.status,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Always clear the reset token cookie after use (success or failure)
|
|
74
|
+
response.cookies.delete(RESET_TOKEN_COOKIE);
|
|
75
|
+
|
|
76
|
+
return response;
|
|
77
|
+
}
|
|
@@ -1,198 +1,198 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { cookies } from 'next/headers';
|
|
3
|
-
|
|
4
|
-
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
5
|
-
/\/$/,
|
|
6
|
-
''
|
|
7
|
-
);
|
|
8
|
-
|
|
9
|
-
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
10
|
-
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
11
|
-
const CSRF_HEADER = 'x-requested-with';
|
|
12
|
-
const CSRF_VALUE = 'brainerce';
|
|
13
|
-
|
|
14
|
-
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
|
|
15
|
-
|
|
16
|
-
/** Auth endpoints whose responses contain tokens to intercept */
|
|
17
|
-
const AUTH_ENDPOINTS = ['customers/login', 'customers/register', 'customers/verify-email'];
|
|
18
|
-
|
|
19
|
-
function isAuthEndpoint(path: string): boolean {
|
|
20
|
-
return AUTH_ENDPOINTS.some((ep) => path.endsWith(ep));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function isSecure(): boolean {
|
|
24
|
-
return process.env.NODE_ENV === 'production';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function setAuthCookies(response: NextResponse, token: string): void {
|
|
28
|
-
response.cookies.set(TOKEN_COOKIE, token, {
|
|
29
|
-
httpOnly: true,
|
|
30
|
-
secure: isSecure(),
|
|
31
|
-
sameSite: 'lax',
|
|
32
|
-
path: '/',
|
|
33
|
-
maxAge: COOKIE_MAX_AGE,
|
|
34
|
-
});
|
|
35
|
-
response.cookies.set(LOGGED_IN_COOKIE, '1', {
|
|
36
|
-
httpOnly: false,
|
|
37
|
-
secure: isSecure(),
|
|
38
|
-
sameSite: 'lax',
|
|
39
|
-
path: '/',
|
|
40
|
-
maxAge: COOKIE_MAX_AGE,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function clearAuthCookies(response: NextResponse): void {
|
|
45
|
-
response.cookies.delete(TOKEN_COOKIE);
|
|
46
|
-
response.cookies.delete(LOGGED_IN_COOKIE);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function proxyRequest(
|
|
50
|
-
request: NextRequest,
|
|
51
|
-
params: { path: string[] }
|
|
52
|
-
): Promise<NextResponse> {
|
|
53
|
-
const method = request.method;
|
|
54
|
-
|
|
55
|
-
// CSRF protection for mutating requests
|
|
56
|
-
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
57
|
-
const csrfHeader = request.headers.get(CSRF_HEADER);
|
|
58
|
-
if (csrfHeader !== CSRF_VALUE) {
|
|
59
|
-
return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Build backend URL from path segments
|
|
64
|
-
const pathSegments = params.path.join('/');
|
|
65
|
-
const backendUrl = new URL(`${BACKEND_URL}/${pathSegments}`);
|
|
66
|
-
|
|
67
|
-
// Forward query parameters
|
|
68
|
-
request.nextUrl.searchParams.forEach((value, key) => {
|
|
69
|
-
backendUrl.searchParams.set(key, value);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Build headers for backend request
|
|
73
|
-
const headers: Record<string, string> = {
|
|
74
|
-
'Content-Type': 'application/json',
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Forward Origin/Referer so backend BrowserOriginGuard accepts proxied requests
|
|
78
|
-
// Always send Origin — same-origin GET requests may not include it, but the backend
|
|
79
|
-
// uses its presence to distinguish fetch() calls from direct browser navigation
|
|
80
|
-
const origin = request.headers.get('origin') || request.nextUrl.origin;
|
|
81
|
-
const referer = request.headers.get('referer');
|
|
82
|
-
headers['Origin'] = origin;
|
|
83
|
-
if (referer) headers['Referer'] = referer;
|
|
84
|
-
|
|
85
|
-
// Forward SDK version header if present
|
|
86
|
-
const sdkVersion = request.headers.get('x-sdk-version');
|
|
87
|
-
if (sdkVersion) {
|
|
88
|
-
headers['X-SDK-Version'] = sdkVersion;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Add auth token from httpOnly cookie
|
|
92
|
-
const cookieStore = await cookies();
|
|
93
|
-
const tokenCookie = cookieStore.get(TOKEN_COOKIE);
|
|
94
|
-
if (tokenCookie?.value) {
|
|
95
|
-
headers['Authorization'] = `Bearer ${tokenCookie.value}`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Forward request body for non-GET requests
|
|
99
|
-
let body: string | undefined;
|
|
100
|
-
if (method !== 'GET' && method !== 'HEAD') {
|
|
101
|
-
try {
|
|
102
|
-
body = await request.text();
|
|
103
|
-
} catch {
|
|
104
|
-
// No body
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Proxy the request to backend
|
|
109
|
-
let backendResponse: Response;
|
|
110
|
-
try {
|
|
111
|
-
backendResponse = await fetch(backendUrl.toString(), {
|
|
112
|
-
method,
|
|
113
|
-
headers,
|
|
114
|
-
body,
|
|
115
|
-
});
|
|
116
|
-
} catch (error) {
|
|
117
|
-
return NextResponse.json({ error: 'Backend service unavailable' }, { status: 502 });
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Read response body
|
|
121
|
-
const responseText = await backendResponse.text();
|
|
122
|
-
|
|
123
|
-
// For auth endpoints: intercept token, set cookie, strip token from response
|
|
124
|
-
if (backendResponse.ok && method === 'POST' && isAuthEndpoint(pathSegments)) {
|
|
125
|
-
try {
|
|
126
|
-
const data = JSON.parse(responseText);
|
|
127
|
-
if (data.token) {
|
|
128
|
-
const token = data.token;
|
|
129
|
-
|
|
130
|
-
// Strip token from client response
|
|
131
|
-
const { token: _stripped, ...safeData } = data;
|
|
132
|
-
|
|
133
|
-
const response = NextResponse.json(safeData, {
|
|
134
|
-
status: backendResponse.status,
|
|
135
|
-
});
|
|
136
|
-
setAuthCookies(response, token);
|
|
137
|
-
return response;
|
|
138
|
-
}
|
|
139
|
-
} catch {
|
|
140
|
-
// Not JSON or no token field — pass through
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Handle 401 responses: clear auth cookies
|
|
145
|
-
if (backendResponse.status === 401 && tokenCookie?.value) {
|
|
146
|
-
const response = new NextResponse(responseText, {
|
|
147
|
-
status: backendResponse.status,
|
|
148
|
-
headers: {
|
|
149
|
-
'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
clearAuthCookies(response);
|
|
153
|
-
return response;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Pass through response as-is
|
|
157
|
-
return new NextResponse(responseText, {
|
|
158
|
-
status: backendResponse.status,
|
|
159
|
-
headers: {
|
|
160
|
-
'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export async function GET(
|
|
166
|
-
request: NextRequest,
|
|
167
|
-
{ params }: { params: Promise<{ path: string[] }> }
|
|
168
|
-
) {
|
|
169
|
-
return proxyRequest(request, await params);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export async function POST(
|
|
173
|
-
request: NextRequest,
|
|
174
|
-
{ params }: { params: Promise<{ path: string[] }> }
|
|
175
|
-
) {
|
|
176
|
-
return proxyRequest(request, await params);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export async function PUT(
|
|
180
|
-
request: NextRequest,
|
|
181
|
-
{ params }: { params: Promise<{ path: string[] }> }
|
|
182
|
-
) {
|
|
183
|
-
return proxyRequest(request, await params);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export async function PATCH(
|
|
187
|
-
request: NextRequest,
|
|
188
|
-
{ params }: { params: Promise<{ path: string[] }> }
|
|
189
|
-
) {
|
|
190
|
-
return proxyRequest(request, await params);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export async function DELETE(
|
|
194
|
-
request: NextRequest,
|
|
195
|
-
{ params }: { params: Promise<{ path: string[] }> }
|
|
196
|
-
) {
|
|
197
|
-
return proxyRequest(request, await params);
|
|
198
|
-
}
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
|
|
4
|
+
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
5
|
+
/\/$/,
|
|
6
|
+
''
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
10
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
11
|
+
const CSRF_HEADER = 'x-requested-with';
|
|
12
|
+
const CSRF_VALUE = 'brainerce';
|
|
13
|
+
|
|
14
|
+
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
|
|
15
|
+
|
|
16
|
+
/** Auth endpoints whose responses contain tokens to intercept */
|
|
17
|
+
const AUTH_ENDPOINTS = ['customers/login', 'customers/register', 'customers/verify-email'];
|
|
18
|
+
|
|
19
|
+
function isAuthEndpoint(path: string): boolean {
|
|
20
|
+
return AUTH_ENDPOINTS.some((ep) => path.endsWith(ep));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isSecure(): boolean {
|
|
24
|
+
return process.env.NODE_ENV === 'production';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setAuthCookies(response: NextResponse, token: string): void {
|
|
28
|
+
response.cookies.set(TOKEN_COOKIE, token, {
|
|
29
|
+
httpOnly: true,
|
|
30
|
+
secure: isSecure(),
|
|
31
|
+
sameSite: 'lax',
|
|
32
|
+
path: '/',
|
|
33
|
+
maxAge: COOKIE_MAX_AGE,
|
|
34
|
+
});
|
|
35
|
+
response.cookies.set(LOGGED_IN_COOKIE, '1', {
|
|
36
|
+
httpOnly: false,
|
|
37
|
+
secure: isSecure(),
|
|
38
|
+
sameSite: 'lax',
|
|
39
|
+
path: '/',
|
|
40
|
+
maxAge: COOKIE_MAX_AGE,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function clearAuthCookies(response: NextResponse): void {
|
|
45
|
+
response.cookies.delete(TOKEN_COOKIE);
|
|
46
|
+
response.cookies.delete(LOGGED_IN_COOKIE);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function proxyRequest(
|
|
50
|
+
request: NextRequest,
|
|
51
|
+
params: { path: string[] }
|
|
52
|
+
): Promise<NextResponse> {
|
|
53
|
+
const method = request.method;
|
|
54
|
+
|
|
55
|
+
// CSRF protection for mutating requests
|
|
56
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
57
|
+
const csrfHeader = request.headers.get(CSRF_HEADER);
|
|
58
|
+
if (csrfHeader !== CSRF_VALUE) {
|
|
59
|
+
return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build backend URL from path segments
|
|
64
|
+
const pathSegments = params.path.join('/');
|
|
65
|
+
const backendUrl = new URL(`${BACKEND_URL}/${pathSegments}`);
|
|
66
|
+
|
|
67
|
+
// Forward query parameters
|
|
68
|
+
request.nextUrl.searchParams.forEach((value, key) => {
|
|
69
|
+
backendUrl.searchParams.set(key, value);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Build headers for backend request
|
|
73
|
+
const headers: Record<string, string> = {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Forward Origin/Referer so backend BrowserOriginGuard accepts proxied requests
|
|
78
|
+
// Always send Origin — same-origin GET requests may not include it, but the backend
|
|
79
|
+
// uses its presence to distinguish fetch() calls from direct browser navigation
|
|
80
|
+
const origin = request.headers.get('origin') || request.nextUrl.origin;
|
|
81
|
+
const referer = request.headers.get('referer');
|
|
82
|
+
headers['Origin'] = origin;
|
|
83
|
+
if (referer) headers['Referer'] = referer;
|
|
84
|
+
|
|
85
|
+
// Forward SDK version header if present
|
|
86
|
+
const sdkVersion = request.headers.get('x-sdk-version');
|
|
87
|
+
if (sdkVersion) {
|
|
88
|
+
headers['X-SDK-Version'] = sdkVersion;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Add auth token from httpOnly cookie
|
|
92
|
+
const cookieStore = await cookies();
|
|
93
|
+
const tokenCookie = cookieStore.get(TOKEN_COOKIE);
|
|
94
|
+
if (tokenCookie?.value) {
|
|
95
|
+
headers['Authorization'] = `Bearer ${tokenCookie.value}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Forward request body for non-GET requests
|
|
99
|
+
let body: string | undefined;
|
|
100
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
101
|
+
try {
|
|
102
|
+
body = await request.text();
|
|
103
|
+
} catch {
|
|
104
|
+
// No body
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Proxy the request to backend
|
|
109
|
+
let backendResponse: Response;
|
|
110
|
+
try {
|
|
111
|
+
backendResponse = await fetch(backendUrl.toString(), {
|
|
112
|
+
method,
|
|
113
|
+
headers,
|
|
114
|
+
body,
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return NextResponse.json({ error: 'Backend service unavailable' }, { status: 502 });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Read response body
|
|
121
|
+
const responseText = await backendResponse.text();
|
|
122
|
+
|
|
123
|
+
// For auth endpoints: intercept token, set cookie, strip token from response
|
|
124
|
+
if (backendResponse.ok && method === 'POST' && isAuthEndpoint(pathSegments)) {
|
|
125
|
+
try {
|
|
126
|
+
const data = JSON.parse(responseText);
|
|
127
|
+
if (data.token) {
|
|
128
|
+
const token = data.token;
|
|
129
|
+
|
|
130
|
+
// Strip token from client response
|
|
131
|
+
const { token: _stripped, ...safeData } = data;
|
|
132
|
+
|
|
133
|
+
const response = NextResponse.json(safeData, {
|
|
134
|
+
status: backendResponse.status,
|
|
135
|
+
});
|
|
136
|
+
setAuthCookies(response, token);
|
|
137
|
+
return response;
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Not JSON or no token field — pass through
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Handle 401 responses: clear auth cookies
|
|
145
|
+
if (backendResponse.status === 401 && tokenCookie?.value) {
|
|
146
|
+
const response = new NextResponse(responseText, {
|
|
147
|
+
status: backendResponse.status,
|
|
148
|
+
headers: {
|
|
149
|
+
'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
clearAuthCookies(response);
|
|
153
|
+
return response;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Pass through response as-is
|
|
157
|
+
return new NextResponse(responseText, {
|
|
158
|
+
status: backendResponse.status,
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function GET(
|
|
166
|
+
request: NextRequest,
|
|
167
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
168
|
+
) {
|
|
169
|
+
return proxyRequest(request, await params);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function POST(
|
|
173
|
+
request: NextRequest,
|
|
174
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
175
|
+
) {
|
|
176
|
+
return proxyRequest(request, await params);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function PUT(
|
|
180
|
+
request: NextRequest,
|
|
181
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
182
|
+
) {
|
|
183
|
+
return proxyRequest(request, await params);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function PATCH(
|
|
187
|
+
request: NextRequest,
|
|
188
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
189
|
+
) {
|
|
190
|
+
return proxyRequest(request, await params);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function DELETE(
|
|
194
|
+
request: NextRequest,
|
|
195
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
196
|
+
) {
|
|
197
|
+
return proxyRequest(request, await params);
|
|
198
|
+
}
|