@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.mjs
CHANGED
|
@@ -1867,39 +1867,24 @@ var EMBEDDED_TEMPLATES = {
|
|
|
1867
1867
|
"src/lib/brainerce.ts.ejs": `import { BrainerceClient } from 'brainerce';
|
|
1868
1868
|
|
|
1869
1869
|
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || 'vc_YOUR_CONNECTION_ID';
|
|
1870
|
-
const API_URL = process.env.NEXT_PUBLIC_BRAINERCE_API_URL || 'https://api.brainerce.com';
|
|
1871
1870
|
|
|
1872
|
-
// Singleton SDK client
|
|
1871
|
+
// Singleton SDK client \u2014 routes through same-origin BFF proxy for httpOnly cookie auth
|
|
1873
1872
|
let clientInstance: BrainerceClient | null = null;
|
|
1874
1873
|
|
|
1875
1874
|
export function getClient(): BrainerceClient {
|
|
1876
1875
|
if (!clientInstance) {
|
|
1877
1876
|
clientInstance = new BrainerceClient({
|
|
1878
1877
|
connectionId: CONNECTION_ID,
|
|
1879
|
-
baseUrl:
|
|
1878
|
+
baseUrl: '/api/store', // same-origin proxy handles auth via httpOnly cookie
|
|
1879
|
+
proxyMode: true, // skip client-side token checks; proxy adds Authorization header
|
|
1880
1880
|
});
|
|
1881
1881
|
}
|
|
1882
1882
|
return clientInstance;
|
|
1883
1883
|
}
|
|
1884
1884
|
|
|
1885
|
-
//
|
|
1886
|
-
const TOKEN_KEY = 'brainerce_customer_token';
|
|
1885
|
+
// Cart ID helpers (not a security token \u2014 safe in localStorage)
|
|
1887
1886
|
const CART_ID_KEY = 'brainerce_cart_id';
|
|
1888
1887
|
|
|
1889
|
-
export function getStoredToken(): string | null {
|
|
1890
|
-
if (typeof window === 'undefined') return null;
|
|
1891
|
-
return localStorage.getItem(TOKEN_KEY);
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
export function setStoredToken(token: string | null): void {
|
|
1895
|
-
if (typeof window === 'undefined') return;
|
|
1896
|
-
if (token) {
|
|
1897
|
-
localStorage.setItem(TOKEN_KEY, token);
|
|
1898
|
-
} else {
|
|
1899
|
-
localStorage.removeItem(TOKEN_KEY);
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
1888
|
export function getStoredCartId(): string | null {
|
|
1904
1889
|
if (typeof window === 'undefined') return null;
|
|
1905
1890
|
return localStorage.getItem(CART_ID_KEY);
|
|
@@ -1914,14 +1899,158 @@ export function setStoredCartId(cartId: string | null): void {
|
|
|
1914
1899
|
}
|
|
1915
1900
|
}
|
|
1916
1901
|
|
|
1917
|
-
// Initialize client
|
|
1902
|
+
// Initialize client (no token hydration \u2014 auth handled by httpOnly cookie)
|
|
1918
1903
|
export function initClient(): BrainerceClient {
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1904
|
+
return getClient();
|
|
1905
|
+
}
|
|
1906
|
+
`,
|
|
1907
|
+
"src/lib/auth.ts": `/**
|
|
1908
|
+
* Client-side auth helpers that call the BFF proxy API routes.
|
|
1909
|
+
* All mutating requests include the CSRF header.
|
|
1910
|
+
* The token is managed server-side via httpOnly cookies \u2014 never exposed to JS.
|
|
1911
|
+
*/
|
|
1912
|
+
|
|
1913
|
+
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
|
|
1914
|
+
|
|
1915
|
+
const CSRF_HEADERS: Record<string, string> = {
|
|
1916
|
+
'Content-Type': 'application/json',
|
|
1917
|
+
'X-Requested-With': 'brainerce',
|
|
1918
|
+
};
|
|
1919
|
+
|
|
1920
|
+
interface LoginResult {
|
|
1921
|
+
customer: {
|
|
1922
|
+
id: string;
|
|
1923
|
+
email: string;
|
|
1924
|
+
firstName?: string;
|
|
1925
|
+
lastName?: string;
|
|
1926
|
+
emailVerified: boolean;
|
|
1927
|
+
};
|
|
1928
|
+
expiresAt: string;
|
|
1929
|
+
requiresVerification?: boolean;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
interface RegisterResult {
|
|
1933
|
+
customer: {
|
|
1934
|
+
id: string;
|
|
1935
|
+
email: string;
|
|
1936
|
+
firstName?: string;
|
|
1937
|
+
lastName?: string;
|
|
1938
|
+
emailVerified: boolean;
|
|
1939
|
+
};
|
|
1940
|
+
expiresAt: string;
|
|
1941
|
+
requiresVerification?: boolean;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
interface AuthStatus {
|
|
1945
|
+
isLoggedIn: boolean;
|
|
1946
|
+
customer?: {
|
|
1947
|
+
id: string;
|
|
1948
|
+
email: string;
|
|
1949
|
+
firstName?: string;
|
|
1950
|
+
lastName?: string;
|
|
1951
|
+
phone?: string;
|
|
1952
|
+
emailVerified: boolean;
|
|
1953
|
+
};
|
|
1954
|
+
error?: string;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
interface VerifyEmailResult {
|
|
1958
|
+
verified: boolean;
|
|
1959
|
+
message?: string;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
async function handleResponse<T>(response: Response): Promise<T> {
|
|
1963
|
+
const data = await response.json();
|
|
1964
|
+
if (!response.ok) {
|
|
1965
|
+
throw new Error(data.message || data.error || \`Request failed (\${response.status})\`);
|
|
1923
1966
|
}
|
|
1924
|
-
return
|
|
1967
|
+
return data as T;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/**
|
|
1971
|
+
* Login via BFF proxy. The proxy sets the httpOnly cookie on success.
|
|
1972
|
+
*/
|
|
1973
|
+
export async function proxyLogin(email: string, password: string): Promise<LoginResult> {
|
|
1974
|
+
const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/login\`, {
|
|
1975
|
+
method: 'POST',
|
|
1976
|
+
headers: CSRF_HEADERS,
|
|
1977
|
+
body: JSON.stringify({ email, password }),
|
|
1978
|
+
});
|
|
1979
|
+
return handleResponse<LoginResult>(response);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/**
|
|
1983
|
+
* Register via BFF proxy. The proxy sets the httpOnly cookie on success.
|
|
1984
|
+
*/
|
|
1985
|
+
export async function proxyRegister(data: {
|
|
1986
|
+
firstName: string;
|
|
1987
|
+
lastName: string;
|
|
1988
|
+
email: string;
|
|
1989
|
+
password: string;
|
|
1990
|
+
}): Promise<RegisterResult> {
|
|
1991
|
+
const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/register\`, {
|
|
1992
|
+
method: 'POST',
|
|
1993
|
+
headers: CSRF_HEADERS,
|
|
1994
|
+
body: JSON.stringify(data),
|
|
1995
|
+
});
|
|
1996
|
+
return handleResponse<RegisterResult>(response);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* Check auth status. Reads httpOnly cookie server-side and validates with backend.
|
|
2001
|
+
*/
|
|
2002
|
+
export async function checkAuthStatus(): Promise<AuthStatus> {
|
|
2003
|
+
const response = await fetch('/api/auth/me');
|
|
2004
|
+
return response.json();
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Logout. Clears httpOnly auth cookies server-side.
|
|
2009
|
+
*/
|
|
2010
|
+
export async function proxyLogout(): Promise<void> {
|
|
2011
|
+
await fetch('/api/auth/logout', {
|
|
2012
|
+
method: 'POST',
|
|
2013
|
+
headers: { 'X-Requested-With': 'brainerce' },
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
/**
|
|
2018
|
+
* Verify email via BFF proxy. The auth token is in the httpOnly cookie (set during login/register).
|
|
2019
|
+
* The proxy adds the Authorization header automatically.
|
|
2020
|
+
*/
|
|
2021
|
+
export async function proxyVerifyEmail(code: string): Promise<VerifyEmailResult> {
|
|
2022
|
+
const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/verify-email\`, {
|
|
2023
|
+
method: 'POST',
|
|
2024
|
+
headers: CSRF_HEADERS,
|
|
2025
|
+
body: JSON.stringify({ code }),
|
|
2026
|
+
});
|
|
2027
|
+
return handleResponse<VerifyEmailResult>(response);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* Resend verification email via BFF proxy.
|
|
2032
|
+
* Uses the auth token from the httpOnly cookie.
|
|
2033
|
+
*/
|
|
2034
|
+
export async function proxyResendVerification(): Promise<{ message: string }> {
|
|
2035
|
+
const response = await fetch(\`/api/store/api/vc/\${CONNECTION_ID}/customers/resend-verification\`, {
|
|
2036
|
+
method: 'POST',
|
|
2037
|
+
headers: CSRF_HEADERS,
|
|
2038
|
+
});
|
|
2039
|
+
return handleResponse<{ message: string }>(response);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* Reset password via BFF proxy.
|
|
2044
|
+
* The reset token is in an httpOnly cookie (set by /api/auth/reset-callback when the user
|
|
2045
|
+
* clicked the email link). The proxy reads it server-side \u2014 the token never reaches client JS.
|
|
2046
|
+
*/
|
|
2047
|
+
export async function proxyResetPassword(newPassword: string): Promise<{ message: string }> {
|
|
2048
|
+
const response = await fetch('/api/auth/reset-password', {
|
|
2049
|
+
method: 'POST',
|
|
2050
|
+
headers: CSRF_HEADERS,
|
|
2051
|
+
body: JSON.stringify({ newPassword }),
|
|
2052
|
+
});
|
|
2053
|
+
return handleResponse<{ message: string }>(response);
|
|
1925
2054
|
}
|
|
1926
2055
|
`,
|
|
1927
2056
|
"src/lib/utils.ts": `import { clsx, type ClassValue } from 'clsx';
|
|
@@ -1934,9 +2063,10 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
1934
2063
|
"src/providers/store-provider.tsx.ejs": `'use client';
|
|
1935
2064
|
|
|
1936
2065
|
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
1937
|
-
import type { StoreInfo, Cart } from 'brainerce';
|
|
2066
|
+
import type { StoreInfo, Cart, CustomerProfile } from 'brainerce';
|
|
1938
2067
|
import { getCartTotals } from 'brainerce';
|
|
1939
|
-
import { getClient, initClient,
|
|
2068
|
+
import { getClient, initClient, setStoredCartId } from '@/lib/brainerce';
|
|
2069
|
+
import { checkAuthStatus, proxyLogout } from '@/lib/auth';
|
|
1940
2070
|
|
|
1941
2071
|
// ---- Store Info Context ----
|
|
1942
2072
|
interface StoreInfoContextValue {
|
|
@@ -1957,17 +2087,17 @@ export function useStoreInfo() {
|
|
|
1957
2087
|
interface AuthContextValue {
|
|
1958
2088
|
isLoggedIn: boolean;
|
|
1959
2089
|
authLoading: boolean;
|
|
1960
|
-
|
|
1961
|
-
login: (
|
|
1962
|
-
logout: () => void
|
|
2090
|
+
customer: CustomerProfile | null;
|
|
2091
|
+
login: () => Promise<void>;
|
|
2092
|
+
logout: () => Promise<void>;
|
|
1963
2093
|
}
|
|
1964
2094
|
|
|
1965
2095
|
const AuthContext = createContext<AuthContextValue>({
|
|
1966
2096
|
isLoggedIn: false,
|
|
1967
2097
|
authLoading: true,
|
|
1968
|
-
|
|
1969
|
-
login: () => {},
|
|
1970
|
-
logout: () => {},
|
|
2098
|
+
customer: null,
|
|
2099
|
+
login: async () => {},
|
|
2100
|
+
logout: async () => {},
|
|
1971
2101
|
});
|
|
1972
2102
|
|
|
1973
2103
|
export function useAuth() {
|
|
@@ -1999,26 +2129,46 @@ export function useCart() {
|
|
|
1999
2129
|
export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
2000
2130
|
const [storeInfo, setStoreInfo] = useState<StoreInfo | null>(null);
|
|
2001
2131
|
const [storeLoading, setStoreLoading] = useState(true);
|
|
2002
|
-
const [
|
|
2132
|
+
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
2133
|
+
const [customer, setCustomer] = useState<CustomerProfile | null>(null);
|
|
2003
2134
|
const [authLoading, setAuthLoading] = useState(true);
|
|
2004
2135
|
const [cart, setCart] = useState<Cart | null>(null);
|
|
2005
2136
|
const [cartLoading, setCartLoading] = useState(true);
|
|
2006
2137
|
|
|
2007
|
-
//
|
|
2138
|
+
// Check auth status via httpOnly cookie (server-side validation)
|
|
2139
|
+
const refreshAuth = useCallback(async () => {
|
|
2140
|
+
try {
|
|
2141
|
+
const status = await checkAuthStatus();
|
|
2142
|
+
setIsLoggedIn(status.isLoggedIn);
|
|
2143
|
+
setCustomer(status.isLoggedIn ? (status.customer as CustomerProfile) : null);
|
|
2144
|
+
} catch {
|
|
2145
|
+
setIsLoggedIn(false);
|
|
2146
|
+
setCustomer(null);
|
|
2147
|
+
} finally {
|
|
2148
|
+
setAuthLoading(false);
|
|
2149
|
+
}
|
|
2150
|
+
}, []);
|
|
2151
|
+
|
|
2152
|
+
// Initialize client, check auth, and fetch store info
|
|
2008
2153
|
useEffect(() => {
|
|
2009
2154
|
const client = initClient();
|
|
2010
|
-
|
|
2011
|
-
if
|
|
2012
|
-
|
|
2155
|
+
|
|
2156
|
+
// Optimistic check: if brainerce_logged_in cookie exists, assume logged in
|
|
2157
|
+
// while we validate the actual token server-side
|
|
2158
|
+
if (typeof document !== 'undefined' && document.cookie.includes('brainerce_logged_in=1')) {
|
|
2159
|
+
setIsLoggedIn(true);
|
|
2013
2160
|
}
|
|
2014
|
-
setAuthLoading(false);
|
|
2015
2161
|
|
|
2162
|
+
// Validate auth token server-side
|
|
2163
|
+
refreshAuth();
|
|
2164
|
+
|
|
2165
|
+
// Fetch store info (public, no auth needed)
|
|
2016
2166
|
client
|
|
2017
2167
|
.getStoreInfo()
|
|
2018
2168
|
.then(setStoreInfo)
|
|
2019
2169
|
.catch(console.error)
|
|
2020
2170
|
.finally(() => setStoreLoading(false));
|
|
2021
|
-
}, []);
|
|
2171
|
+
}, [refreshAuth]);
|
|
2022
2172
|
|
|
2023
2173
|
// Cart management
|
|
2024
2174
|
const refreshCart = useCallback(async () => {
|
|
@@ -2041,24 +2191,26 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2041
2191
|
|
|
2042
2192
|
useEffect(() => {
|
|
2043
2193
|
refreshCart();
|
|
2044
|
-
}, [refreshCart,
|
|
2194
|
+
}, [refreshCart, isLoggedIn]);
|
|
2045
2195
|
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
setToken(newToken);
|
|
2196
|
+
// Called after successful login (cookie already set by proxy)
|
|
2197
|
+
const login = useCallback(async () => {
|
|
2198
|
+
// Refresh auth state from server (reads httpOnly cookie)
|
|
2199
|
+
await refreshAuth();
|
|
2051
2200
|
|
|
2052
2201
|
// Merge guest session cart into customer cart
|
|
2202
|
+
const client = getClient();
|
|
2053
2203
|
client.syncCartOnLogin().catch(console.error);
|
|
2054
|
-
}, []);
|
|
2204
|
+
}, [refreshAuth]);
|
|
2205
|
+
|
|
2206
|
+
const logout = useCallback(async () => {
|
|
2207
|
+
// Clear httpOnly cookie server-side
|
|
2208
|
+
await proxyLogout();
|
|
2055
2209
|
|
|
2056
|
-
const logout = useCallback(() => {
|
|
2057
2210
|
const client = getClient();
|
|
2058
|
-
client.clearCustomerToken();
|
|
2059
2211
|
client.onLogout();
|
|
2060
|
-
|
|
2061
|
-
|
|
2212
|
+
setIsLoggedIn(false);
|
|
2213
|
+
setCustomer(null);
|
|
2062
2214
|
setCart(null);
|
|
2063
2215
|
refreshCart();
|
|
2064
2216
|
}, [refreshCart]);
|
|
@@ -2071,7 +2223,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
|
2071
2223
|
|
|
2072
2224
|
return (
|
|
2073
2225
|
<StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
|
|
2074
|
-
<AuthContext.Provider value={{ isLoggedIn
|
|
2226
|
+
<AuthContext.Provider value={{ isLoggedIn, authLoading, customer, login, logout }}>
|
|
2075
2227
|
<CartContext.Provider
|
|
2076
2228
|
value={{ cart, cartLoading, refreshCart, itemCount, totals }}
|
|
2077
2229
|
>
|
|
@@ -3891,8 +4043,8 @@ export default function OrderConfirmationPage() {
|
|
|
3891
4043
|
import { useState } from 'react';
|
|
3892
4044
|
import { useRouter } from 'next/navigation';
|
|
3893
4045
|
import Link from 'next/link';
|
|
3894
|
-
import { getClient } from '@/lib/brainerce';
|
|
3895
4046
|
import { useAuth } from '@/providers/store-provider';
|
|
4047
|
+
import { proxyLogin } from '@/lib/auth';
|
|
3896
4048
|
import { LoginForm } from '@/components/auth/login-form';
|
|
3897
4049
|
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
3898
4050
|
import { useTranslations } from '@/lib/translations';
|
|
@@ -3906,15 +4058,16 @@ export default function LoginPage() {
|
|
|
3906
4058
|
async function handleLogin(email: string, password: string) {
|
|
3907
4059
|
try {
|
|
3908
4060
|
setError(null);
|
|
3909
|
-
const
|
|
3910
|
-
const result = await client.loginCustomer(email, password);
|
|
4061
|
+
const result = await proxyLogin(email, password);
|
|
3911
4062
|
|
|
3912
4063
|
if (result.requiresVerification) {
|
|
3913
|
-
|
|
4064
|
+
// Verification token is NOT the auth JWT \u2014 safe to pass in URL
|
|
4065
|
+
router.push('/verify-email');
|
|
3914
4066
|
return;
|
|
3915
4067
|
}
|
|
3916
4068
|
|
|
3917
|
-
auth
|
|
4069
|
+
// Cookie was set by the proxy; refresh auth state
|
|
4070
|
+
await auth.login();
|
|
3918
4071
|
router.push('/');
|
|
3919
4072
|
} catch (err) {
|
|
3920
4073
|
const message = err instanceof Error ? err.message : 'Login failed. Please try again.';
|
|
@@ -3950,8 +4103,8 @@ export default function LoginPage() {
|
|
|
3950
4103
|
import { useState } from 'react';
|
|
3951
4104
|
import { useRouter } from 'next/navigation';
|
|
3952
4105
|
import Link from 'next/link';
|
|
3953
|
-
import { getClient } from '@/lib/brainerce';
|
|
3954
4106
|
import { useAuth } from '@/providers/store-provider';
|
|
4107
|
+
import { proxyRegister } from '@/lib/auth';
|
|
3955
4108
|
import { RegisterForm } from '@/components/auth/register-form';
|
|
3956
4109
|
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
3957
4110
|
import { useTranslations } from '@/lib/translations';
|
|
@@ -3970,20 +4123,16 @@ export default function RegisterPage() {
|
|
|
3970
4123
|
}) {
|
|
3971
4124
|
try {
|
|
3972
4125
|
setError(null);
|
|
3973
|
-
const
|
|
3974
|
-
const result = await client.registerCustomer({
|
|
3975
|
-
firstName: data.firstName,
|
|
3976
|
-
lastName: data.lastName,
|
|
3977
|
-
email: data.email,
|
|
3978
|
-
password: data.password,
|
|
3979
|
-
});
|
|
4126
|
+
const result = await proxyRegister(data);
|
|
3980
4127
|
|
|
3981
4128
|
if (result.requiresVerification) {
|
|
3982
|
-
|
|
4129
|
+
// Cookie already set by proxy; verify-email uses it for auth
|
|
4130
|
+
router.push('/verify-email');
|
|
3983
4131
|
return;
|
|
3984
4132
|
}
|
|
3985
4133
|
|
|
3986
|
-
auth
|
|
4134
|
+
// Cookie was set by the proxy; refresh auth state
|
|
4135
|
+
await auth.login();
|
|
3987
4136
|
router.push('/');
|
|
3988
4137
|
} catch (err) {
|
|
3989
4138
|
const message = err instanceof Error ? err.message : 'Registration failed. Please try again.';
|
|
@@ -4017,10 +4166,10 @@ export default function RegisterPage() {
|
|
|
4017
4166
|
"src/app/verify-email/page.tsx": `'use client';
|
|
4018
4167
|
|
|
4019
4168
|
import { Suspense, useState, useRef, useEffect, useCallback } from 'react';
|
|
4020
|
-
import { useRouter
|
|
4169
|
+
import { useRouter } from 'next/navigation';
|
|
4021
4170
|
import Link from 'next/link';
|
|
4022
|
-
import { getClient } from '@/lib/brainerce';
|
|
4023
4171
|
import { useAuth } from '@/providers/store-provider';
|
|
4172
|
+
import { proxyVerifyEmail, proxyResendVerification } from '@/lib/auth';
|
|
4024
4173
|
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
4025
4174
|
import { useTranslations } from '@/lib/translations';
|
|
4026
4175
|
|
|
@@ -4029,10 +4178,8 @@ const RESEND_COOLDOWN_SECONDS = 60;
|
|
|
4029
4178
|
|
|
4030
4179
|
function VerifyEmailContent() {
|
|
4031
4180
|
const router = useRouter();
|
|
4032
|
-
const searchParams = useSearchParams();
|
|
4033
4181
|
const auth = useAuth();
|
|
4034
4182
|
|
|
4035
|
-
const token = searchParams.get('token');
|
|
4036
4183
|
const t = useTranslations('auth');
|
|
4037
4184
|
|
|
4038
4185
|
const [digits, setDigits] = useState<string[]>(Array(CODE_LENGTH).fill(''));
|
|
@@ -4060,18 +4207,17 @@ function VerifyEmailContent() {
|
|
|
4060
4207
|
|
|
4061
4208
|
const handleSubmit = useCallback(
|
|
4062
4209
|
async (code: string) => {
|
|
4063
|
-
if (
|
|
4210
|
+
if (code.length !== CODE_LENGTH || loading) return;
|
|
4064
4211
|
|
|
4065
4212
|
try {
|
|
4066
4213
|
setLoading(true);
|
|
4067
4214
|
setError(null);
|
|
4068
|
-
|
|
4069
|
-
const result = await
|
|
4215
|
+
// Auth token is in httpOnly cookie \u2014 proxy adds Authorization header
|
|
4216
|
+
const result = await proxyVerifyEmail(code);
|
|
4070
4217
|
|
|
4071
4218
|
if (result.verified) {
|
|
4072
|
-
//
|
|
4073
|
-
|
|
4074
|
-
auth.login(authToken);
|
|
4219
|
+
// Refresh auth state (cookie already set)
|
|
4220
|
+
await auth.login();
|
|
4075
4221
|
setSuccess('Email verified successfully! Redirecting...');
|
|
4076
4222
|
setTimeout(() => router.push('/'), 1500);
|
|
4077
4223
|
} else {
|
|
@@ -4085,7 +4231,7 @@ function VerifyEmailContent() {
|
|
|
4085
4231
|
setLoading(false);
|
|
4086
4232
|
}
|
|
4087
4233
|
},
|
|
4088
|
-
[
|
|
4234
|
+
[loading, auth, router]
|
|
4089
4235
|
);
|
|
4090
4236
|
|
|
4091
4237
|
function handleDigitChange(index: number, value: string) {
|
|
@@ -4139,13 +4285,12 @@ function VerifyEmailContent() {
|
|
|
4139
4285
|
}
|
|
4140
4286
|
|
|
4141
4287
|
async function handleResend() {
|
|
4142
|
-
if (
|
|
4288
|
+
if (resending || cooldown > 0) return;
|
|
4143
4289
|
|
|
4144
4290
|
try {
|
|
4145
4291
|
setResending(true);
|
|
4146
4292
|
setError(null);
|
|
4147
|
-
|
|
4148
|
-
await client.resendVerificationEmail(token);
|
|
4293
|
+
await proxyResendVerification();
|
|
4149
4294
|
setSuccess('Verification code sent! Check your email.');
|
|
4150
4295
|
setCooldown(RESEND_COOLDOWN_SECONDS);
|
|
4151
4296
|
// Clear digits for fresh entry
|
|
@@ -4165,37 +4310,6 @@ function VerifyEmailContent() {
|
|
|
4165
4310
|
handleSubmit(code);
|
|
4166
4311
|
}
|
|
4167
4312
|
|
|
4168
|
-
// No token provided
|
|
4169
|
-
if (!token) {
|
|
4170
|
-
return (
|
|
4171
|
-
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
4172
|
-
<div className="w-full max-w-md space-y-4 text-center">
|
|
4173
|
-
<svg
|
|
4174
|
-
className="text-muted-foreground mx-auto h-12 w-12"
|
|
4175
|
-
fill="none"
|
|
4176
|
-
viewBox="0 0 24 24"
|
|
4177
|
-
stroke="currentColor"
|
|
4178
|
-
>
|
|
4179
|
-
<path
|
|
4180
|
-
strokeLinecap="round"
|
|
4181
|
-
strokeLinejoin="round"
|
|
4182
|
-
strokeWidth={1.5}
|
|
4183
|
-
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"
|
|
4184
|
-
/>
|
|
4185
|
-
</svg>
|
|
4186
|
-
<h1 className="text-foreground text-2xl font-bold">{t('verificationInvalid')}</h1>
|
|
4187
|
-
<p className="text-muted-foreground text-sm">{t('verificationInvalidDesc')}</p>
|
|
4188
|
-
<Link
|
|
4189
|
-
href="/register"
|
|
4190
|
-
className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
|
|
4191
|
-
>
|
|
4192
|
-
{t('goToRegister')}
|
|
4193
|
-
</Link>
|
|
4194
|
-
</div>
|
|
4195
|
-
</div>
|
|
4196
|
-
);
|
|
4197
|
-
}
|
|
4198
|
-
|
|
4199
4313
|
return (
|
|
4200
4314
|
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
4201
4315
|
<div className="w-full max-w-md space-y-6">
|
|
@@ -4325,8 +4439,8 @@ function OAuthCallbackContent() {
|
|
|
4325
4439
|
const t = useTranslations('auth');
|
|
4326
4440
|
|
|
4327
4441
|
const oauthSuccess = searchParams.get('oauth_success');
|
|
4328
|
-
const token = searchParams.get('token');
|
|
4329
4442
|
const oauthError = searchParams.get('oauth_error');
|
|
4443
|
+
// Token is no longer in URL \u2014 it was set as httpOnly cookie by /api/auth/oauth-callback
|
|
4330
4444
|
|
|
4331
4445
|
useEffect(() => {
|
|
4332
4446
|
// Prevent double-processing in React StrictMode
|
|
@@ -4338,13 +4452,15 @@ function OAuthCallbackContent() {
|
|
|
4338
4452
|
return;
|
|
4339
4453
|
}
|
|
4340
4454
|
|
|
4341
|
-
if (oauthSuccess === 'true'
|
|
4342
|
-
auth
|
|
4343
|
-
|
|
4455
|
+
if (oauthSuccess === 'true') {
|
|
4456
|
+
// Cookie was already set by the API route; refresh auth state
|
|
4457
|
+
auth.login().then(() => {
|
|
4458
|
+
router.push('/');
|
|
4459
|
+
});
|
|
4344
4460
|
} else {
|
|
4345
4461
|
setError(t('authFailedDesc'));
|
|
4346
4462
|
}
|
|
4347
|
-
}, [oauthSuccess,
|
|
4463
|
+
}, [oauthSuccess, oauthError, auth, router, t]);
|
|
4348
4464
|
|
|
4349
4465
|
if (error) {
|
|
4350
4466
|
return (
|
|
@@ -4511,6 +4627,348 @@ export default function AccountPage() {
|
|
|
4511
4627
|
</div>
|
|
4512
4628
|
);
|
|
4513
4629
|
}
|
|
4630
|
+
`,
|
|
4631
|
+
"src/app/api/store/[...path]/route.ts": `import { NextRequest, NextResponse } from 'next/server';
|
|
4632
|
+
import { cookies } from 'next/headers';
|
|
4633
|
+
|
|
4634
|
+
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
4635
|
+
/\\/$/,
|
|
4636
|
+
''
|
|
4637
|
+
);
|
|
4638
|
+
|
|
4639
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4640
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
4641
|
+
const CSRF_HEADER = 'x-requested-with';
|
|
4642
|
+
const CSRF_VALUE = 'brainerce';
|
|
4643
|
+
|
|
4644
|
+
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
|
|
4645
|
+
|
|
4646
|
+
/** Auth endpoints whose responses contain tokens to intercept */
|
|
4647
|
+
const AUTH_ENDPOINTS = ['customers/login', 'customers/register', 'customers/verify-email'];
|
|
4648
|
+
|
|
4649
|
+
function isAuthEndpoint(path: string): boolean {
|
|
4650
|
+
return AUTH_ENDPOINTS.some((ep) => path.endsWith(ep));
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4653
|
+
function isSecure(): boolean {
|
|
4654
|
+
return process.env.NODE_ENV === 'production';
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4657
|
+
function setAuthCookies(response: NextResponse, token: string): void {
|
|
4658
|
+
response.cookies.set(TOKEN_COOKIE, token, {
|
|
4659
|
+
httpOnly: true,
|
|
4660
|
+
secure: isSecure(),
|
|
4661
|
+
sameSite: 'lax',
|
|
4662
|
+
path: '/',
|
|
4663
|
+
maxAge: COOKIE_MAX_AGE,
|
|
4664
|
+
});
|
|
4665
|
+
response.cookies.set(LOGGED_IN_COOKIE, '1', {
|
|
4666
|
+
httpOnly: false,
|
|
4667
|
+
secure: isSecure(),
|
|
4668
|
+
sameSite: 'lax',
|
|
4669
|
+
path: '/',
|
|
4670
|
+
maxAge: COOKIE_MAX_AGE,
|
|
4671
|
+
});
|
|
4672
|
+
}
|
|
4673
|
+
|
|
4674
|
+
function clearAuthCookies(response: NextResponse): void {
|
|
4675
|
+
response.cookies.delete(TOKEN_COOKIE);
|
|
4676
|
+
response.cookies.delete(LOGGED_IN_COOKIE);
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4679
|
+
async function proxyRequest(
|
|
4680
|
+
request: NextRequest,
|
|
4681
|
+
params: { path: string[] }
|
|
4682
|
+
): Promise<NextResponse> {
|
|
4683
|
+
const method = request.method;
|
|
4684
|
+
|
|
4685
|
+
// CSRF protection for mutating requests
|
|
4686
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
4687
|
+
const csrfHeader = request.headers.get(CSRF_HEADER);
|
|
4688
|
+
if (csrfHeader !== CSRF_VALUE) {
|
|
4689
|
+
return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 });
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
|
|
4693
|
+
// Build backend URL from path segments
|
|
4694
|
+
const pathSegments = params.path.join('/');
|
|
4695
|
+
const backendUrl = new URL(\`\${BACKEND_URL}/\${pathSegments}\`);
|
|
4696
|
+
|
|
4697
|
+
// Forward query parameters
|
|
4698
|
+
request.nextUrl.searchParams.forEach((value, key) => {
|
|
4699
|
+
backendUrl.searchParams.set(key, value);
|
|
4700
|
+
});
|
|
4701
|
+
|
|
4702
|
+
// Build headers for backend request
|
|
4703
|
+
const headers: Record<string, string> = {
|
|
4704
|
+
'Content-Type': 'application/json',
|
|
4705
|
+
};
|
|
4706
|
+
|
|
4707
|
+
// Forward SDK version header if present
|
|
4708
|
+
const sdkVersion = request.headers.get('x-sdk-version');
|
|
4709
|
+
if (sdkVersion) {
|
|
4710
|
+
headers['X-SDK-Version'] = sdkVersion;
|
|
4711
|
+
}
|
|
4712
|
+
|
|
4713
|
+
// Add auth token from httpOnly cookie
|
|
4714
|
+
const cookieStore = await cookies();
|
|
4715
|
+
const tokenCookie = cookieStore.get(TOKEN_COOKIE);
|
|
4716
|
+
if (tokenCookie?.value) {
|
|
4717
|
+
headers['Authorization'] = \`Bearer \${tokenCookie.value}\`;
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4720
|
+
// Forward request body for non-GET requests
|
|
4721
|
+
let body: string | undefined;
|
|
4722
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
4723
|
+
try {
|
|
4724
|
+
body = await request.text();
|
|
4725
|
+
} catch {
|
|
4726
|
+
// No body
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
|
|
4730
|
+
// Proxy the request to backend
|
|
4731
|
+
let backendResponse: Response;
|
|
4732
|
+
try {
|
|
4733
|
+
backendResponse = await fetch(backendUrl.toString(), {
|
|
4734
|
+
method,
|
|
4735
|
+
headers,
|
|
4736
|
+
body,
|
|
4737
|
+
});
|
|
4738
|
+
} catch (error) {
|
|
4739
|
+
return NextResponse.json({ error: 'Backend service unavailable' }, { status: 502 });
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4742
|
+
// Read response body
|
|
4743
|
+
const responseText = await backendResponse.text();
|
|
4744
|
+
|
|
4745
|
+
// For auth endpoints: intercept token, set cookie, strip token from response
|
|
4746
|
+
if (backendResponse.ok && method === 'POST' && isAuthEndpoint(pathSegments)) {
|
|
4747
|
+
try {
|
|
4748
|
+
const data = JSON.parse(responseText);
|
|
4749
|
+
if (data.token) {
|
|
4750
|
+
const token = data.token;
|
|
4751
|
+
|
|
4752
|
+
// Strip token from client response
|
|
4753
|
+
const { token: _stripped, ...safeData } = data;
|
|
4754
|
+
|
|
4755
|
+
const response = NextResponse.json(safeData, {
|
|
4756
|
+
status: backendResponse.status,
|
|
4757
|
+
});
|
|
4758
|
+
setAuthCookies(response, token);
|
|
4759
|
+
return response;
|
|
4760
|
+
}
|
|
4761
|
+
} catch {
|
|
4762
|
+
// Not JSON or no token field \u2014 pass through
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4766
|
+
// Handle 401 responses: clear auth cookies
|
|
4767
|
+
if (backendResponse.status === 401 && tokenCookie?.value) {
|
|
4768
|
+
const response = new NextResponse(responseText, {
|
|
4769
|
+
status: backendResponse.status,
|
|
4770
|
+
headers: {
|
|
4771
|
+
'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
|
|
4772
|
+
},
|
|
4773
|
+
});
|
|
4774
|
+
clearAuthCookies(response);
|
|
4775
|
+
return response;
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
// Pass through response as-is
|
|
4779
|
+
return new NextResponse(responseText, {
|
|
4780
|
+
status: backendResponse.status,
|
|
4781
|
+
headers: {
|
|
4782
|
+
'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
|
|
4783
|
+
},
|
|
4784
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
export async function GET(
|
|
4788
|
+
request: NextRequest,
|
|
4789
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
4790
|
+
) {
|
|
4791
|
+
return proxyRequest(request, await params);
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4794
|
+
export async function POST(
|
|
4795
|
+
request: NextRequest,
|
|
4796
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
4797
|
+
) {
|
|
4798
|
+
return proxyRequest(request, await params);
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
export async function PUT(
|
|
4802
|
+
request: NextRequest,
|
|
4803
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
4804
|
+
) {
|
|
4805
|
+
return proxyRequest(request, await params);
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
export async function PATCH(
|
|
4809
|
+
request: NextRequest,
|
|
4810
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
4811
|
+
) {
|
|
4812
|
+
return proxyRequest(request, await params);
|
|
4813
|
+
}
|
|
4814
|
+
|
|
4815
|
+
export async function DELETE(
|
|
4816
|
+
request: NextRequest,
|
|
4817
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
4818
|
+
) {
|
|
4819
|
+
return proxyRequest(request, await params);
|
|
4820
|
+
}
|
|
4821
|
+
`,
|
|
4822
|
+
"src/app/api/auth/oauth-callback/route.ts": `import { NextRequest, NextResponse } from 'next/server';
|
|
4823
|
+
|
|
4824
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4825
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
4826
|
+
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
|
|
4827
|
+
|
|
4828
|
+
function isSecure(): boolean {
|
|
4829
|
+
return process.env.NODE_ENV === 'production';
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4832
|
+
/**
|
|
4833
|
+
* OAuth callback handler.
|
|
4834
|
+
* The backend redirects here with ?token=jwt&oauth_success=true after OAuth code exchange.
|
|
4835
|
+
* We set the httpOnly cookie and redirect to the client-side callback page (without the token).
|
|
4836
|
+
*/
|
|
4837
|
+
export async function GET(request: NextRequest) {
|
|
4838
|
+
const { searchParams } = request.nextUrl;
|
|
4839
|
+
const token = searchParams.get('token');
|
|
4840
|
+
const oauthSuccess = searchParams.get('oauth_success');
|
|
4841
|
+
const oauthError = searchParams.get('oauth_error');
|
|
4842
|
+
|
|
4843
|
+
// Build redirect URL to client-side callback page
|
|
4844
|
+
const redirectUrl = new URL('/auth/callback', request.url);
|
|
4845
|
+
|
|
4846
|
+
if (oauthError) {
|
|
4847
|
+
redirectUrl.searchParams.set('oauth_error', oauthError);
|
|
4848
|
+
return NextResponse.redirect(redirectUrl);
|
|
4849
|
+
}
|
|
4850
|
+
|
|
4851
|
+
if (oauthSuccess === 'true' && token) {
|
|
4852
|
+
redirectUrl.searchParams.set('oauth_success', 'true');
|
|
4853
|
+
|
|
4854
|
+
const response = NextResponse.redirect(redirectUrl);
|
|
4855
|
+
|
|
4856
|
+
// Set httpOnly cookie with the token
|
|
4857
|
+
response.cookies.set(TOKEN_COOKIE, token, {
|
|
4858
|
+
httpOnly: true,
|
|
4859
|
+
secure: isSecure(),
|
|
4860
|
+
sameSite: 'lax',
|
|
4861
|
+
path: '/',
|
|
4862
|
+
maxAge: COOKIE_MAX_AGE,
|
|
4863
|
+
});
|
|
4864
|
+
|
|
4865
|
+
// Set indicator cookie (readable by client JS)
|
|
4866
|
+
response.cookies.set(LOGGED_IN_COOKIE, '1', {
|
|
4867
|
+
httpOnly: false,
|
|
4868
|
+
secure: isSecure(),
|
|
4869
|
+
sameSite: 'lax',
|
|
4870
|
+
path: '/',
|
|
4871
|
+
maxAge: COOKIE_MAX_AGE,
|
|
4872
|
+
});
|
|
4873
|
+
|
|
4874
|
+
return response;
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
// Fallback: no token or success flag
|
|
4878
|
+
redirectUrl.searchParams.set('oauth_error', 'Authentication failed');
|
|
4879
|
+
return NextResponse.redirect(redirectUrl);
|
|
4880
|
+
}
|
|
4881
|
+
`,
|
|
4882
|
+
"src/app/api/auth/me/route.ts": `import { NextResponse } from 'next/server';
|
|
4883
|
+
import { cookies } from 'next/headers';
|
|
4884
|
+
|
|
4885
|
+
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
4886
|
+
/\\/$/,
|
|
4887
|
+
''
|
|
4888
|
+
);
|
|
4889
|
+
|
|
4890
|
+
const CONNECTION_ID = process.env.NEXT_PUBLIC_BRAINERCE_CONNECTION_ID || '';
|
|
4891
|
+
|
|
4892
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4893
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
4894
|
+
|
|
4895
|
+
/**
|
|
4896
|
+
* Auth status check endpoint.
|
|
4897
|
+
* Reads the httpOnly cookie, validates against backend, returns auth state.
|
|
4898
|
+
*/
|
|
4899
|
+
export async function GET() {
|
|
4900
|
+
const cookieStore = await cookies();
|
|
4901
|
+
const tokenCookie = cookieStore.get(TOKEN_COOKIE);
|
|
4902
|
+
|
|
4903
|
+
if (!tokenCookie?.value) {
|
|
4904
|
+
return NextResponse.json({ isLoggedIn: false });
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
try {
|
|
4908
|
+
// Validate token by calling backend profile endpoint
|
|
4909
|
+
const response = await fetch(\`\${BACKEND_URL}/api/vc/\${CONNECTION_ID}/customers/me\`, {
|
|
4910
|
+
headers: {
|
|
4911
|
+
Authorization: \`Bearer \${tokenCookie.value}\`,
|
|
4912
|
+
'Content-Type': 'application/json',
|
|
4913
|
+
},
|
|
4914
|
+
});
|
|
4915
|
+
|
|
4916
|
+
if (!response.ok) {
|
|
4917
|
+
// Token is invalid or expired \u2014 clear cookies
|
|
4918
|
+
const res = NextResponse.json({ isLoggedIn: false });
|
|
4919
|
+
res.cookies.delete(TOKEN_COOKIE);
|
|
4920
|
+
res.cookies.delete(LOGGED_IN_COOKIE);
|
|
4921
|
+
return res;
|
|
4922
|
+
}
|
|
4923
|
+
|
|
4924
|
+
const customer = await response.json();
|
|
4925
|
+
return NextResponse.json({ isLoggedIn: true, customer });
|
|
4926
|
+
} catch {
|
|
4927
|
+
// Backend unreachable \u2014 don't clear cookies, might be temporary
|
|
4928
|
+
return NextResponse.json({ isLoggedIn: false, error: 'Service unavailable' }, { status: 503 });
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
`,
|
|
4932
|
+
"src/app/api/auth/logout/route.ts": `import { NextResponse } from 'next/server';
|
|
4933
|
+
|
|
4934
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4935
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
4936
|
+
|
|
4937
|
+
/**
|
|
4938
|
+
* Logout endpoint. Clears auth cookies.
|
|
4939
|
+
*/
|
|
4940
|
+
export async function POST() {
|
|
4941
|
+
const response = NextResponse.json({ success: true });
|
|
4942
|
+
response.cookies.delete(TOKEN_COOKIE);
|
|
4943
|
+
response.cookies.delete(LOGGED_IN_COOKIE);
|
|
4944
|
+
return response;
|
|
4945
|
+
}
|
|
4946
|
+
`,
|
|
4947
|
+
"src/middleware.ts": `import { NextRequest, NextResponse } from 'next/server';
|
|
4948
|
+
|
|
4949
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
4950
|
+
|
|
4951
|
+
/** Routes that require customer authentication */
|
|
4952
|
+
const PROTECTED_PATHS = ['/account'];
|
|
4953
|
+
|
|
4954
|
+
export function middleware(request: NextRequest) {
|
|
4955
|
+
const { pathname } = request.nextUrl;
|
|
4956
|
+
const isProtected = PROTECTED_PATHS.some((p) => pathname.startsWith(p));
|
|
4957
|
+
|
|
4958
|
+
if (isProtected) {
|
|
4959
|
+
const token = request.cookies.get(TOKEN_COOKIE);
|
|
4960
|
+
if (!token?.value) {
|
|
4961
|
+
const loginUrl = new URL('/login', request.url);
|
|
4962
|
+
return NextResponse.redirect(loginUrl);
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
|
|
4966
|
+
return NextResponse.next();
|
|
4967
|
+
}
|
|
4968
|
+
|
|
4969
|
+
export const config = {
|
|
4970
|
+
matcher: ['/account/:path*'],
|
|
4971
|
+
};
|
|
4514
4972
|
`,
|
|
4515
4973
|
"src/components/products/product-card.tsx": `'use client';
|
|
4516
4974
|
|
|
@@ -5842,8 +6300,11 @@ declare global {
|
|
|
5842
6300
|
}
|
|
5843
6301
|
|
|
5844
6302
|
// Payment SDK script URLs \u2014 resolved per provider
|
|
6303
|
+
// SRI hashes must be regenerated when the provider updates their SDK
|
|
5845
6304
|
const PAYMENT_SDK_URL = 'https://cdn.meshulam.co.il/sdk/gs.min.js';
|
|
6305
|
+
const PAYMENT_SDK_SRI = 'sha384-e1OYzZERZLgbR8Zw1I4Ww/J7qXGyEsZb7LF0z5W/sbnTsFwtxop7sKZsLGNp6CB1';
|
|
5846
6306
|
const APPLE_PAY_SDK_URL = 'https://meshulam.co.il/_media/js/apple_pay_sdk/sdk.min.js';
|
|
6307
|
+
const APPLE_PAY_SDK_SRI = 'sha384-CG67HXmWBeyuRR/jqFsYr6dGEMFyuZw3/hRmcyACJRYHGb/1ebEkQZyzdJXDc9/u';
|
|
5847
6308
|
|
|
5848
6309
|
interface PaymentStepProps {
|
|
5849
6310
|
checkoutId: string;
|
|
@@ -5854,7 +6315,7 @@ interface PaymentStepProps {
|
|
|
5854
6315
|
* Load a script tag if not already present in the DOM.
|
|
5855
6316
|
* Resolves when the script loads (or immediately if already present).
|
|
5856
6317
|
*/
|
|
5857
|
-
function loadScript(src: string, optional = false): Promise<void> {
|
|
6318
|
+
function loadScript(src: string, optional = false, integrity?: string): Promise<void> {
|
|
5858
6319
|
return new Promise((resolve) => {
|
|
5859
6320
|
if (document.querySelector(\`script[src="\${src}"]\`)) {
|
|
5860
6321
|
resolve();
|
|
@@ -5863,6 +6324,10 @@ function loadScript(src: string, optional = false): Promise<void> {
|
|
|
5863
6324
|
const script = document.createElement('script');
|
|
5864
6325
|
script.src = src;
|
|
5865
6326
|
script.async = true;
|
|
6327
|
+
if (integrity) {
|
|
6328
|
+
script.integrity = integrity;
|
|
6329
|
+
script.crossOrigin = 'anonymous';
|
|
6330
|
+
}
|
|
5866
6331
|
script.onload = () => resolve();
|
|
5867
6332
|
script.onerror = () => {
|
|
5868
6333
|
if (optional) {
|
|
@@ -5955,10 +6420,10 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
|
|
|
5955
6420
|
|
|
5956
6421
|
async function loadSdkScripts() {
|
|
5957
6422
|
// Load Apple Pay SDK (optional)
|
|
5958
|
-
await loadScript(APPLE_PAY_SDK_URL, true);
|
|
6423
|
+
await loadScript(APPLE_PAY_SDK_URL, true, APPLE_PAY_SDK_SRI);
|
|
5959
6424
|
|
|
5960
6425
|
// Load payment SDK script
|
|
5961
|
-
await loadScript(PAYMENT_SDK_URL);
|
|
6426
|
+
await loadScript(PAYMENT_SDK_URL, false, PAYMENT_SDK_SRI);
|
|
5962
6427
|
|
|
5963
6428
|
// Wait for SDK global to be set by the script
|
|
5964
6429
|
const available = await waitForPaymentSdkGlobal();
|
|
@@ -6621,7 +7086,7 @@ export function OAuthButtons({ className }: OAuthButtonsProps) {
|
|
|
6621
7086
|
try {
|
|
6622
7087
|
setRedirecting(provider);
|
|
6623
7088
|
const client = getClient();
|
|
6624
|
-
const redirectUrl = window.location.origin + '/auth/callback';
|
|
7089
|
+
const redirectUrl = window.location.origin + '/api/auth/oauth-callback';
|
|
6625
7090
|
const result = await client.getOAuthAuthorizeUrl(provider, { redirectUrl });
|
|
6626
7091
|
window.location.href = result.authorizationUrl;
|
|
6627
7092
|
} catch (err) {
|
|
@@ -8256,6 +8721,39 @@ my-store/
|
|
|
8256
8721
|
| \`app/auth/callback/page.tsx\` | Extracts OAuth token from URL params, stores in localStorage |
|
|
8257
8722
|
| \`app/account/page.tsx\` | Protected page: getMyProfile() + getMyOrders() |
|
|
8258
8723
|
| \`components/Header.tsx\` | Site header with nav, cart count badge, search autocomplete |
|
|
8724
|
+
|
|
8725
|
+
## Content Security Policy (CSP)
|
|
8726
|
+
|
|
8727
|
+
Payment SDKs load iframes and scripts from external domains. Add these CSP headers in \`next.config.js\`:
|
|
8728
|
+
|
|
8729
|
+
\`\`\`js
|
|
8730
|
+
// next.config.js
|
|
8731
|
+
module.exports = {
|
|
8732
|
+
async headers() {
|
|
8733
|
+
return [{
|
|
8734
|
+
source: '/(.*)',
|
|
8735
|
+
headers: [{
|
|
8736
|
+
key: 'Content-Security-Policy',
|
|
8737
|
+
value: [
|
|
8738
|
+
"default-src 'self'",
|
|
8739
|
+
"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",
|
|
8740
|
+
"style-src 'self' 'unsafe-inline'",
|
|
8741
|
+
"img-src 'self' data: blob: https:",
|
|
8742
|
+
"font-src 'self' data:",
|
|
8743
|
+
"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",
|
|
8744
|
+
"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",
|
|
8745
|
+
"worker-src 'self' blob:",
|
|
8746
|
+
].join('; '),
|
|
8747
|
+
}],
|
|
8748
|
+
}];
|
|
8749
|
+
},
|
|
8750
|
+
};
|
|
8751
|
+
\`\`\`
|
|
8752
|
+
|
|
8753
|
+
**Required domains by provider:**
|
|
8754
|
+
- **Grow**: \`*.meshulam.co.il\`, \`grow.link\`, \`*.grow.link\`, \`*.grow.security\`, \`*.creditguard.co.il\`
|
|
8755
|
+
- **Stripe**: \`js.stripe.com\`, \`hooks.stripe.com\`, \`*.stripe.com\`
|
|
8756
|
+
- **Google Pay**: \`pay.google.com\`, \`google.com\`
|
|
8259
8757
|
`;
|
|
8260
8758
|
|
|
8261
8759
|
// src/resources/project-template.ts
|