create-brainerce-store 1.28.13 → 1.28.17
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 +1 -1
- package/messages/en.json +390 -389
- package/messages/he.json +390 -389
- package/package.json +46 -46
- package/templates/nextjs/base/next.config.ts +47 -47
- package/templates/nextjs/base/src/app/api/auth/logout/route.ts +15 -15
- package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +66 -66
- package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +76 -76
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +235 -229
- package/templates/nextjs/base/src/app/checkout/page.tsx +975 -975
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +73 -76
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +529 -501
- package/templates/nextjs/base/src/app/products/page.tsx +475 -482
- package/templates/nextjs/base/src/app/reset-password/page.tsx +138 -138
- package/templates/nextjs/base/src/components/auth/register-form.tsx +245 -245
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +416 -416
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +656 -656
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +88 -88
- package/templates/nextjs/base/src/lib/brainerce.ts.ejs +6 -2
- package/templates/nextjs/base/src/lib/csrf.ts +11 -11
- package/templates/nextjs/base/src/lib/nonce.ts +10 -10
- package/templates/nextjs/base/src/lib/safe-redirect.ts +45 -45
- package/templates/nextjs/base/src/lib/sanitize-html.ts +93 -93
- package/templates/nextjs/base/src/lib/validation.ts +37 -37
package/package.json
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "create-brainerce-store",
|
|
3
|
-
"version": "1.28.
|
|
4
|
-
"description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
5
|
-
"bin": {
|
|
6
|
-
"create-brainerce-store": "dist/index.js"
|
|
7
|
-
},
|
|
8
|
-
"files": [
|
|
9
|
-
"dist",
|
|
10
|
-
"templates",
|
|
11
|
-
"messages"
|
|
12
|
-
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "tsup src/index.ts --format cjs --dts --clean",
|
|
15
|
-
"dev": "tsup src/index.ts --format cjs --dts --watch",
|
|
16
|
-
"clean": "rimraf dist",
|
|
17
|
-
"prepublishOnly": "npm run build"
|
|
18
|
-
},
|
|
19
|
-
"dependencies": {
|
|
20
|
-
"chalk": "^4.1.2",
|
|
21
|
-
"commander": "^12.1.0",
|
|
22
|
-
"ejs": "^3.1.10",
|
|
23
|
-
"fs-extra": "^11.2.0",
|
|
24
|
-
"ora": "^5.4.1",
|
|
25
|
-
"prompts": "^2.4.2"
|
|
26
|
-
},
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"@types/ejs": "^3.1.5",
|
|
29
|
-
"@types/fs-extra": "^11.0.4",
|
|
30
|
-
"@types/prompts": "^2.4.9",
|
|
31
|
-
"tsup": "^8.0.0",
|
|
32
|
-
"typescript": "^5.4.0"
|
|
33
|
-
},
|
|
34
|
-
"engines": {
|
|
35
|
-
"node": ">=18"
|
|
36
|
-
},
|
|
37
|
-
"keywords": [
|
|
38
|
-
"brainerce",
|
|
39
|
-
"ecommerce",
|
|
40
|
-
"storefront",
|
|
41
|
-
"scaffold",
|
|
42
|
-
"create",
|
|
43
|
-
"nextjs"
|
|
44
|
-
],
|
|
45
|
-
"license": "MIT"
|
|
46
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "create-brainerce-store",
|
|
3
|
+
"version": "1.28.17",
|
|
4
|
+
"description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-brainerce-store": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"templates",
|
|
11
|
+
"messages"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup src/index.ts --format cjs --dts --clean",
|
|
15
|
+
"dev": "tsup src/index.ts --format cjs --dts --watch",
|
|
16
|
+
"clean": "rimraf dist",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"chalk": "^4.1.2",
|
|
21
|
+
"commander": "^12.1.0",
|
|
22
|
+
"ejs": "^3.1.10",
|
|
23
|
+
"fs-extra": "^11.2.0",
|
|
24
|
+
"ora": "^5.4.1",
|
|
25
|
+
"prompts": "^2.4.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/ejs": "^3.1.5",
|
|
29
|
+
"@types/fs-extra": "^11.0.4",
|
|
30
|
+
"@types/prompts": "^2.4.9",
|
|
31
|
+
"tsup": "^8.0.0",
|
|
32
|
+
"typescript": "^5.4.0"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"brainerce",
|
|
39
|
+
"ecommerce",
|
|
40
|
+
"storefront",
|
|
41
|
+
"scaffold",
|
|
42
|
+
"create",
|
|
43
|
+
"nextjs"
|
|
44
|
+
],
|
|
45
|
+
"license": "MIT"
|
|
46
|
+
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import type { NextConfig } from 'next';
|
|
2
|
-
|
|
3
|
-
const nextConfig: NextConfig = {
|
|
4
|
-
// isomorphic-dompurify ships jsdom, which at runtime reads stylesheet files
|
|
5
|
-
// from its own package directory. Webpack bundling breaks those relative
|
|
6
|
-
// lookups — loading it externally from node_modules keeps the paths intact.
|
|
7
|
-
serverExternalPackages: ['isomorphic-dompurify'],
|
|
8
|
-
images: {
|
|
9
|
-
// The storefront is a consumer of the Brainerce API — it has to render
|
|
10
|
-
// whatever image URLs the API returns. In practice those URLs can be on
|
|
11
|
-
// cdn.brainerce.com OR on an upstream merchant host (WooCommerce, Shopify,
|
|
12
|
-
// self-hosted) depending on whether the product's image-import job has
|
|
13
|
-
// completed on the backend. Rather than hard-fail on unknown hosts, skip
|
|
14
|
-
// the server-side optimizer entirely and let the browser fetch each image
|
|
15
|
-
// directly from origin. No server-side fetching → no SSRF or DoS surface
|
|
16
|
-
// on this Next server. Trade-off: no webp/resize/lazy optimization, so
|
|
17
|
-
// LCP is marginally worse. Acceptable; the storefront is not the right
|
|
18
|
-
// layer to enforce a hostname policy.
|
|
19
|
-
unoptimized: true,
|
|
20
|
-
},
|
|
21
|
-
async headers() {
|
|
22
|
-
return [
|
|
23
|
-
{
|
|
24
|
-
source: '/(.*)',
|
|
25
|
-
headers: [
|
|
26
|
-
{
|
|
27
|
-
key: 'Strict-Transport-Security',
|
|
28
|
-
value: 'max-age=63072000; includeSubDomains; preload',
|
|
29
|
-
},
|
|
30
|
-
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
31
|
-
// SAMEORIGIN (not DENY) so iframe-based payment providers (e.g. Cardcom)
|
|
32
|
-
// can redirect the iframe back to /payment-complete on the storefront
|
|
33
|
-
// itself after a successful charge — the postMessage relay needs the
|
|
34
|
-
// parent frame to be able to render our own same-origin page.
|
|
35
|
-
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
|
|
36
|
-
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
37
|
-
{
|
|
38
|
-
key: 'Permissions-Policy',
|
|
39
|
-
value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export default nextConfig;
|
|
1
|
+
import type { NextConfig } from 'next';
|
|
2
|
+
|
|
3
|
+
const nextConfig: NextConfig = {
|
|
4
|
+
// isomorphic-dompurify ships jsdom, which at runtime reads stylesheet files
|
|
5
|
+
// from its own package directory. Webpack bundling breaks those relative
|
|
6
|
+
// lookups — loading it externally from node_modules keeps the paths intact.
|
|
7
|
+
serverExternalPackages: ['isomorphic-dompurify'],
|
|
8
|
+
images: {
|
|
9
|
+
// The storefront is a consumer of the Brainerce API — it has to render
|
|
10
|
+
// whatever image URLs the API returns. In practice those URLs can be on
|
|
11
|
+
// cdn.brainerce.com OR on an upstream merchant host (WooCommerce, Shopify,
|
|
12
|
+
// self-hosted) depending on whether the product's image-import job has
|
|
13
|
+
// completed on the backend. Rather than hard-fail on unknown hosts, skip
|
|
14
|
+
// the server-side optimizer entirely and let the browser fetch each image
|
|
15
|
+
// directly from origin. No server-side fetching → no SSRF or DoS surface
|
|
16
|
+
// on this Next server. Trade-off: no webp/resize/lazy optimization, so
|
|
17
|
+
// LCP is marginally worse. Acceptable; the storefront is not the right
|
|
18
|
+
// layer to enforce a hostname policy.
|
|
19
|
+
unoptimized: true,
|
|
20
|
+
},
|
|
21
|
+
async headers() {
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
source: '/(.*)',
|
|
25
|
+
headers: [
|
|
26
|
+
{
|
|
27
|
+
key: 'Strict-Transport-Security',
|
|
28
|
+
value: 'max-age=63072000; includeSubDomains; preload',
|
|
29
|
+
},
|
|
30
|
+
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
31
|
+
// SAMEORIGIN (not DENY) so iframe-based payment providers (e.g. Cardcom)
|
|
32
|
+
// can redirect the iframe back to /payment-complete on the storefront
|
|
33
|
+
// itself after a successful charge — the postMessage relay needs the
|
|
34
|
+
// parent frame to be able to render our own same-origin page.
|
|
35
|
+
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
|
|
36
|
+
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
37
|
+
{
|
|
38
|
+
key: 'Permissions-Policy',
|
|
39
|
+
value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default nextConfig;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { checkCsrf } from '@/lib/csrf';
|
|
3
|
-
|
|
4
|
-
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
5
|
-
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
6
|
-
|
|
7
|
-
export async function POST(request: NextRequest) {
|
|
8
|
-
const csrfError = checkCsrf(request);
|
|
9
|
-
if (csrfError) return csrfError;
|
|
10
|
-
|
|
11
|
-
const response = NextResponse.json({ success: true });
|
|
12
|
-
response.cookies.delete(TOKEN_COOKIE);
|
|
13
|
-
response.cookies.delete(LOGGED_IN_COOKIE);
|
|
14
|
-
return response;
|
|
15
|
-
}
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { checkCsrf } from '@/lib/csrf';
|
|
3
|
+
|
|
4
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
5
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
6
|
+
|
|
7
|
+
export async function POST(request: NextRequest) {
|
|
8
|
+
const csrfError = checkCsrf(request);
|
|
9
|
+
if (csrfError) return csrfError;
|
|
10
|
+
|
|
11
|
+
const response = NextResponse.json({ success: true });
|
|
12
|
+
response.cookies.delete(TOKEN_COOKIE);
|
|
13
|
+
response.cookies.delete(LOGGED_IN_COOKIE);
|
|
14
|
+
return response;
|
|
15
|
+
}
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
|
|
3
|
-
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4
|
-
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
5
|
-
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
|
|
6
|
-
|
|
7
|
-
function isSecure(): boolean {
|
|
8
|
-
return process.env.NODE_ENV === 'production';
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* OAuth callback handler.
|
|
13
|
-
* The backend redirects here with ?token=jwt&oauth_success=true after OAuth code exchange.
|
|
14
|
-
* We set the httpOnly cookie and redirect to the client-side callback page (without the token).
|
|
15
|
-
*/
|
|
16
|
-
export async function GET(request: NextRequest) {
|
|
17
|
-
const { searchParams } = request.nextUrl;
|
|
18
|
-
const token = searchParams.get('token');
|
|
19
|
-
const oauthSuccess = searchParams.get('oauth_success');
|
|
20
|
-
const oauthError = searchParams.get('oauth_error');
|
|
21
|
-
|
|
22
|
-
// Build redirect URL to client-side callback page
|
|
23
|
-
const redirectUrl = new URL('/auth/callback', request.url);
|
|
24
|
-
|
|
25
|
-
if (oauthError) {
|
|
26
|
-
redirectUrl.searchParams.set('oauth_error', oauthError);
|
|
27
|
-
const response = NextResponse.redirect(redirectUrl);
|
|
28
|
-
response.headers.set('Referrer-Policy', 'no-referrer');
|
|
29
|
-
return response;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (oauthSuccess === 'true' && token) {
|
|
33
|
-
redirectUrl.searchParams.set('oauth_success', 'true');
|
|
34
|
-
|
|
35
|
-
const response = NextResponse.redirect(redirectUrl);
|
|
36
|
-
|
|
37
|
-
// Set httpOnly cookie with the token
|
|
38
|
-
response.cookies.set(TOKEN_COOKIE, token, {
|
|
39
|
-
httpOnly: true,
|
|
40
|
-
secure: isSecure(),
|
|
41
|
-
sameSite: 'lax',
|
|
42
|
-
path: '/',
|
|
43
|
-
maxAge: COOKIE_MAX_AGE,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Set indicator cookie (readable by client JS)
|
|
47
|
-
response.cookies.set(LOGGED_IN_COOKIE, '1', {
|
|
48
|
-
httpOnly: false,
|
|
49
|
-
secure: isSecure(),
|
|
50
|
-
sameSite: 'lax',
|
|
51
|
-
path: '/',
|
|
52
|
-
maxAge: COOKIE_MAX_AGE,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Prevent token leaking via Referer header on the downstream navigation
|
|
56
|
-
response.headers.set('Referrer-Policy', 'no-referrer');
|
|
57
|
-
|
|
58
|
-
return response;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Fallback: no token or success flag
|
|
62
|
-
redirectUrl.searchParams.set('oauth_error', 'Authentication failed');
|
|
63
|
-
const response = NextResponse.redirect(redirectUrl);
|
|
64
|
-
response.headers.set('Referrer-Policy', 'no-referrer');
|
|
65
|
-
return response;
|
|
66
|
-
}
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
5
|
+
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
|
|
6
|
+
|
|
7
|
+
function isSecure(): boolean {
|
|
8
|
+
return process.env.NODE_ENV === 'production';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OAuth callback handler.
|
|
13
|
+
* The backend redirects here with ?token=jwt&oauth_success=true after OAuth code exchange.
|
|
14
|
+
* We set the httpOnly cookie and redirect to the client-side callback page (without the token).
|
|
15
|
+
*/
|
|
16
|
+
export async function GET(request: NextRequest) {
|
|
17
|
+
const { searchParams } = request.nextUrl;
|
|
18
|
+
const token = searchParams.get('token');
|
|
19
|
+
const oauthSuccess = searchParams.get('oauth_success');
|
|
20
|
+
const oauthError = searchParams.get('oauth_error');
|
|
21
|
+
|
|
22
|
+
// Build redirect URL to client-side callback page
|
|
23
|
+
const redirectUrl = new URL('/auth/callback', request.url);
|
|
24
|
+
|
|
25
|
+
if (oauthError) {
|
|
26
|
+
redirectUrl.searchParams.set('oauth_error', oauthError);
|
|
27
|
+
const response = NextResponse.redirect(redirectUrl);
|
|
28
|
+
response.headers.set('Referrer-Policy', 'no-referrer');
|
|
29
|
+
return response;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (oauthSuccess === 'true' && token) {
|
|
33
|
+
redirectUrl.searchParams.set('oauth_success', 'true');
|
|
34
|
+
|
|
35
|
+
const response = NextResponse.redirect(redirectUrl);
|
|
36
|
+
|
|
37
|
+
// Set httpOnly cookie with the token
|
|
38
|
+
response.cookies.set(TOKEN_COOKIE, token, {
|
|
39
|
+
httpOnly: true,
|
|
40
|
+
secure: isSecure(),
|
|
41
|
+
sameSite: 'lax',
|
|
42
|
+
path: '/',
|
|
43
|
+
maxAge: COOKIE_MAX_AGE,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Set indicator cookie (readable by client JS)
|
|
47
|
+
response.cookies.set(LOGGED_IN_COOKIE, '1', {
|
|
48
|
+
httpOnly: false,
|
|
49
|
+
secure: isSecure(),
|
|
50
|
+
sameSite: 'lax',
|
|
51
|
+
path: '/',
|
|
52
|
+
maxAge: COOKIE_MAX_AGE,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Prevent token leaking via Referer header on the downstream navigation
|
|
56
|
+
response.headers.set('Referrer-Policy', 'no-referrer');
|
|
57
|
+
|
|
58
|
+
return response;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fallback: no token or success flag
|
|
62
|
+
redirectUrl.searchParams.set('oauth_error', 'Authentication failed');
|
|
63
|
+
const response = NextResponse.redirect(redirectUrl);
|
|
64
|
+
response.headers.set('Referrer-Policy', 'no-referrer');
|
|
65
|
+
return response;
|
|
66
|
+
}
|
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { cookies } from 'next/headers';
|
|
3
|
-
import { validatePassword } from '@/lib/validation';
|
|
4
|
-
import { checkCsrf } from '@/lib/csrf';
|
|
5
|
-
|
|
6
|
-
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
7
|
-
/\/$/,
|
|
8
|
-
''
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
|
|
12
|
-
|
|
13
|
-
const RESET_TOKEN_COOKIE = 'brainerce_reset_token';
|
|
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
|
-
const csrfError = checkCsrf(request);
|
|
22
|
-
if (csrfError) return csrfError;
|
|
23
|
-
|
|
24
|
-
// Read reset token from httpOnly cookie
|
|
25
|
-
const cookieStore = await cookies();
|
|
26
|
-
const resetTokenCookie = cookieStore.get(RESET_TOKEN_COOKIE);
|
|
27
|
-
|
|
28
|
-
if (!resetTokenCookie?.value) {
|
|
29
|
-
return NextResponse.json(
|
|
30
|
-
{ error: 'No reset token found. Please request a new password reset link.' },
|
|
31
|
-
{ status: 400 }
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Parse request body
|
|
36
|
-
const body = await request.json();
|
|
37
|
-
const { newPassword } = body;
|
|
38
|
-
|
|
39
|
-
const passwordError = validatePassword(newPassword);
|
|
40
|
-
if (passwordError) {
|
|
41
|
-
return NextResponse.json({ error: passwordError }, { status: 400 });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Proxy to backend
|
|
45
|
-
const backendUrl = `${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/reset-password`;
|
|
46
|
-
|
|
47
|
-
const backendResponse = await fetch(backendUrl, {
|
|
48
|
-
method: 'POST',
|
|
49
|
-
headers: {
|
|
50
|
-
'Content-Type': 'application/json',
|
|
51
|
-
Origin: request.nextUrl.origin,
|
|
52
|
-
},
|
|
53
|
-
body: JSON.stringify({
|
|
54
|
-
token: resetTokenCookie.value,
|
|
55
|
-
newPassword,
|
|
56
|
-
}),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
let data: unknown;
|
|
60
|
-
try {
|
|
61
|
-
data = await backendResponse.json();
|
|
62
|
-
} catch {
|
|
63
|
-
const response = NextResponse.json({ error: 'Invalid response from backend' }, { status: 502 });
|
|
64
|
-
response.cookies.delete(RESET_TOKEN_COOKIE);
|
|
65
|
-
return response;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const response = NextResponse.json(data, {
|
|
69
|
-
status: backendResponse.status,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Always clear the reset token cookie after use (success or failure)
|
|
73
|
-
response.cookies.delete(RESET_TOKEN_COOKIE);
|
|
74
|
-
|
|
75
|
-
return response;
|
|
76
|
-
}
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
import { validatePassword } from '@/lib/validation';
|
|
4
|
+
import { checkCsrf } from '@/lib/csrf';
|
|
5
|
+
|
|
6
|
+
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
7
|
+
/\/$/,
|
|
8
|
+
''
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
|
|
12
|
+
|
|
13
|
+
const RESET_TOKEN_COOKIE = 'brainerce_reset_token';
|
|
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
|
+
const csrfError = checkCsrf(request);
|
|
22
|
+
if (csrfError) return csrfError;
|
|
23
|
+
|
|
24
|
+
// Read reset token from httpOnly cookie
|
|
25
|
+
const cookieStore = await cookies();
|
|
26
|
+
const resetTokenCookie = cookieStore.get(RESET_TOKEN_COOKIE);
|
|
27
|
+
|
|
28
|
+
if (!resetTokenCookie?.value) {
|
|
29
|
+
return NextResponse.json(
|
|
30
|
+
{ error: 'No reset token found. Please request a new password reset link.' },
|
|
31
|
+
{ status: 400 }
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Parse request body
|
|
36
|
+
const body = await request.json();
|
|
37
|
+
const { newPassword } = body;
|
|
38
|
+
|
|
39
|
+
const passwordError = validatePassword(newPassword);
|
|
40
|
+
if (passwordError) {
|
|
41
|
+
return NextResponse.json({ error: passwordError }, { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Proxy to backend
|
|
45
|
+
const backendUrl = `${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/reset-password`;
|
|
46
|
+
|
|
47
|
+
const backendResponse = await fetch(backendUrl, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
Origin: request.nextUrl.origin,
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
token: resetTokenCookie.value,
|
|
55
|
+
newPassword,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let data: unknown;
|
|
60
|
+
try {
|
|
61
|
+
data = await backendResponse.json();
|
|
62
|
+
} catch {
|
|
63
|
+
const response = NextResponse.json({ error: 'Invalid response from backend' }, { status: 502 });
|
|
64
|
+
response.cookies.delete(RESET_TOKEN_COOKIE);
|
|
65
|
+
return response;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const response = NextResponse.json(data, {
|
|
69
|
+
status: backendResponse.status,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Always clear the reset token cookie after use (success or failure)
|
|
73
|
+
response.cookies.delete(RESET_TOKEN_COOKIE);
|
|
74
|
+
|
|
75
|
+
return response;
|
|
76
|
+
}
|