create-brainerce-store 1.6.0 → 1.7.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 +1 -1
- package/messages/en.json +286 -286
- package/messages/he.json +286 -286
- package/package.json +1 -1
- package/templates/nextjs/base/.env.local.ejs +3 -1
- package/templates/nextjs/base/next.config.ts +31 -9
- package/templates/nextjs/base/src/app/api/auth/logout/route.ts +14 -0
- package/templates/nextjs/base/src/app/api/auth/me/route.ts +49 -0
- package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +59 -0
- package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +41 -0
- package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +68 -0
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +190 -0
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +92 -90
- package/templates/nextjs/base/src/app/forgot-password/page.tsx +2 -1
- package/templates/nextjs/base/src/app/login/page.tsx +59 -58
- package/templates/nextjs/base/src/app/register/page.tsx +64 -68
- package/templates/nextjs/base/src/app/reset-password/page.tsx +14 -43
- package/templates/nextjs/base/src/app/verify-email/page.tsx +258 -293
- package/templates/nextjs/base/src/components/auth/login-form.tsx +101 -101
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +379 -372
- package/templates/nextjs/base/src/lib/auth.ts +148 -0
- package/templates/nextjs/base/src/lib/brainerce.ts.ejs +6 -26
- package/templates/nextjs/base/src/middleware.ts +25 -0
- package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +50 -27
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
|
|
4
|
+
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
5
|
+
/\/$/,
|
|
6
|
+
''
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const 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
|
+
try {
|
|
27
|
+
// Validate token by calling backend profile endpoint
|
|
28
|
+
const response = await fetch(`${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/me`, {
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${tokenCookie.value}`,
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
// Token is invalid or expired — clear cookies
|
|
37
|
+
const res = NextResponse.json({ isLoggedIn: false });
|
|
38
|
+
res.cookies.delete(TOKEN_COOKIE);
|
|
39
|
+
res.cookies.delete(LOGGED_IN_COOKIE);
|
|
40
|
+
return res;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const customer = await response.json();
|
|
44
|
+
return NextResponse.json({ isLoggedIn: true, customer });
|
|
45
|
+
} catch {
|
|
46
|
+
// Backend unreachable — don't clear cookies, might be temporary
|
|
47
|
+
return NextResponse.json({ isLoggedIn: false, error: 'Service unavailable' }, { status: 503 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
|
|
4
|
+
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
5
|
+
/\/$/,
|
|
6
|
+
''
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const CONNECTION_ID = process.env.BRAINERCE_CONNECTION_ID || '';
|
|
10
|
+
|
|
11
|
+
const RESET_TOKEN_COOKIE = 'brainerce_reset_token';
|
|
12
|
+
const CSRF_HEADER = 'x-requested-with';
|
|
13
|
+
const CSRF_VALUE = 'brainerce';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* BFF endpoint for password reset.
|
|
17
|
+
* Reads the reset token from the httpOnly cookie (set by /api/auth/reset-callback)
|
|
18
|
+
* and proxies the request to the backend. The token never touches client JS.
|
|
19
|
+
*/
|
|
20
|
+
export async function POST(request: NextRequest) {
|
|
21
|
+
// CSRF check
|
|
22
|
+
const csrfHeader = request.headers.get(CSRF_HEADER);
|
|
23
|
+
if (csrfHeader !== CSRF_VALUE) {
|
|
24
|
+
return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Read reset token from httpOnly cookie
|
|
28
|
+
const cookieStore = await cookies();
|
|
29
|
+
const resetTokenCookie = cookieStore.get(RESET_TOKEN_COOKIE);
|
|
30
|
+
|
|
31
|
+
if (!resetTokenCookie?.value) {
|
|
32
|
+
return NextResponse.json(
|
|
33
|
+
{ error: 'No reset token found. Please request a new password reset link.' },
|
|
34
|
+
{ status: 400 }
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Parse request body
|
|
39
|
+
const body = await request.json();
|
|
40
|
+
const { newPassword } = body;
|
|
41
|
+
|
|
42
|
+
if (!newPassword) {
|
|
43
|
+
return NextResponse.json({ error: 'New password is required' }, { status: 400 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Proxy to backend
|
|
47
|
+
const backendUrl = `${BACKEND_URL}/api/vc/${CONNECTION_ID}/customers/reset-password`;
|
|
48
|
+
|
|
49
|
+
const backendResponse = await fetch(backendUrl, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
token: resetTokenCookie.value,
|
|
54
|
+
newPassword,
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const data = await backendResponse.json();
|
|
59
|
+
|
|
60
|
+
const response = NextResponse.json(data, {
|
|
61
|
+
status: backendResponse.status,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Always clear the reset token cookie after use (success or failure)
|
|
65
|
+
response.cookies.delete(RESET_TOKEN_COOKIE);
|
|
66
|
+
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
|
|
4
|
+
const BACKEND_URL = (process.env.BRAINERCE_API_URL || 'https://api.brainerce.com').replace(
|
|
5
|
+
/\/$/,
|
|
6
|
+
''
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const TOKEN_COOKIE = 'brainerce_customer_token';
|
|
10
|
+
const LOGGED_IN_COOKIE = 'brainerce_logged_in';
|
|
11
|
+
const CSRF_HEADER = 'x-requested-with';
|
|
12
|
+
const CSRF_VALUE = 'brainerce';
|
|
13
|
+
|
|
14
|
+
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days
|
|
15
|
+
|
|
16
|
+
/** Auth endpoints whose responses contain tokens to intercept */
|
|
17
|
+
const AUTH_ENDPOINTS = ['customers/login', 'customers/register', 'customers/verify-email'];
|
|
18
|
+
|
|
19
|
+
function isAuthEndpoint(path: string): boolean {
|
|
20
|
+
return AUTH_ENDPOINTS.some((ep) => path.endsWith(ep));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isSecure(): boolean {
|
|
24
|
+
return process.env.NODE_ENV === 'production';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setAuthCookies(response: NextResponse, token: string): void {
|
|
28
|
+
response.cookies.set(TOKEN_COOKIE, token, {
|
|
29
|
+
httpOnly: true,
|
|
30
|
+
secure: isSecure(),
|
|
31
|
+
sameSite: 'lax',
|
|
32
|
+
path: '/',
|
|
33
|
+
maxAge: COOKIE_MAX_AGE,
|
|
34
|
+
});
|
|
35
|
+
response.cookies.set(LOGGED_IN_COOKIE, '1', {
|
|
36
|
+
httpOnly: false,
|
|
37
|
+
secure: isSecure(),
|
|
38
|
+
sameSite: 'lax',
|
|
39
|
+
path: '/',
|
|
40
|
+
maxAge: COOKIE_MAX_AGE,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function clearAuthCookies(response: NextResponse): void {
|
|
45
|
+
response.cookies.delete(TOKEN_COOKIE);
|
|
46
|
+
response.cookies.delete(LOGGED_IN_COOKIE);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function proxyRequest(
|
|
50
|
+
request: NextRequest,
|
|
51
|
+
params: { path: string[] }
|
|
52
|
+
): Promise<NextResponse> {
|
|
53
|
+
const method = request.method;
|
|
54
|
+
|
|
55
|
+
// CSRF protection for mutating requests
|
|
56
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
57
|
+
const csrfHeader = request.headers.get(CSRF_HEADER);
|
|
58
|
+
if (csrfHeader !== CSRF_VALUE) {
|
|
59
|
+
return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build backend URL from path segments
|
|
64
|
+
const pathSegments = params.path.join('/');
|
|
65
|
+
const backendUrl = new URL(`${BACKEND_URL}/${pathSegments}`);
|
|
66
|
+
|
|
67
|
+
// Forward query parameters
|
|
68
|
+
request.nextUrl.searchParams.forEach((value, key) => {
|
|
69
|
+
backendUrl.searchParams.set(key, value);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Build headers for backend request
|
|
73
|
+
const headers: Record<string, string> = {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Forward SDK version header if present
|
|
78
|
+
const sdkVersion = request.headers.get('x-sdk-version');
|
|
79
|
+
if (sdkVersion) {
|
|
80
|
+
headers['X-SDK-Version'] = sdkVersion;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add auth token from httpOnly cookie
|
|
84
|
+
const cookieStore = await cookies();
|
|
85
|
+
const tokenCookie = cookieStore.get(TOKEN_COOKIE);
|
|
86
|
+
if (tokenCookie?.value) {
|
|
87
|
+
headers['Authorization'] = `Bearer ${tokenCookie.value}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Forward request body for non-GET requests
|
|
91
|
+
let body: string | undefined;
|
|
92
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
93
|
+
try {
|
|
94
|
+
body = await request.text();
|
|
95
|
+
} catch {
|
|
96
|
+
// No body
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Proxy the request to backend
|
|
101
|
+
let backendResponse: Response;
|
|
102
|
+
try {
|
|
103
|
+
backendResponse = await fetch(backendUrl.toString(), {
|
|
104
|
+
method,
|
|
105
|
+
headers,
|
|
106
|
+
body,
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return NextResponse.json({ error: 'Backend service unavailable' }, { status: 502 });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Read response body
|
|
113
|
+
const responseText = await backendResponse.text();
|
|
114
|
+
|
|
115
|
+
// For auth endpoints: intercept token, set cookie, strip token from response
|
|
116
|
+
if (backendResponse.ok && method === 'POST' && isAuthEndpoint(pathSegments)) {
|
|
117
|
+
try {
|
|
118
|
+
const data = JSON.parse(responseText);
|
|
119
|
+
if (data.token) {
|
|
120
|
+
const token = data.token;
|
|
121
|
+
|
|
122
|
+
// Strip token from client response
|
|
123
|
+
const { token: _stripped, ...safeData } = data;
|
|
124
|
+
|
|
125
|
+
const response = NextResponse.json(safeData, {
|
|
126
|
+
status: backendResponse.status,
|
|
127
|
+
});
|
|
128
|
+
setAuthCookies(response, token);
|
|
129
|
+
return response;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Not JSON or no token field — pass through
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle 401 responses: clear auth cookies
|
|
137
|
+
if (backendResponse.status === 401 && tokenCookie?.value) {
|
|
138
|
+
const response = new NextResponse(responseText, {
|
|
139
|
+
status: backendResponse.status,
|
|
140
|
+
headers: {
|
|
141
|
+
'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
clearAuthCookies(response);
|
|
145
|
+
return response;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Pass through response as-is
|
|
149
|
+
return new NextResponse(responseText, {
|
|
150
|
+
status: backendResponse.status,
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': backendResponse.headers.get('Content-Type') || 'application/json',
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function GET(
|
|
158
|
+
request: NextRequest,
|
|
159
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
160
|
+
) {
|
|
161
|
+
return proxyRequest(request, await params);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function POST(
|
|
165
|
+
request: NextRequest,
|
|
166
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
167
|
+
) {
|
|
168
|
+
return proxyRequest(request, await params);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function PUT(
|
|
172
|
+
request: NextRequest,
|
|
173
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
174
|
+
) {
|
|
175
|
+
return proxyRequest(request, await params);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function PATCH(
|
|
179
|
+
request: NextRequest,
|
|
180
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
181
|
+
) {
|
|
182
|
+
return proxyRequest(request, await params);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function DELETE(
|
|
186
|
+
request: NextRequest,
|
|
187
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
188
|
+
) {
|
|
189
|
+
return proxyRequest(request, await params);
|
|
190
|
+
}
|
|
@@ -1,90 +1,92 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Suspense, useEffect, useState, useRef } from 'react';
|
|
4
|
-
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
|
-
import { useAuth } from '@/providers/store-provider';
|
|
6
|
-
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
7
|
-
import { useTranslations } from '@/lib/translations';
|
|
8
|
-
|
|
9
|
-
function OAuthCallbackContent() {
|
|
10
|
-
const router = useRouter();
|
|
11
|
-
const searchParams = useSearchParams();
|
|
12
|
-
const auth = useAuth();
|
|
13
|
-
const [error, setError] = useState<string | null>(null);
|
|
14
|
-
const processedRef = useRef(false);
|
|
15
|
-
const t = useTranslations('auth');
|
|
16
|
-
|
|
17
|
-
const oauthSuccess = searchParams.get('oauth_success');
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
// Prevent double-processing in React StrictMode
|
|
23
|
-
if (processedRef.current) return;
|
|
24
|
-
processedRef.current = true;
|
|
25
|
-
|
|
26
|
-
if (oauthError) {
|
|
27
|
-
setError(oauthError);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (oauthSuccess === 'true'
|
|
32
|
-
auth
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Suspense, useEffect, useState, useRef } from 'react';
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
|
+
import { useAuth } from '@/providers/store-provider';
|
|
6
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
7
|
+
import { useTranslations } from '@/lib/translations';
|
|
8
|
+
|
|
9
|
+
function OAuthCallbackContent() {
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
const searchParams = useSearchParams();
|
|
12
|
+
const auth = useAuth();
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
const processedRef = useRef(false);
|
|
15
|
+
const t = useTranslations('auth');
|
|
16
|
+
|
|
17
|
+
const oauthSuccess = searchParams.get('oauth_success');
|
|
18
|
+
const oauthError = searchParams.get('oauth_error');
|
|
19
|
+
// Token is no longer in URL — it was set as httpOnly cookie by /api/auth/oauth-callback
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
// Prevent double-processing in React StrictMode
|
|
23
|
+
if (processedRef.current) return;
|
|
24
|
+
processedRef.current = true;
|
|
25
|
+
|
|
26
|
+
if (oauthError) {
|
|
27
|
+
setError(oauthError);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (oauthSuccess === 'true') {
|
|
32
|
+
// Cookie was already set by the API route; refresh auth state
|
|
33
|
+
auth.login().then(() => {
|
|
34
|
+
router.push('/');
|
|
35
|
+
});
|
|
36
|
+
} else {
|
|
37
|
+
setError(t('authFailedDesc'));
|
|
38
|
+
}
|
|
39
|
+
}, [oauthSuccess, oauthError, auth, router, t]);
|
|
40
|
+
|
|
41
|
+
if (error) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
44
|
+
<div className="w-full max-w-md space-y-4 text-center">
|
|
45
|
+
<svg
|
|
46
|
+
className="text-destructive mx-auto h-12 w-12"
|
|
47
|
+
fill="none"
|
|
48
|
+
viewBox="0 0 24 24"
|
|
49
|
+
stroke="currentColor"
|
|
50
|
+
>
|
|
51
|
+
<path
|
|
52
|
+
strokeLinecap="round"
|
|
53
|
+
strokeLinejoin="round"
|
|
54
|
+
strokeWidth={1.5}
|
|
55
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.834-2.694-.834-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
56
|
+
/>
|
|
57
|
+
</svg>
|
|
58
|
+
<h1 className="text-foreground text-2xl font-bold">{t('authFailed')}</h1>
|
|
59
|
+
<p className="text-muted-foreground text-sm">{error}</p>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => router.push('/login')}
|
|
63
|
+
className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
|
|
64
|
+
>
|
|
65
|
+
{t('backToLogin')}
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex min-h-[60vh] flex-col items-center justify-center px-4 py-12">
|
|
74
|
+
<LoadingSpinner size="lg" />
|
|
75
|
+
<p className="text-muted-foreground mt-4">{t('completingSignIn')}</p>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default function OAuthCallbackPage() {
|
|
81
|
+
return (
|
|
82
|
+
<Suspense
|
|
83
|
+
fallback={
|
|
84
|
+
<div className="flex min-h-[60vh] items-center justify-center">
|
|
85
|
+
<LoadingSpinner size="lg" />
|
|
86
|
+
</div>
|
|
87
|
+
}
|
|
88
|
+
>
|
|
89
|
+
<OAuthCallbackContent />
|
|
90
|
+
</Suspense>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -21,7 +21,8 @@ export default function ForgotPasswordPage() {
|
|
|
21
21
|
setLoading(true);
|
|
22
22
|
setError(null);
|
|
23
23
|
const client = getClient();
|
|
24
|
-
|
|
24
|
+
const resetUrl = `${window.location.origin}/api/auth/reset-callback`;
|
|
25
|
+
await client.forgotPassword(email, { resetUrl });
|
|
25
26
|
setSent(true);
|
|
26
27
|
} catch (err) {
|
|
27
28
|
const message =
|