@brainerce/mcp-server 1.1.0 → 1.2.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/bin/http.js +619 -121
- package/dist/bin/stdio.js +619 -121
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +619 -121
- package/dist/index.mjs +619 -121
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1910,39 +1910,24 @@ var EMBEDDED_TEMPLATES = {
|
|
|
1910
1910
|
"src/lib/brainerce.ts.ejs": `import { BrainerceClient } from 'brainerce';
|
|
1911
1911
|
|
|
1912
1912
|
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || 'vc_YOUR_CONNECTION_ID';
|
|
1913
|
-
const API_URL = process.env.NEXT_PUBLIC_BRAINERCE_API_URL || 'https://api.brainerce.com';
|
|
1914
1913
|
|
|
1915
|
-
// Singleton SDK client
|
|
1914
|
+
// Singleton SDK client \u2014 routes through same-origin BFF proxy for httpOnly cookie auth
|
|
1916
1915
|
let clientInstance: BrainerceClient | null = null;
|
|
1917
1916
|
|
|
1918
1917
|
export function getClient(): BrainerceClient {
|
|
1919
1918
|
if (!clientInstance) {
|
|
1920
1919
|
clientInstance = new BrainerceClient({
|
|
1921
1920
|
connectionId: CONNECTION_ID,
|
|
1922
|
-
baseUrl:
|
|
1921
|
+
baseUrl: '/api/store', // same-origin proxy handles auth via httpOnly cookie
|
|
1922
|
+
proxyMode: true, // skip client-side token checks; proxy adds Authorization header
|
|
1923
1923
|
});
|
|
1924
1924
|
}
|
|
1925
1925
|
return clientInstance;
|
|
1926
1926
|
}
|
|
1927
1927
|
|
|
1928
|
-
//
|
|
1929
|
-
const TOKEN_KEY = 'brainerce_customer_token';
|
|
1928
|
+
// Cart ID helpers (not a security token \u2014 safe in localStorage)
|
|
1930
1929
|
const CART_ID_KEY = 'brainerce_cart_id';
|
|
1931
1930
|
|
|
1932
|
-
export function getStoredToken(): string | null {
|
|
1933
|
-
if (typeof window === 'undefined') return null;
|
|
1934
|
-
return localStorage.getItem(TOKEN_KEY);
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
export function setStoredToken(token: string | null): void {
|
|
1938
|
-
if (typeof window === 'undefined') return;
|
|
1939
|
-
if (token) {
|
|
1940
|
-
localStorage.setItem(TOKEN_KEY, token);
|
|
1941
|
-
} else {
|
|
1942
|
-
localStorage.removeItem(TOKEN_KEY);
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
1931
|
export function getStoredCartId(): string | null {
|
|
1947
1932
|
if (typeof window === 'undefined') return null;
|
|
1948
1933
|
return localStorage.getItem(CART_ID_KEY);
|
|
@@ -1957,14 +1942,158 @@ export function setStoredCartId(cartId: string | null): void {
|
|
|
1957
1942
|
}
|
|
1958
1943
|
}
|
|
1959
1944
|
|
|
1960
|
-
// Initialize client
|
|
1945
|
+
// Initialize client (no token hydration \u2014 auth handled by httpOnly cookie)
|
|
1961
1946
|
export function initClient(): BrainerceClient {
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1947
|
+
return getClient();
|
|
1948
|
+
}
|
|
1949
|
+
`,
|
|
1950
|
+
"src/lib/auth.ts": `/**
|
|
1951
|
+
* Client-side auth helpers that call the BFF proxy API routes.
|
|
1952
|
+
* All mutating requests include the CSRF header.
|
|
1953
|
+
* The token is managed server-side via httpOnly cookies \u2014 never exposed to JS.
|
|
1954
|
+
*/
|
|
1955
|
+
|
|
1956
|
+
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
|
|
1957
|
+
|
|
1958
|
+
const CSRF_HEADERS: Record<string, string> = {
|
|
1959
|
+
'Content-Type': 'application/json',
|
|
1960
|
+
'X-Requested-With': 'brainerce',
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
interface LoginResult {
|
|
1964
|
+
customer: {
|
|
1965
|
+
id: string;
|
|
1966
|
+
email: string;
|
|
1967
|
+
firstName?: string;
|
|
1968
|
+
lastName?: string;
|
|
1969
|
+
emailVerified: boolean;
|
|
1970
|
+
};
|
|
1971
|
+
expiresAt: string;
|
|
1972
|
+
requiresVerification?: boolean;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
interface RegisterResult {
|
|
1976
|
+
customer: {
|
|
1977
|
+
id: string;
|
|
1978
|
+
email: string;
|
|
1979
|
+
firstName?: string;
|
|
1980
|
+
lastName?: string;
|
|
1981
|
+
emailVerified: boolean;
|
|
1982
|
+
};
|
|
1983
|
+
expiresAt: string;
|
|
1984
|
+
requiresVerification?: boolean;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
interface AuthStatus {
|
|
1988
|
+
isLoggedIn: boolean;
|
|
1989
|
+
customer?: {
|
|
1990
|
+
id: string;
|
|
1991
|
+
email: string;
|
|
1992
|
+
firstName?: string;
|
|
1993
|
+
lastName?: string;
|
|
1994
|
+
phone?: string;
|
|
1995
|
+
emailVerified: boolean;
|
|
1996
|
+
};
|
|
1997
|
+
error?: string;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
interface VerifyEmailResult {
|
|
2001
|
+
verified: boolean;
|
|
2002
|
+
message?: string;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
async function handleResponse<T>(response: Response): Promise<T> {
|
|
2006
|
+
const data = await response.json();
|
|
2007
|
+
if (!response.ok) {
|
|
2008
|
+
throw new Error(data.message || data.error || \`Request failed (\${response.status})\`);
|
|
1966
2009
|
}
|
|
1967
|
-
return
|
|
2010
|
+
return data as T;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
* Login via BFF proxy. The proxy sets the httpOnly cookie on success.
|
|
2015
|
+
*/
|
|
2016
|
+
export async function proxyLogin(email: string, password: string): Promise<LoginResult> {
|
|
2017
|
+
const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/login\`, {
|
|
2018
|
+
method: 'POST',
|
|
2019
|
+
headers: CSRF_HEADERS,
|
|
2020
|
+
body: JSON.stringify({ email, password }),
|
|
2021
|
+
});
|
|
2022
|
+
return handleResponse<LoginResult>(response);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/**
|
|
2026
|
+
* Register via BFF proxy. The proxy sets the httpOnly cookie on success.
|
|
2027
|
+
*/
|
|
2028
|
+
export async function proxyRegister(data: {
|
|
2029
|
+
firstName: string;
|
|
2030
|
+
lastName: string;
|
|
2031
|
+
email: string;
|
|
2032
|
+
password: string;
|
|
2033
|
+
}): Promise<RegisterResult> {
|
|
2034
|
+
const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/register\`, {
|
|
2035
|
+
method: 'POST',
|
|
2036
|
+
headers: CSRF_HEADERS,
|
|
2037
|
+
body: JSON.stringify(data),
|
|
2038
|
+
});
|
|
2039
|
+
return handleResponse<RegisterResult>(response);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* Check auth status. Reads httpOnly cookie server-side and validates with backend.
|
|
2044
|
+
*/
|
|
2045
|
+
export async function checkAuthStatus(): Promise<AuthStatus> {
|
|
2046
|
+
const response = await fetch('/api/auth/me');
|
|
2047
|
+
return response.json();
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/**
|
|
2051
|
+
* Logout. Clears httpOnly auth cookies server-side.
|
|
2052
|
+
*/
|
|
2053
|
+
export async function proxyLogout(): Promise<void> {
|
|
2054
|
+
await fetch('/api/auth/logout', {
|
|
2055
|
+
method: 'POST',
|
|
2056
|
+
headers: { 'X-Requested-With': 'brainerce' },
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
/**
|
|
2061
|
+
* Verify email via BFF proxy. The auth token is in the httpOnly cookie (set during login/register).
|
|
2062
|
+
* The proxy adds the Authorization header automatically.
|
|
2063
|
+
*/
|
|
2064
|
+
export async function proxyVerifyEmail(code: string): Promise<VerifyEmailResult> {
|
|
2065
|
+
const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/verify-email\`, {
|
|
2066
|
+
method: 'POST',
|
|
2067
|
+
headers: CSRF_HEADERS,
|
|
2068
|
+
body: JSON.stringify({ code }),
|
|
2069
|
+
});
|
|
2070
|
+
return handleResponse<VerifyEmailResult>(response);
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/**
|
|
2074
|
+
* Resend verification email via BFF proxy.
|
|
2075
|
+
* Uses the auth token from the httpOnly cookie.
|
|
2076
|
+
*/
|
|
2077
|
+
export async function proxyResendVerification(): Promise<{ message: string }> {
|
|
2078
|
+
const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/resend-verification\`, {
|
|
2079
|
+
method: 'POST',
|
|
2080
|
+
headers: CSRF_HEADERS,
|
|
2081
|
+
});
|
|
2082
|
+
return handleResponse<{ message: string }>(response);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* Reset password via BFF proxy.
|
|
2087
|
+
* The reset token is in an httpOnly cookie (set by /api/auth/reset-callback when the user
|
|
2088
|
+
* clicked the email link). The proxy reads it server-side \u2014 the token never reaches client JS.
|
|
2089
|
+
*/
|
|
2090
|
+
export async function proxyResetPassword(newPassword: string): Promise<{ message: string }> {
|
|
2091
|
+
const response = await fetch('/api/auth/reset-password', {
|
|
2092
|
+
method: 'POST',
|
|
2093
|
+
headers: CSRF_HEADERS,
|
|
2094
|
+
body: JSON.stringify({ newPassword }),
|
|
2095
|
+
});
|
|
2096
|
+
return handleResponse<{ message: string }>(response);
|
|
1968
2097
|
}
|
|
1969
2098
|
`,
|
|
1970
2099
|
"src/lib/utils.ts": `import { clsx, type ClassValue } from 'clsx';
|
|
@@ -1977,9 +2106,10 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
1977
2106
|
"src/providers/store-provider.tsx.ejs": `'use client';
|
|
1978
2107
|
|
|
1979
2108
|
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
1980
|
-
import type { StoreInfo, Cart } from 'brainerce';
|
|
2109
|
+
import type { StoreInfo, Cart, CustomerProfile } from 'brainerce';
|
|
1981
2110
|
import { getCartTotals } from 'brainerce';
|
|
1982
|
-
import { getClient, initClient,
|
|
2111
|
+
import { getClient, initClient, setStoredCartId } from '@/lib/brainerce';
|
|
2112
|
+
import { checkAuthStatus, proxyLogout } from '@/lib/auth';
|
|
1983
2113
|
|
|
1984
2114
|
// ---- Store Info Context ----
|
|
1985
2115
|
interface StoreInfoContextValue {
|
|
@@ -2000,17 +2130,17 @@ export function useStoreInfo() {
|
|
|
2000
2130
|
interface AuthContextValue {
|
|
2001
2131
|
isLoggedIn: boolean;
|
|
2002
2132
|
authLoading: boolean;
|
|
2003
|
-
|
|
2004
|
-
login: (
|
|
2005
|
-
logout: () => void
|
|
2133
|
+
customer: CustomerProfile | null;
|
|
2134
|
+
login: () => Promise<void>;
|
|
2135
|
+
logout: () => Promise<void>;
|
|
2006
2136
|
}
|
|
2007
2137
|
|
|
2008
2138
|
const AuthContext = createContext<AuthContextValue>({
|
|
2009
2139
|
isLoggedIn: false,
|
|
2010
2140
|
authLoading: true,
|
|
2011
|
-
|
|
2012
|
-
login: () => {},
|
|
2013
|
-
logout: () => {},
|
|
2141
|
+
customer: null,
|
|
2142
|
+
login: async () => {},
|
|
2143
|
+
logout: async () => {},
|
|
2014
2144
|
});
|
|
2015
2145
|
|
|
2016
2146
|
export function useAuth() {
|
|
@@ -2042,26 +2172,46 @@ export function useCart() {
|
|
|
2042
2172
|
export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
2043
2173
|
const [storeInfo, setStoreInfo] = useState<StoreInfo | null>(null);
|
|
2044
2174
|
const [storeLoading, setStoreLoading] = useState(true);
|
|
2045
|
-
const [
|
|
2175
|
+
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
2176
|
+
const [customer, setCustomer] = useState<CustomerProfile | null>(null);
|
|
2046
2177
|
const [authLoading, setAuthLoading] = useState(true);
|
|
2047
2178
|
const [cart, setCart] = useState<Cart | null>(null);
|
|
2048
2179
|
const [cartLoading, setCartLoading] = useState(true);
|
|
2049
2180
|
|
|
2050
|
-
//
|
|
2181
|
+
// Check auth status via httpOnly cookie (server-side validation)
|
|
2182
|
+
const refreshAuth = useCallback(async () => {
|
|
2183
|
+
try {
|
|
2184
|
+
const status = await checkAuthStatus();
|
|
2185
|
+
setIsLoggedIn(status.isLoggedIn);
|
|
2186
|
+
setCustomer(status.isLoggedIn ? (status.customer as CustomerProfile) : null);
|
|
2187
|
+
} catch {
|
|
2188
|
+
setIsLoggedIn(false);
|
|
2189
|
+
setCustomer(null);
|
|
2190
|
+
} finally {
|
|
2191
|
+
setAuthLoading(false);
|
|
2192
|
+
}
|
|
2193
|
+
}, []);
|
|
2194
|
+
|
|
2195
|
+
// Initialize client, check auth, and fetch store info
|
|
2051
2196
|
useEffect(() => {
|
|
2052
2197
|
const client = initClient();
|
|
2053
|
-
|
|
2054
|
-
if
|
|
2055
|
-
|
|
2198
|
+
|
|
2199
|
+
// Optimistic check: if brainerce_logged_in cookie exists, assume logged in
|
|
2200
|
+
// while we validate the actual token server-side
|
|
2201
|
+
if (typeof document !== 'undefined' && document.cookie.includes('brainerce_logged_in=1')) {
|
|
2202
|
+
setIsLoggedIn(true);
|
|
2056
2203
|
}
|
|
2057
|
-
setAuthLoading(false);
|
|
2058
2204
|
|
|
2205
|
+
// Validate auth token server-side
|
|
2206
|
+
refreshAuth();
|
|
2207
|
+
|
|
2208
|
+
// Fetch store info (public, no auth needed)
|
|
2059
2209
|
client
|
|
2060
2210
|
.getStoreInfo()
|
|
2061
2211
|
.then(setStoreInfo)
|
|
2062
2212
|
.catch(console.error)
|
|
2063
2213
|
.finally(() => setStoreLoading(false));
|
|
2064
|
-
}, []);
|
|
2214
|
+
}, [refreshAuth]);
|
|
2065
2215
|
|
|
2066
2216
|
// Cart management
|
|
2067
2217
|
const refreshCart = useCallback(async () => {
|
|
@@ -2084,24 +2234,26 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2084
2234
|
|
|
2085
2235
|
useEffect(() => {
|
|
2086
2236
|
refreshCart();
|
|
2087
|
-
}, [refreshCart,
|
|
2237
|
+
}, [refreshCart, isLoggedIn]);
|
|
2088
2238
|
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
setToken(newToken);
|
|
2239
|
+
// Called after successful login (cookie already set by proxy)
|
|
2240
|
+
const login = useCallback(async () => {
|
|
2241
|
+
// Refresh auth state from server (reads httpOnly cookie)
|
|
2242
|
+
await refreshAuth();
|
|
2094
2243
|
|
|
2095
2244
|
// Merge guest session cart into customer cart
|
|
2245
|
+
const client = getClient();
|
|
2096
2246
|
client.syncCartOnLogin().catch(console.error);
|
|
2097
|
-
}, []);
|
|
2247
|
+
}, [refreshAuth]);
|
|
2248
|
+
|
|
2249
|
+
const logout = useCallback(async () => {
|
|
2250
|
+
// Clear httpOnly cookie server-side
|
|
2251
|
+
await proxyLogout();
|
|
2098
2252
|
|
|
2099
|
-
const logout = useCallback(() => {
|
|
2100
2253
|
const client = getClient();
|
|
2101
|
-
client.clearCustomerToken();
|
|
2102
2254
|
client.onLogout();
|
|
2103
|
-
|
|
2104
|
-
|
|
2255
|
+
setIsLoggedIn(false);
|
|
2256
|
+
setCustomer(null);
|
|
2105
2257
|
setCart(null);
|
|
2106
2258
|
refreshCart();
|
|
2107
2259
|
}, [refreshCart]);
|
|
@@ -2114,7 +2266,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2114
2266
|
|
|
2115
2267
|
return (
|
|
2116
2268
|
<StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
|
|
2117
|
-
<AuthContext.Provider value={{ isLoggedIn
|
|
2269
|
+
<AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
|
|
2118
2270
|
<CartContext.Provider
|
|
2119
2271
|
value={{ cart, cartLoading, refreshCart, itemCount, totals }}
|
|
2120
2272
|
>
|
|
@@ -3934,8 +4086,8 @@ export default function OrderConfirmationPage() {
|
|
|
3934
4086
|
import { useState } from 'react';
|
|
3935
4087
|
import { useRouter } from 'next/navigation';
|
|
3936
4088
|
import Link from 'next/link';
|
|
3937
|
-
import { getClient } from '@/lib/brainerce';
|
|
3938
4089
|
import { useAuth } from '@/providers/store-provider';
|
|
4090
|
+
import { proxyLogin } from '@/lib/auth';
|
|
3939
4091
|
import { LoginForm } from '@/components/auth/login-form';
|
|
3940
4092
|
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
3941
4093
|
import { useTranslations } from '@/lib/translations';
|
|
@@ -3949,15 +4101,16 @@ export default function LoginPage() {
|
|
|
3949
4101
|
async function handleLogin(email: string, password: string) {
|
|
3950
4102
|
try {
|
|
3951
4103
|
setError(null);
|
|
3952
|
-
const
|
|
3953
|
-
const result = await client.loginCustomer(email, password);
|
|
4104
|
+
const result = await proxyLogin(email, password);
|
|
3954
4105
|
|
|
3955
4106
|
if (result.requiresVerification) {
|
|
3956
|
-
|
|
4107
|
+
// Verification token is NOT the auth JWT \u2014 safe to pass in URL
|
|
4108
|
+
router.push('/verify-email');
|
|
3957
4109
|
return;
|
|
3958
4110
|
}
|
|
3959
4111
|
|
|
3960
|
-
auth
|
|
4112
|
+
// Cookie was set by the proxy; refresh auth state
|
|
4113
|
+
await auth.login();
|
|
3961
4114
|
router.push('/');
|
|
3962
4115
|
} catch (err) {
|
|
3963
4116
|
const message = err instanceof Error ? err.message : 'Login failed. Please try again.';
|
|
@@ -3993,8 +4146,8 @@ export default function LoginPage() {
|
|
|
3993
4146
|
import { useState } from 'react';
|
|
3994
4147
|
import { useRouter } from 'next/navigation';
|
|
3995
4148
|
import Link from 'next/link';
|
|
3996
|
-
import { getClient } from '@/lib/brainerce';
|
|
3997
4149
|
import { useAuth } from '@/providers/store-provider';
|
|
4150
|
+
import { proxyRegister } from '@/lib/auth';
|
|
3998
4151
|
import { RegisterForm } from '@/components/auth/register-form';
|
|
3999
4152
|
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
4000
4153
|
import { useTranslations } from '@/lib/translations';
|
|
@@ -4013,20 +4166,16 @@ export default function RegisterPage() {
|
|
|
4013
4166
|
}) {
|
|
4014
4167
|
try {
|
|
4015
4168
|
setError(null);
|
|
4016
|
-
const
|
|
4017
|
-
const result = await client.registerCustomer({
|
|
4018
|
-
firstName: data.firstName,
|
|
4019
|
-
lastName: data.lastName,
|
|
4020
|
-
email: data.email,
|
|
4021
|
-
password: data.password,
|
|
4022
|
-
});
|
|
4169
|
+
const result = await proxyRegister(data);
|
|
4023
4170
|
|
|
4024
4171
|
if (result.requiresVerification) {
|
|
4025
|
-
|
|
4172
|
+
// Cookie already set by proxy; verify-email uses it for auth
|
|
4173
|
+
router.push('/verify-email');
|
|
4026
4174
|
return;
|
|
4027
4175
|
}
|
|
4028
4176
|
|
|
4029
|
-
auth
|
|
4177
|
+
// Cookie was set by the proxy; refresh auth state
|
|
4178
|
+
await auth.login();
|
|
4030
4179
|
router.push('/');
|
|
4031
4180
|
} catch (err) {
|
|
4032
4181
|
const message = err instanceof Error ? err.message : 'Registration failed. Please try again.';
|
|
@@ -4060,10 +4209,10 @@ export default function RegisterPage() {
|
|
|
4060
4209
|
"src/app/verify-email/page.tsx": `'use client';
|
|
4061
4210
|
|
|
4062
4211
|
import { Suspense, useState, useRef, useEffect, useCallback } from 'react';
|
|
4063
|
-
import { useRouter
|
|
4212
|
+
import { useRouter } from 'next/navigation';
|
|
4064
4213
|
import Link from 'next/link';
|
|
4065
|
-
import { getClient } from '@/lib/brainerce';
|
|
4066
4214
|
import { useAuth } from '@/providers/store-provider';
|
|
4215
|
+
import { proxyVerifyEmail, proxyResendVerification } from '@/lib/auth';
|
|
4067
4216
|
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
4068
4217
|
import { useTranslations } from '@/lib/translations';
|
|
4069
4218
|
|
|
@@ -4072,10 +4221,8 @@ const RESEND_COOLDOWN_SECONDS = 60;
|
|
|
4072
4221
|
|
|
4073
4222
|
function VerifyEmailContent() {
|
|
4074
4223
|
const router = useRouter();
|
|
4075
|
-
const searchParams = useSearchParams();
|
|
4076
4224
|
const auth = useAuth();
|
|
4077
4225
|
|
|
4078
|
-
const token = searchParams.get('token');
|
|
4079
4226
|
const t = useTranslations('auth');
|
|
4080
4227
|
|
|
4081
4228
|
const [digits, setDigits] = useState<string[]>(Array(CODE_LENGTH).fill(''));
|
|
@@ -4103,18 +4250,17 @@ function VerifyEmailContent() {
|
|
|
4103
4250
|
|
|
4104
4251
|
const handleSubmit = useCallback(
|
|
4105
4252
|
async (code: string) => {
|
|
4106
|
-
if (
|
|
4253
|
+
if (code.length !== CODE_LENGTH || loading) return;
|
|
4107
4254
|
|
|
4108
4255
|
try {
|
|
4109
4256
|
setLoading(true);
|
|
4110
4257
|
setError(null);
|
|
4111
|
-
|
|
4112
|
-
const result = await
|
|
4258
|
+
// Auth token is in httpOnly cookie \u2014 proxy adds Authorization header
|
|
4259
|
+
const result = await proxyVerifyEmail(code);
|
|
4113
4260
|
|
|
4114
4261
|
if (result.verified) {
|
|
4115
|
-
//
|
|
4116
|
-
|
|
4117
|
-
auth.login(authToken);
|
|
4262
|
+
// Refresh auth state (cookie already set)
|
|
4263
|
+
await auth.login();
|
|
4118
4264
|
setSuccess('Email verified successfully! Redirecting...');
|
|
4119
4265
|
setTimeout(() => router.push('/'), 1500);
|
|
4120
4266
|
} else {
|
|
@@ -4128,7 +4274,7 @@ function VerifyEmailContent() {
|
|
|
4128
4274
|
setLoading(false);
|
|
4129
4275
|
}
|
|
4130
4276
|
},
|
|
4131
|
-
[
|
|
4277
|
+
[loading, auth, router]
|
|
4132
4278
|
);
|
|
4133
4279
|
|
|
4134
4280
|
function handleDigitChange(index: number, value: string) {
|
|
@@ -4182,13 +4328,12 @@ function VerifyEmailContent() {
|
|
|
4182
4328
|
}
|
|
4183
4329
|
|
|
4184
4330
|
async function handleResend() {
|
|
4185
|
-
if (
|
|
4331
|
+
if (resending || cooldown > 0) return;
|
|
4186
4332
|
|
|
4187
4333
|
try {
|
|
4188
4334
|
setResending(true);
|
|
4189
4335
|
setError(null);
|
|
4190
|
-
|
|
4191
|
-
await client.resendVerificationEmail(token);
|
|
4336
|
+
await proxyResendVerification();
|
|
4192
4337
|
setSuccess('Verification code sent! Check your email.');
|
|
4193
4338
|
setCooldown(RESEND_COOLDOWN_SECONDS);
|
|
4194
4339
|
// Clear digits for fresh entry
|
|
@@ -4208,37 +4353,6 @@ function VerifyEmailContent() {
|
|
|
4208
4353
|
handleSubmit(code);
|
|
4209
4354
|
}
|
|
4210
4355
|
|
|
4211
|
-
// No token provided
|
|
4212
|
-
if (!token) {
|
|
4213
|
-
return (
|
|
4214
|
-
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
4215
|
-
<div className="w-full max-w-md space-y-4 text-center">
|
|
4216
|
-
<svg
|
|
4217
|
-
className="text-muted-foreground mx-auto h-12 w-12"
|
|
4218
|
-
fill="none"
|
|
4219
|
-
viewBox="0 0 24 24"
|
|
4220
|
-
stroke="currentColor"
|
|
4221
|
-
>
|
|
4222
|
-
<path
|
|
4223
|
-
strokeLinecap="round"
|
|
4224
|
-
strokeLinejoin="round"
|
|
4225
|
-
strokeWidth={1.5}
|
|
4226
|
-
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
4227
|
-
/>
|
|
4228
|
-
</svg>
|
|
4229
|
-
<h1 className="text-foreground text-2xl font-bold">{t('verificationInvalid')}</h1>
|
|
4230
|
-
<p className="text-muted-foreground text-sm">{t('verificationInvalidDesc')}</p>
|
|
4231
|
-
<Link
|
|
4232
|
-
href="/register"
|
|
4233
|
-
className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
|
|
4234
|
-
>
|
|
4235
|
-
{t('goToRegister')}
|
|
4236
|
-
</Link>
|
|
4237
|
-
</div>
|
|
4238
|
-
</div>
|
|
4239
|
-
);
|
|
4240
|
-
}
|
|
4241
|
-
|
|
4242
4356
|
return (
|
|
4243
4357
|
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
4244
4358
|
<div className="w-full max-w-md space-y-6">
|
|
@@ -4368,8 +4482,8 @@ function OAuthCallbackContent() {
|
|
|
4368
4482
|
const t = useTranslations('auth');
|
|
4369
4483
|
|
|
4370
4484
|
const oauthSuccess = searchParams.get('oauth_success');
|
|
4371
|
-
const token = searchParams.get('token');
|
|
4372
4485
|
const oauthError = searchParams.get('oauth_error');
|
|
4486
|
+
// Token is no longer in URL \u2014 it was set as httpOnly cookie by /api/auth/oauth-callback
|
|
4373
4487
|
|
|
4374
4488
|
useEffect(() => {
|
|
4375
4489
|
// Prevent double-processing in React StrictMode
|
|
@@ -4381,13 +4495,15 @@ function OAuthCallbackContent() {
|
|
|
4381
4495
|
return;
|
|
4382
4496
|
}
|
|
4383
4497
|
|
|
4384
|
-
if (oauthSuccess === 'true'
|
|
4385
|
-
auth
|
|
4386
|
-
|
|
4498
|
+
if (oauthSuccess === 'true') {
|
|
4499
|
+
// Cookie was already set by the API route; refresh auth state
|
|
4500
|
+
auth.login().then(() => {
|
|
4501
|
+
router.push('/');
|
|
4502
|
+
});
|
|
4387
4503
|
} else {
|
|
4388
4504
|
setError(t('authFailedDesc'));
|
|
4389
4505
|
}
|
|
4390
|
-
}, [oauthSuccess,
|
|
4506
|
+
}, [oauthSuccess, oauthError, auth, router, t]);
|
|
4391
4507
|
|
|
4392
4508
|
if (error) {
|
|
4393
4509
|
return (
|
|
@@ -4554,6 +4670,348 @@ export default function AccountPage() {
|
|
|
4554
4670
|
</div>
|
|
4555
4671
|
);
|
|
4556
4672
|
}
|
|
4673
|
+
`,
|
|
4674
|
+
"src/app/api/store/[...path]/route.ts": `import { NextRequest, NextResponse } from 'next/server';
|
|
4675
|
+
import { cookies } from 'next/headers';
|
|
4676
|
+
|
|
4677
|
+
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
4678
|
+
/\\/$/,
|
|
4679
|
+
''
|
|
4680
|
+
);
|
|
4681
|
+
|
|
4682
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4683
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
4684
|
+
const CSRF_HEADER = 'x-requested-with';
|
|
4685
|
+
const CSRF_VALUE = 'brainerce';
|
|
4686
|
+
|
|
4687
|
+
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
|
|
4688
|
+
|
|
4689
|
+
/** Auth endpoints whose responses contain tokens to intercept */
|
|
4690
|
+
const AUTH_ENDPOINTS = ['customers/login', 'customers/register', 'customers/verify-email'];
|
|
4691
|
+
|
|
4692
|
+
function isAuthEndpoint(path: string): boolean {
|
|
4693
|
+
return AUTH_ENDPOINTS.some((ep) => path.endsWith(ep));
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4696
|
+
function isSecure(): boolean {
|
|
4697
|
+
return process.env.NODE_ENV === 'production';
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4700
|
+
function setAuthCookies(response: NextResponse, token: string): void {
|
|
4701
|
+
response.cookies.set(TOKEN_COOKIE, token, {
|
|
4702
|
+
httpOnly: true,
|
|
4703
|
+
secure: isSecure(),
|
|
4704
|
+
sameSite: 'lax',
|
|
4705
|
+
path: '/',
|
|
4706
|
+
maxAge: COOKIE_MAX_AGE,
|
|
4707
|
+
});
|
|
4708
|
+
response.cookies.set(LOGGED_IN_COOKIE, '1', {
|
|
4709
|
+
httpOnly: false,
|
|
4710
|
+
secure: isSecure(),
|
|
4711
|
+
sameSite: 'lax',
|
|
4712
|
+
path: '/',
|
|
4713
|
+
maxAge: COOKIE_MAX_AGE,
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
function clearAuthCookies(response: NextResponse): void {
|
|
4718
|
+
response.cookies.delete(TOKEN_COOKIE);
|
|
4719
|
+
response.cookies.delete(LOGGED_IN_COOKIE);
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4722
|
+
async function proxyRequest(
|
|
4723
|
+
request: NextRequest,
|
|
4724
|
+
params: { path: string[] }
|
|
4725
|
+
): Promise<NextResponse> {
|
|
4726
|
+
const method = request.method;
|
|
4727
|
+
|
|
4728
|
+
// CSRF protection for mutating requests
|
|
4729
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
4730
|
+
const csrfHeader = request.headers.get(CSRF_HEADER);
|
|
4731
|
+
if (csrfHeader !== CSRF_VALUE) {
|
|
4732
|
+
return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 });
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4736
|
+
// Build backend URL from path segments
|
|
4737
|
+
const pathSegments = params.path.join('/');
|
|
4738
|
+
const backendUrl = new URL(\`\${BACKEND_URL}/\${pathSegments}\`);
|
|
4739
|
+
|
|
4740
|
+
// Forward query parameters
|
|
4741
|
+
request.nextUrl.searchParams.forEach((value, key) => {
|
|
4742
|
+
backendUrl.searchParams.set(key, value);
|
|
4743
|
+
});
|
|
4744
|
+
|
|
4745
|
+
// Build headers for backend request
|
|
4746
|
+
const headers: Record<string, string> = {
|
|
4747
|
+
'Content-Type': 'application/json',
|
|
4748
|
+
};
|
|
4749
|
+
|
|
4750
|
+
// Forward SDK version header if present
|
|
4751
|
+
const sdkVersion = request.headers.get('x-sdk-version');
|
|
4752
|
+
if (sdkVersion) {
|
|
4753
|
+
headers['X-SDK-Version'] = sdkVersion;
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
// Add auth token from httpOnly cookie
|
|
4757
|
+
const cookieStore = await cookies();
|
|
4758
|
+
const tokenCookie = cookieStore.get(TOKEN_COOKIE);
|
|
4759
|
+
if (tokenCookie?.value) {
|
|
4760
|
+
headers['Authorization'] = \`Bearer \${tokenCookie.value}\`;
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
// Forward request body for non-GET requests
|
|
4764
|
+
let body: string | undefined;
|
|
4765
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
4766
|
+
try {
|
|
4767
|
+
body = await request.text();
|
|
4768
|
+
} catch {
|
|
4769
|
+
// No body
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4773
|
+
// Proxy the request to backend
|
|
4774
|
+
let backendResponse: Response;
|
|
4775
|
+
try {
|
|
4776
|
+
backendResponse = await fetch(backendUrl.toString(), {
|
|
4777
|
+
method,
|
|
4778
|
+
headers,
|
|
4779
|
+
body,
|
|
4780
|
+
});
|
|
4781
|
+
} catch (error) {
|
|
4782
|
+
return NextResponse.json({ error: 'Backend service unavailable' }, { status: 502 });
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4785
|
+
// Read response body
|
|
4786
|
+
const responseText = await backendResponse.text();
|
|
4787
|
+
|
|
4788
|
+
// For auth endpoints: intercept token, set cookie, strip token from response
|
|
4789
|
+
if (backendResponse.ok && method === 'POST' && isAuthEndpoint(pathSegments)) {
|
|
4790
|
+
try {
|
|
4791
|
+
const data = JSON.parse(responseText);
|
|
4792
|
+
if (data.token) {
|
|
4793
|
+
const token = data.token;
|
|
4794
|
+
|
|
4795
|
+
// Strip token from client response
|
|
4796
|
+
const { token: _stripped, ...safeData } = data;
|
|
4797
|
+
|
|
4798
|
+
const response = NextResponse.json(safeData, {
|
|
4799
|
+
status: backendResponse.status,
|
|
4800
|
+
});
|
|
4801
|
+
setAuthCookies(response, token);
|
|
4802
|
+
return response;
|
|
4803
|
+
}
|
|
4804
|
+
} catch {
|
|
4805
|
+
// Not JSON or no token field \u2014 pass through
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
|
|
4809
|
+
// Handle 401 responses: clear auth cookies
|
|
4810
|
+
if (backendResponse.status === 401 && tokenCookie?.value) {
|
|
4811
|
+
const response = new NextResponse(responseText, {
|
|
4812
|
+
status: backendResponse.status,
|
|
4813
|
+
headers: {
|
|
4814
|
+
'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
|
|
4815
|
+
},
|
|
4816
|
+
});
|
|
4817
|
+
clearAuthCookies(response);
|
|
4818
|
+
return response;
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
// Pass through response as-is
|
|
4822
|
+
return new NextResponse(responseText, {
|
|
4823
|
+
status: backendResponse.status,
|
|
4824
|
+
headers: {
|
|
4825
|
+
'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
|
|
4826
|
+
},
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
export async function GET(
|
|
4831
|
+
request: NextRequest,
|
|
4832
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
4833
|
+
) {
|
|
4834
|
+
return proxyRequest(request, await params);
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
export async function POST(
|
|
4838
|
+
request: NextRequest,
|
|
4839
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
4840
|
+
) {
|
|
4841
|
+
return proxyRequest(request, await params);
|
|
4842
|
+
}
|
|
4843
|
+
|
|
4844
|
+
export async function PUT(
|
|
4845
|
+
request: NextRequest,
|
|
4846
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
4847
|
+
) {
|
|
4848
|
+
return proxyRequest(request, await params);
|
|
4849
|
+
}
|
|
4850
|
+
|
|
4851
|
+
export async function PATCH(
|
|
4852
|
+
request: NextRequest,
|
|
4853
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
4854
|
+
) {
|
|
4855
|
+
return proxyRequest(request, await params);
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
export async function DELETE(
|
|
4859
|
+
request: NextRequest,
|
|
4860
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
4861
|
+
) {
|
|
4862
|
+
return proxyRequest(request, await params);
|
|
4863
|
+
}
|
|
4864
|
+
`,
|
|
4865
|
+
"src/app/api/auth/oauth-callback/route.ts": `import { NextRequest, NextResponse } from 'next/server';
|
|
4866
|
+
|
|
4867
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4868
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
4869
|
+
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
|
|
4870
|
+
|
|
4871
|
+
function isSecure(): boolean {
|
|
4872
|
+
return process.env.NODE_ENV === 'production';
|
|
4873
|
+
}
|
|
4874
|
+
|
|
4875
|
+
/**
|
|
4876
|
+
* OAuth callback handler.
|
|
4877
|
+
* The backend redirects here with ?token=jwt&oauth_success=true after OAuth code exchange.
|
|
4878
|
+
* We set the httpOnly cookie and redirect to the client-side callback page (without the token).
|
|
4879
|
+
*/
|
|
4880
|
+
export async function GET(request: NextRequest) {
|
|
4881
|
+
const { searchParams } = request.nextUrl;
|
|
4882
|
+
const token = searchParams.get('token');
|
|
4883
|
+
const oauthSuccess = searchParams.get('oauth_success');
|
|
4884
|
+
const oauthError = searchParams.get('oauth_error');
|
|
4885
|
+
|
|
4886
|
+
// Build redirect URL to client-side callback page
|
|
4887
|
+
const redirectUrl = new URL('/auth/callback', request.url);
|
|
4888
|
+
|
|
4889
|
+
if (oauthError) {
|
|
4890
|
+
redirectUrl.searchParams.set('oauth_error', oauthError);
|
|
4891
|
+
return NextResponse.redirect(redirectUrl);
|
|
4892
|
+
}
|
|
4893
|
+
|
|
4894
|
+
if (oauthSuccess === 'true' && token) {
|
|
4895
|
+
redirectUrl.searchParams.set('oauth_success', 'true');
|
|
4896
|
+
|
|
4897
|
+
const response = NextResponse.redirect(redirectUrl);
|
|
4898
|
+
|
|
4899
|
+
// Set httpOnly cookie with the token
|
|
4900
|
+
response.cookies.set(TOKEN_COOKIE, token, {
|
|
4901
|
+
httpOnly: true,
|
|
4902
|
+
secure: isSecure(),
|
|
4903
|
+
sameSite: 'lax',
|
|
4904
|
+
path: '/',
|
|
4905
|
+
maxAge: COOKIE_MAX_AGE,
|
|
4906
|
+
});
|
|
4907
|
+
|
|
4908
|
+
// Set indicator cookie (readable by client JS)
|
|
4909
|
+
response.cookies.set(LOGGED_IN_COOKIE, '1', {
|
|
4910
|
+
httpOnly: false,
|
|
4911
|
+
secure: isSecure(),
|
|
4912
|
+
sameSite: 'lax',
|
|
4913
|
+
path: '/',
|
|
4914
|
+
maxAge: COOKIE_MAX_AGE,
|
|
4915
|
+
});
|
|
4916
|
+
|
|
4917
|
+
return response;
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
// Fallback: no token or success flag
|
|
4921
|
+
redirectUrl.searchParams.set('oauth_error', 'Authentication failed');
|
|
4922
|
+
return NextResponse.redirect(redirectUrl);
|
|
4923
|
+
}
|
|
4924
|
+
`,
|
|
4925
|
+
"src/app/api/auth/me/route.ts": `import { NextResponse } from 'next/server';
|
|
4926
|
+
import { cookies } from 'next/headers';
|
|
4927
|
+
|
|
4928
|
+
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
4929
|
+
/\\/$/,
|
|
4930
|
+
''
|
|
4931
|
+
);
|
|
4932
|
+
|
|
4933
|
+
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
|
|
4934
|
+
|
|
4935
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4936
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
4937
|
+
|
|
4938
|
+
/**
|
|
4939
|
+
* Auth status check endpoint.
|
|
4940
|
+
* Reads the httpOnly cookie, validates against backend, returns auth state.
|
|
4941
|
+
*/
|
|
4942
|
+
export async function GET() {
|
|
4943
|
+
const cookieStore = await cookies();
|
|
4944
|
+
const tokenCookie = cookieStore.get(TOKEN_COOKIE);
|
|
4945
|
+
|
|
4946
|
+
if (!tokenCookie?.value) {
|
|
4947
|
+
return NextResponse.json({ isLoggedIn: false });
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
try {
|
|
4951
|
+
// Validate token by calling backend profile endpoint
|
|
4952
|
+
const response = await fetch(\`\${BACKEND_URL}/api/vc/\${CONNECTION_ID}/customers/me\`, {
|
|
4953
|
+
headers: {
|
|
4954
|
+
Authorization: \`Bearer \${tokenCookie.value}\`,
|
|
4955
|
+
'Content-Type': 'application/json',
|
|
4956
|
+
},
|
|
4957
|
+
});
|
|
4958
|
+
|
|
4959
|
+
if (!response.ok) {
|
|
4960
|
+
// Token is invalid or expired \u2014 clear cookies
|
|
4961
|
+
const res = NextResponse.json({ isLoggedIn: false });
|
|
4962
|
+
res.cookies.delete(TOKEN_COOKIE);
|
|
4963
|
+
res.cookies.delete(LOGGED_IN_COOKIE);
|
|
4964
|
+
return res;
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
const customer = await response.json();
|
|
4968
|
+
return NextResponse.json({ isLoggedIn: true, customer });
|
|
4969
|
+
} catch {
|
|
4970
|
+
// Backend unreachable \u2014 don't clear cookies, might be temporary
|
|
4971
|
+
return NextResponse.json({ isLoggedIn: false, error: 'Service unavailable' }, { status: 503 });
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
`,
|
|
4975
|
+
"src/app/api/auth/logout/route.ts": `import { NextResponse } from 'next/server';
|
|
4976
|
+
|
|
4977
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4978
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
4979
|
+
|
|
4980
|
+
/**
|
|
4981
|
+
* Logout endpoint. Clears auth cookies.
|
|
4982
|
+
*/
|
|
4983
|
+
export async function POST() {
|
|
4984
|
+
const response = NextResponse.json({ success: true });
|
|
4985
|
+
response.cookies.delete(TOKEN_COOKIE);
|
|
4986
|
+
response.cookies.delete(LOGGED_IN_COOKIE);
|
|
4987
|
+
return response;
|
|
4988
|
+
}
|
|
4989
|
+
`,
|
|
4990
|
+
"src/middleware.ts": `import { NextRequest, NextResponse } from 'next/server';
|
|
4991
|
+
|
|
4992
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4993
|
+
|
|
4994
|
+
/** Routes that require customer authentication */
|
|
4995
|
+
const PROTECTED_PATHS = ['/account'];
|
|
4996
|
+
|
|
4997
|
+
export function middleware(request: NextRequest) {
|
|
4998
|
+
const { pathname } = request.nextUrl;
|
|
4999
|
+
const isProtected = PROTECTED_PATHS.some((p) => pathname.startsWith(p));
|
|
5000
|
+
|
|
5001
|
+
if (isProtected) {
|
|
5002
|
+
const token = request.cookies.get(TOKEN_COOKIE);
|
|
5003
|
+
if (!token?.value) {
|
|
5004
|
+
const loginUrl = new URL('/login', request.url);
|
|
5005
|
+
return NextResponse.redirect(loginUrl);
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
5008
|
+
|
|
5009
|
+
return NextResponse.next();
|
|
5010
|
+
}
|
|
5011
|
+
|
|
5012
|
+
export const config = {
|
|
5013
|
+
matcher: ['/account/:path*'],
|
|
5014
|
+
};
|
|
4557
5015
|
`,
|
|
4558
5016
|
"src/components/products/product-card.tsx": `'use client';
|
|
4559
5017
|
|
|
@@ -5885,8 +6343,11 @@ declare global {
|
|
|
5885
6343
|
}
|
|
5886
6344
|
|
|
5887
6345
|
// Payment SDK script URLs \u2014 resolved per provider
|
|
6346
|
+
// SRI hashes must be regenerated when the provider updates their SDK
|
|
5888
6347
|
const PAYMENT_SDK_URL = 'https://cdn.meshulam.co.il/sdk/gs.min.js';
|
|
6348
|
+
const PAYMENT_SDK_SRI = 'sha384-e1OYzZERZLgbR8Zw1I4Ww/J7qXGyEsZb7LF0z5W/sbnTsFwtxop7sKZsLGNp6CB1';
|
|
5889
6349
|
const APPLE_PAY_SDK_URL = 'https://meshulam.co.il/_media/js/apple_pay_sdk/sdk.min.js';
|
|
6350
|
+
const APPLE_PAY_SDK_SRI = 'sha384-CG67HXmWBeyuRR/jqFsYr6dGEMFyuZw3/hRmcyACJRYHGb/1ebEkQZyzdJXDc9/u';
|
|
5890
6351
|
|
|
5891
6352
|
interface PaymentStepProps {
|
|
5892
6353
|
checkoutId: string;
|
|
@@ -5897,7 +6358,7 @@ interface PaymentStepProps {
|
|
|
5897
6358
|
* Load a script tag if not already present in the DOM.
|
|
5898
6359
|
* Resolves when the script loads (or immediately if already present).
|
|
5899
6360
|
*/
|
|
5900
|
-
function loadScript(src: string, optional = false): Promise<void> {
|
|
6361
|
+
function loadScript(src: string, optional = false, integrity?: string): Promise<void> {
|
|
5901
6362
|
return new Promise((resolve) => {
|
|
5902
6363
|
if (document.querySelector(\`script[src="\${src}"]\`)) {
|
|
5903
6364
|
resolve();
|
|
@@ -5906,6 +6367,10 @@ function loadScript(src: string, optional = false): Promise<void> {
|
|
|
5906
6367
|
const script = document.createElement('script');
|
|
5907
6368
|
script.src = src;
|
|
5908
6369
|
script.async = true;
|
|
6370
|
+
if (integrity) {
|
|
6371
|
+
script.integrity = integrity;
|
|
6372
|
+
script.crossOrigin = 'anonymous';
|
|
6373
|
+
}
|
|
5909
6374
|
script.onload = () => resolve();
|
|
5910
6375
|
script.onerror = () => {
|
|
5911
6376
|
if (optional) {
|
|
@@ -5998,10 +6463,10 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
5998
6463
|
|
|
5999
6464
|
async function loadSdkScripts() {
|
|
6000
6465
|
// Load Apple Pay SDK (optional)
|
|
6001
|
-
await loadScript(APPLE_PAY_SDK_URL, true);
|
|
6466
|
+
await loadScript(APPLE_PAY_SDK_URL, true, APPLE_PAY_SDK_SRI);
|
|
6002
6467
|
|
|
6003
6468
|
// Load payment SDK script
|
|
6004
|
-
await loadScript(PAYMENT_SDK_URL);
|
|
6469
|
+
await loadScript(PAYMENT_SDK_URL, false, PAYMENT_SDK_SRI);
|
|
6005
6470
|
|
|
6006
6471
|
// Wait for SDK global to be set by the script
|
|
6007
6472
|
const available = await waitForPaymentSdkGlobal();
|
|
@@ -6664,7 +7129,7 @@ export function OAuthButtons({ className }: OAuthButtonsProps) {
|
|
|
6664
7129
|
try {
|
|
6665
7130
|
setRedirecting(provider);
|
|
6666
7131
|
const client = getClient();
|
|
6667
|
-
const redirectUrl = window.location.origin + '/auth/callback';
|
|
7132
|
+
const redirectUrl = window.location.origin + '/api/auth/oauth-callback';
|
|
6668
7133
|
const result = await client.getOAuthAuthorizeUrl(provider, { redirectUrl });
|
|
6669
7134
|
window.location.href = result.authorizationUrl;
|
|
6670
7135
|
} catch (err) {
|
|
@@ -8299,6 +8764,39 @@ my-store/
|
|
|
8299
8764
|
| \`app/auth/callback/page.tsx\` | Extracts OAuth token from URL params, stores in localStorage |
|
|
8300
8765
|
| \`app/account/page.tsx\` | Protected page: getMyProfile() + getMyOrders() |
|
|
8301
8766
|
| \`components/Header.tsx\` | Site header with nav, cart count badge, search autocomplete |
|
|
8767
|
+
|
|
8768
|
+
## Content Security Policy (CSP)
|
|
8769
|
+
|
|
8770
|
+
Payment SDKs load iframes and scripts from external domains. Add these CSP headers in \`next.config.js\`:
|
|
8771
|
+
|
|
8772
|
+
\`\`\`js
|
|
8773
|
+
// next.config.js
|
|
8774
|
+
module.exports = {
|
|
8775
|
+
async headers() {
|
|
8776
|
+
return [{
|
|
8777
|
+
source: '/(.*)',
|
|
8778
|
+
headers: [{
|
|
8779
|
+
key: 'Content-Security-Policy',
|
|
8780
|
+
value: [
|
|
8781
|
+
"default-src 'self'",
|
|
8782
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.meshulam.co.il https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://js.stripe.com https://pay.google.com",
|
|
8783
|
+
"style-src 'self' 'unsafe-inline'",
|
|
8784
|
+
"img-src 'self' data: blob: https:",
|
|
8785
|
+
"font-src 'self' data:",
|
|
8786
|
+
"frame-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com",
|
|
8787
|
+
"connect-src 'self' https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://google.com https://pay.google.com https://*.stripe.com https://*.creditguard.co.il",
|
|
8788
|
+
"worker-src 'self' blob:",
|
|
8789
|
+
].join('; '),
|
|
8790
|
+
}],
|
|
8791
|
+
}];
|
|
8792
|
+
},
|
|
8793
|
+
};
|
|
8794
|
+
\`\`\`
|
|
8795
|
+
|
|
8796
|
+
**Required domains by provider:**
|
|
8797
|
+
- **Grow**: \`*.meshulam.co.il\`, \`grow.link\`, \`*.grow.link\`, \`*.grow.security\`, \`*.creditguard.co.il\`
|
|
8798
|
+
- **Stripe**: \`js.stripe.com\`, \`hooks.stripe.com\`, \`*.stripe.com\`
|
|
8799
|
+
- **Google Pay**: \`pay.google.com\`, \`google.com\`
|
|
8302
8800
|
`;
|
|
8303
8801
|
|
|
8304
8802
|
// src/resources/project-template.ts
|