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,122 +1,122 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { useRouter } from 'next/navigation';
|
|
5
|
-
import type { CustomerProfile, Order } from 'brainerce';
|
|
6
|
-
import { getClient } from '@/lib/brainerce';
|
|
7
|
-
import { useAuth } from '@/providers/store-provider';
|
|
8
|
-
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
9
|
-
import { ProfileSection } from '@/components/account/profile-section';
|
|
10
|
-
import { AddressBook } from '@/components/account/address-book';
|
|
11
|
-
import { OrderHistory } from '@/components/account/order-history';
|
|
12
|
-
import { useTranslations } from '@/lib/translations';
|
|
13
|
-
|
|
14
|
-
export default function AccountPage() {
|
|
15
|
-
const router = useRouter();
|
|
16
|
-
const { isLoggedIn, authLoading, logout } = useAuth();
|
|
17
|
-
const t = useTranslations('account');
|
|
18
|
-
const tc = useTranslations('common');
|
|
19
|
-
const [profile, setProfile] = useState<CustomerProfile | null>(null);
|
|
20
|
-
const [orders, setOrders] = useState<Order[]>([]);
|
|
21
|
-
const [loading, setLoading] = useState(true);
|
|
22
|
-
const [error, setError] = useState<string | null>(null);
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (authLoading) return;
|
|
26
|
-
|
|
27
|
-
if (!isLoggedIn) {
|
|
28
|
-
router.push('/login');
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function loadAccountData() {
|
|
33
|
-
try {
|
|
34
|
-
const client = getClient();
|
|
35
|
-
const [profileResult, ordersResult] = await Promise.allSettled([
|
|
36
|
-
client.getMyProfile(),
|
|
37
|
-
client.getMyOrders({ limit: 20 }),
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
if (profileResult.status === 'fulfilled') {
|
|
41
|
-
setProfile(profileResult.value);
|
|
42
|
-
} else {
|
|
43
|
-
setError('Failed to load profile.');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (ordersResult.status === 'fulfilled') {
|
|
47
|
-
setOrders(ordersResult.value.data);
|
|
48
|
-
}
|
|
49
|
-
} catch (err) {
|
|
50
|
-
const message = err instanceof Error ? err.message : 'Failed to load account data.';
|
|
51
|
-
setError(message);
|
|
52
|
-
} finally {
|
|
53
|
-
setLoading(false);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
loadAccountData();
|
|
58
|
-
}, [isLoggedIn, authLoading, router]);
|
|
59
|
-
|
|
60
|
-
if (authLoading || !isLoggedIn) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (loading) {
|
|
65
|
-
return (
|
|
66
|
-
<div className="flex min-h-[60vh] items-center justify-center">
|
|
67
|
-
<LoadingSpinner size="lg" />
|
|
68
|
-
</div>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (error && !profile) {
|
|
73
|
-
return (
|
|
74
|
-
<div className="mx-auto max-w-3xl px-4 py-16 text-center sm:px-6 lg:px-8">
|
|
75
|
-
<h1 className="text-foreground text-2xl font-bold">{tc('error')}</h1>
|
|
76
|
-
<p className="text-muted-foreground mt-2">{error}</p>
|
|
77
|
-
<button
|
|
78
|
-
type="button"
|
|
79
|
-
onClick={() => window.location.reload()}
|
|
80
|
-
className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
|
|
81
|
-
>
|
|
82
|
-
{tc('tryAgain')}
|
|
83
|
-
</button>
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
|
90
|
-
<div className="mb-6 flex items-center justify-between">
|
|
91
|
-
<h1 className="text-foreground text-2xl font-bold">{t('myAccount')}</h1>
|
|
92
|
-
<button
|
|
93
|
-
type="button"
|
|
94
|
-
onClick={logout}
|
|
95
|
-
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
96
|
-
>
|
|
97
|
-
{t('signOut')}
|
|
98
|
-
</button>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
{/* Profile Section */}
|
|
102
|
-
{profile && (
|
|
103
|
-
<ProfileSection profile={profile} onProfileUpdate={setProfile} className="mb-6" />
|
|
104
|
-
)}
|
|
105
|
-
|
|
106
|
-
{/* Address Book */}
|
|
107
|
-
{profile && (
|
|
108
|
-
<AddressBook
|
|
109
|
-
addresses={profile.addresses ?? []}
|
|
110
|
-
onUpdate={(updated) => setProfile((p) => (p ? { ...p, addresses: updated } : p))}
|
|
111
|
-
className="mb-8"
|
|
112
|
-
/>
|
|
113
|
-
)}
|
|
114
|
-
|
|
115
|
-
{/* Order History */}
|
|
116
|
-
<div>
|
|
117
|
-
<h2 className="text-foreground mb-4 text-lg font-semibold">{t('orderHistory')}</h2>
|
|
118
|
-
<OrderHistory orders={orders} />
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import type { CustomerProfile, Order } from 'brainerce';
|
|
6
|
+
import { getClient } from '@/lib/brainerce';
|
|
7
|
+
import { useAuth } from '@/providers/store-provider';
|
|
8
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
9
|
+
import { ProfileSection } from '@/components/account/profile-section';
|
|
10
|
+
import { AddressBook } from '@/components/account/address-book';
|
|
11
|
+
import { OrderHistory } from '@/components/account/order-history';
|
|
12
|
+
import { useTranslations } from '@/lib/translations';
|
|
13
|
+
|
|
14
|
+
export default function AccountPage() {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const { isLoggedIn, authLoading, logout } = useAuth();
|
|
17
|
+
const t = useTranslations('account');
|
|
18
|
+
const tc = useTranslations('common');
|
|
19
|
+
const [profile, setProfile] = useState<CustomerProfile | null>(null);
|
|
20
|
+
const [orders, setOrders] = useState<Order[]>([]);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (authLoading) return;
|
|
26
|
+
|
|
27
|
+
if (!isLoggedIn) {
|
|
28
|
+
router.push('/login');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function loadAccountData() {
|
|
33
|
+
try {
|
|
34
|
+
const client = getClient();
|
|
35
|
+
const [profileResult, ordersResult] = await Promise.allSettled([
|
|
36
|
+
client.getMyProfile(),
|
|
37
|
+
client.getMyOrders({ limit: 20 }),
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
if (profileResult.status === 'fulfilled') {
|
|
41
|
+
setProfile(profileResult.value);
|
|
42
|
+
} else {
|
|
43
|
+
setError('Failed to load profile.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (ordersResult.status === 'fulfilled') {
|
|
47
|
+
setOrders(ordersResult.value.data);
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : 'Failed to load account data.';
|
|
51
|
+
setError(message);
|
|
52
|
+
} finally {
|
|
53
|
+
setLoading(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
loadAccountData();
|
|
58
|
+
}, [isLoggedIn, authLoading, router]);
|
|
59
|
+
|
|
60
|
+
if (authLoading || !isLoggedIn) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (loading) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex min-h-[60vh] items-center justify-center">
|
|
67
|
+
<LoadingSpinner size="lg" />
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (error && !profile) {
|
|
73
|
+
return (
|
|
74
|
+
<div className="mx-auto max-w-3xl px-4 py-16 text-center sm:px-6 lg:px-8">
|
|
75
|
+
<h1 className="text-foreground text-2xl font-bold">{tc('error')}</h1>
|
|
76
|
+
<p className="text-muted-foreground mt-2">{error}</p>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={() => window.location.reload()}
|
|
80
|
+
className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
|
|
81
|
+
>
|
|
82
|
+
{tc('tryAgain')}
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
|
90
|
+
<div className="mb-6 flex items-center justify-between">
|
|
91
|
+
<h1 className="text-foreground text-2xl font-bold">{t('myAccount')}</h1>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={logout}
|
|
95
|
+
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
96
|
+
>
|
|
97
|
+
{t('signOut')}
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Profile Section */}
|
|
102
|
+
{profile && (
|
|
103
|
+
<ProfileSection profile={profile} onProfileUpdate={setProfile} className="mb-6" />
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{/* Address Book */}
|
|
107
|
+
{profile && (
|
|
108
|
+
<AddressBook
|
|
109
|
+
addresses={profile.addresses ?? []}
|
|
110
|
+
onUpdate={(updated) => setProfile((p) => (p ? { ...p, addresses: updated } : p))}
|
|
111
|
+
className="mb-8"
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{/* Order History */}
|
|
116
|
+
<div>
|
|
117
|
+
<h2 className="text-foreground mb-4 text-lg font-semibold">{t('orderHistory')}</h2>
|
|
118
|
+
<OrderHistory orders={orders} />
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
2
|
-
|
|
3
|
-
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4
|
-
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Logout endpoint. Clears auth cookies.
|
|
8
|
-
*/
|
|
9
|
-
export async function POST() {
|
|
10
|
-
const response = NextResponse.json({ success: true });
|
|
11
|
-
response.cookies.delete(TOKEN_COOKIE);
|
|
12
|
-
response.cookies.delete(LOGGED_IN_COOKIE);
|
|
13
|
-
return response;
|
|
14
|
-
}
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Logout endpoint. Clears auth cookies.
|
|
8
|
+
*/
|
|
9
|
+
export async function POST() {
|
|
10
|
+
const response = NextResponse.json({ success: true });
|
|
11
|
+
response.cookies.delete(TOKEN_COOKIE);
|
|
12
|
+
response.cookies.delete(LOGGED_IN_COOKIE);
|
|
13
|
+
return response;
|
|
14
|
+
}
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
import { 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 TOKEN_COOKIE = 'brainerce_customer_token';
|
|
12
|
-
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Auth status check endpoint.
|
|
16
|
-
* Reads the httpOnly cookie, validates against backend, returns auth state.
|
|
17
|
-
*/
|
|
18
|
-
export async function GET() {
|
|
19
|
-
const cookieStore = await cookies();
|
|
20
|
-
const tokenCookie = cookieStore.get(TOKEN_COOKIE);
|
|
21
|
-
|
|
22
|
-
if (!tokenCookie?.value) {
|
|
23
|
-
return NextResponse.json({ isLoggedIn: false });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Derive Origin from the incoming request so the backend's BrowserOriginGuard accepts it
|
|
27
|
-
const requestHeaders = await headers();
|
|
28
|
-
const host = requestHeaders.get('host') || 'localhost:3000';
|
|
29
|
-
const proto = requestHeaders.get('x-forwarded-proto') || 'http';
|
|
30
|
-
const origin = requestHeaders.get('origin') || `${proto}://${host}`;
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
// Validate token by calling backend profile endpoint
|
|
34
|
-
const response = await fetch(`${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/me`, {
|
|
35
|
-
headers: {
|
|
36
|
-
Authorization: `Bearer ${tokenCookie.value}`,
|
|
37
|
-
'Content-Type': 'application/json',
|
|
38
|
-
Origin: origin,
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
// Token is invalid or expired — clear cookies
|
|
44
|
-
const res = NextResponse.json({ isLoggedIn: false });
|
|
45
|
-
res.cookies.delete(TOKEN_COOKIE);
|
|
46
|
-
res.cookies.delete(LOGGED_IN_COOKIE);
|
|
47
|
-
return res;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const customer = await response.json();
|
|
51
|
-
return NextResponse.json({ isLoggedIn: true, customer });
|
|
52
|
-
} catch {
|
|
53
|
-
// Backend unreachable — don't clear cookies, might be temporary
|
|
54
|
-
return NextResponse.json({ isLoggedIn: false, error: 'Service unavailable' }, { status: 503 });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
1
|
+
import { 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 TOKEN_COOKIE = 'brainerce_customer_token';
|
|
12
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Auth status check endpoint.
|
|
16
|
+
* Reads the httpOnly cookie, validates against backend, returns auth state.
|
|
17
|
+
*/
|
|
18
|
+
export async function GET() {
|
|
19
|
+
const cookieStore = await cookies();
|
|
20
|
+
const tokenCookie = cookieStore.get(TOKEN_COOKIE);
|
|
21
|
+
|
|
22
|
+
if (!tokenCookie?.value) {
|
|
23
|
+
return NextResponse.json({ isLoggedIn: false });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Derive Origin from the incoming request so the backend's BrowserOriginGuard accepts it
|
|
27
|
+
const requestHeaders = await headers();
|
|
28
|
+
const host = requestHeaders.get('host') || 'localhost:3000';
|
|
29
|
+
const proto = requestHeaders.get('x-forwarded-proto') || 'http';
|
|
30
|
+
const origin = requestHeaders.get('origin') || `${proto}://${host}`;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Validate token by calling backend profile endpoint
|
|
34
|
+
const response = await fetch(`${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/me`, {
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${tokenCookie.value}`,
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
Origin: origin,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
// Token is invalid or expired — clear cookies
|
|
44
|
+
const res = NextResponse.json({ isLoggedIn: false });
|
|
45
|
+
res.cookies.delete(TOKEN_COOKIE);
|
|
46
|
+
res.cookies.delete(LOGGED_IN_COOKIE);
|
|
47
|
+
return res;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const customer = await response.json();
|
|
51
|
+
return NextResponse.json({ isLoggedIn: true, customer });
|
|
52
|
+
} catch {
|
|
53
|
+
// Backend unreachable — don't clear cookies, might be temporary
|
|
54
|
+
return NextResponse.json({ isLoggedIn: false, error: 'Service unavailable' }, { status: 503 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,59 +1,59 @@
|
|
|
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
|
-
return NextResponse.redirect(redirectUrl);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (oauthSuccess === 'true' && token) {
|
|
31
|
-
redirectUrl.searchParams.set('oauth_success', 'true');
|
|
32
|
-
|
|
33
|
-
const response = NextResponse.redirect(redirectUrl);
|
|
34
|
-
|
|
35
|
-
// Set httpOnly cookie with the token
|
|
36
|
-
response.cookies.set(TOKEN_COOKIE, token, {
|
|
37
|
-
httpOnly: true,
|
|
38
|
-
secure: isSecure(),
|
|
39
|
-
sameSite: 'lax',
|
|
40
|
-
path: '/',
|
|
41
|
-
maxAge: COOKIE_MAX_AGE,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Set indicator cookie (readable by client JS)
|
|
45
|
-
response.cookies.set(LOGGED_IN_COOKIE, '1', {
|
|
46
|
-
httpOnly: false,
|
|
47
|
-
secure: isSecure(),
|
|
48
|
-
sameSite: 'lax',
|
|
49
|
-
path: '/',
|
|
50
|
-
maxAge: COOKIE_MAX_AGE,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
return response;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Fallback: no token or success flag
|
|
57
|
-
redirectUrl.searchParams.set('oauth_error', 'Authentication failed');
|
|
58
|
-
return NextResponse.redirect(redirectUrl);
|
|
59
|
-
}
|
|
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
|
+
return NextResponse.redirect(redirectUrl);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (oauthSuccess === 'true' && token) {
|
|
31
|
+
redirectUrl.searchParams.set('oauth_success', 'true');
|
|
32
|
+
|
|
33
|
+
const response = NextResponse.redirect(redirectUrl);
|
|
34
|
+
|
|
35
|
+
// Set httpOnly cookie with the token
|
|
36
|
+
response.cookies.set(TOKEN_COOKIE, token, {
|
|
37
|
+
httpOnly: true,
|
|
38
|
+
secure: isSecure(),
|
|
39
|
+
sameSite: 'lax',
|
|
40
|
+
path: '/',
|
|
41
|
+
maxAge: COOKIE_MAX_AGE,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Set indicator cookie (readable by client JS)
|
|
45
|
+
response.cookies.set(LOGGED_IN_COOKIE, '1', {
|
|
46
|
+
httpOnly: false,
|
|
47
|
+
secure: isSecure(),
|
|
48
|
+
sameSite: 'lax',
|
|
49
|
+
path: '/',
|
|
50
|
+
maxAge: COOKIE_MAX_AGE,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fallback: no token or success flag
|
|
57
|
+
redirectUrl.searchParams.set('oauth_error', 'Authentication failed');
|
|
58
|
+
return NextResponse.redirect(redirectUrl);
|
|
59
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
|
|
3
|
-
const RESET_TOKEN_COOKIE = 'brainerce_reset_token';
|
|
4
|
-
const RESET_TOKEN_MAX_AGE = 10 * 60; // 10 minutes
|
|
5
|
-
|
|
6
|
-
function isSecure(): boolean {
|
|
7
|
-
return process.env.NODE_ENV === 'production';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Password-reset callback handler.
|
|
12
|
-
* The email link redirects here with ?token=... from the backend.
|
|
13
|
-
* We store the token in an httpOnly cookie and redirect to /reset-password (clean URL).
|
|
14
|
-
* This mirrors the OAuth callback pattern — the token never reaches client JS.
|
|
15
|
-
*/
|
|
16
|
-
export async function GET(request: NextRequest) {
|
|
17
|
-
const { searchParams } = request.nextUrl;
|
|
18
|
-
const token = searchParams.get('token');
|
|
19
|
-
|
|
20
|
-
if (!token) {
|
|
21
|
-
const redirectUrl = new URL('/forgot-password', request.url);
|
|
22
|
-
return NextResponse.redirect(redirectUrl);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const redirectUrl = new URL('/reset-password', request.url);
|
|
26
|
-
const response = NextResponse.redirect(redirectUrl);
|
|
27
|
-
|
|
28
|
-
// Set httpOnly cookie with the reset token (short-lived)
|
|
29
|
-
response.cookies.set(RESET_TOKEN_COOKIE, token, {
|
|
30
|
-
httpOnly: true,
|
|
31
|
-
secure: isSecure(),
|
|
32
|
-
sameSite: 'lax',
|
|
33
|
-
path: '/',
|
|
34
|
-
maxAge: RESET_TOKEN_MAX_AGE,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// Prevent token leaking via Referer header
|
|
38
|
-
response.headers.set('Referrer-Policy', 'no-referrer');
|
|
39
|
-
|
|
40
|
-
return response;
|
|
41
|
-
}
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
const RESET_TOKEN_COOKIE = 'brainerce_reset_token';
|
|
4
|
+
const RESET_TOKEN_MAX_AGE = 10 * 60; // 10 minutes
|
|
5
|
+
|
|
6
|
+
function isSecure(): boolean {
|
|
7
|
+
return process.env.NODE_ENV === 'production';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Password-reset callback handler.
|
|
12
|
+
* The email link redirects here with ?token=... from the backend.
|
|
13
|
+
* We store the token in an httpOnly cookie and redirect to /reset-password (clean URL).
|
|
14
|
+
* This mirrors the OAuth callback pattern — the token never reaches client JS.
|
|
15
|
+
*/
|
|
16
|
+
export async function GET(request: NextRequest) {
|
|
17
|
+
const { searchParams } = request.nextUrl;
|
|
18
|
+
const token = searchParams.get('token');
|
|
19
|
+
|
|
20
|
+
if (!token) {
|
|
21
|
+
const redirectUrl = new URL('/forgot-password', request.url);
|
|
22
|
+
return NextResponse.redirect(redirectUrl);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const redirectUrl = new URL('/reset-password', request.url);
|
|
26
|
+
const response = NextResponse.redirect(redirectUrl);
|
|
27
|
+
|
|
28
|
+
// Set httpOnly cookie with the reset token (short-lived)
|
|
29
|
+
response.cookies.set(RESET_TOKEN_COOKIE, token, {
|
|
30
|
+
httpOnly: true,
|
|
31
|
+
secure: isSecure(),
|
|
32
|
+
sameSite: 'lax',
|
|
33
|
+
path: '/',
|
|
34
|
+
maxAge: RESET_TOKEN_MAX_AGE,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Prevent token leaking via Referer header
|
|
38
|
+
response.headers.set('Referrer-Policy', 'no-referrer');
|
|
39
|
+
|
|
40
|
+
return response;
|
|
41
|
+
}
|