keystone-design-bootstrap 1.0.82 → 1.0.83
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/design_system/sections/index.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tracking/index.d.ts +20 -1
- package/dist/tracking/index.js.map +1 -1
- package/package.json +1 -1
- package/src/design_system/portal/LoginForm.tsx +280 -272
- package/src/design_system/portal/LoginModalController.tsx +4 -2
- package/src/next/layouts/root-layout.tsx +5 -4
- package/src/next/routes/consumer-auth.ts +106 -1
|
@@ -60,10 +60,12 @@ export function LoginModalController() {
|
|
|
60
60
|
// Close the modal once the router transition has fully completed and the
|
|
61
61
|
// server component has re-rendered with the new authenticated state.
|
|
62
62
|
useEffect(() => {
|
|
63
|
-
if (refreshing && !isPending)
|
|
63
|
+
if (!(refreshing && !isPending)) return;
|
|
64
|
+
const timeout = window.setTimeout(() => {
|
|
64
65
|
setOpen(false);
|
|
65
66
|
setRefreshing(false);
|
|
66
|
-
}
|
|
67
|
+
}, 0);
|
|
68
|
+
return () => window.clearTimeout(timeout);
|
|
67
69
|
}, [refreshing, isPending]);
|
|
68
70
|
|
|
69
71
|
return (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { Metadata } from 'next';
|
|
3
|
+
import Script from 'next/script';
|
|
3
4
|
|
|
4
5
|
import { HeaderNavigation, FooterHome } from '../../design_system/sections';
|
|
5
6
|
import { ThemeProvider } from '../../contexts';
|
|
@@ -239,12 +240,12 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
|
239
240
|
|
|
240
241
|
return (
|
|
241
242
|
<html lang="en" data-theme={theme}>
|
|
242
|
-
<
|
|
243
|
+
<body>
|
|
243
244
|
{gtmBootstrapScript ? (
|
|
244
|
-
<
|
|
245
|
+
<Script id="google-tag-manager">
|
|
246
|
+
{gtmBootstrapScript}
|
|
247
|
+
</Script>
|
|
245
248
|
) : null}
|
|
246
|
-
</head>
|
|
247
|
-
<body>
|
|
248
249
|
{gtmContainerId ? (
|
|
249
250
|
<noscript>
|
|
250
251
|
<iframe
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* import { createConsumerAuthHandlers } from 'keystone-design-bootstrap/next/routes/consumer-auth';
|
|
8
8
|
* export const { POST } = createConsumerAuthHandlers({ NextResponse });
|
|
9
9
|
*
|
|
10
|
-
* Handles: initiate, login, signup, logout
|
|
10
|
+
* Handles: initiate, login, signup, send_code, verify_code, passwordless_auth, logout
|
|
11
11
|
*
|
|
12
12
|
* Env (server-side only):
|
|
13
13
|
* - API_URL (default: http://localhost:3000/api/v1)
|
|
@@ -151,6 +151,108 @@ async function handleSignup(request: Request, NR: NextResponseLike): Promise<Res
|
|
|
151
151
|
return response;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// POST /api/consumer/send_code
|
|
155
|
+
// Sends a 4-digit SMS verification code for passwordless login/signup.
|
|
156
|
+
async function handleSendCode(request: Request, NR: NextResponseLike): Promise<Response> {
|
|
157
|
+
const body = await request.json().catch(() => ({})) as Record<string, string>;
|
|
158
|
+
const { phone } = body;
|
|
159
|
+
if (!phone) return NR.json({ error: 'Phone is required.' }, { status: 422 });
|
|
160
|
+
|
|
161
|
+
const res = await fetch(`${getApiUrl()}/consumer/auth/send_code`, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: apiHeaders(request),
|
|
164
|
+
body: JSON.stringify({ phone }),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const json = await res.json().catch(() => ({}));
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
return NR.json(
|
|
170
|
+
{
|
|
171
|
+
error: json.error || 'Failed to send verification code. Please try again.',
|
|
172
|
+
code: json.code,
|
|
173
|
+
retry_in_seconds: json.retry_in_seconds,
|
|
174
|
+
},
|
|
175
|
+
{ status: res.status }
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return NR.json({
|
|
180
|
+
resend_available_at: json.data?.resend_available_at ?? null,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// POST /api/consumer/verify_code
|
|
185
|
+
// Verifies the SMS code and returns a short-lived verification token.
|
|
186
|
+
async function handleVerifyCode(request: Request, NR: NextResponseLike): Promise<Response> {
|
|
187
|
+
const body = await request.json().catch(() => ({})) as Record<string, string>;
|
|
188
|
+
const { phone, code } = body;
|
|
189
|
+
if (!phone || !code) return NR.json({ error: 'Phone and code are required.' }, { status: 422 });
|
|
190
|
+
|
|
191
|
+
const res = await fetch(`${getApiUrl()}/consumer/auth/verify_code`, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: apiHeaders(request),
|
|
194
|
+
body: JSON.stringify({ phone, code }),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const json = await res.json().catch(() => ({}));
|
|
198
|
+
if (!res.ok) {
|
|
199
|
+
return NR.json(
|
|
200
|
+
{ error: json.error || 'Verification failed. Please try again.', code: json.code },
|
|
201
|
+
{ status: res.status }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return NR.json({
|
|
206
|
+
verification_token: json.data?.verification_token,
|
|
207
|
+
first_name: json.data?.first_name ?? '',
|
|
208
|
+
last_name: json.data?.last_name ?? '',
|
|
209
|
+
email: json.data?.email ?? '',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// POST /api/consumer/passwordless_auth
|
|
214
|
+
// Completes passwordless auth and sets HttpOnly JWT cookie.
|
|
215
|
+
async function handlePasswordlessAuth(request: Request, NR: NextResponseLike): Promise<Response> {
|
|
216
|
+
const body = await request.json().catch(() => ({})) as Record<string, string>;
|
|
217
|
+
const { phone, verification_token, first_name, last_name, email } = body;
|
|
218
|
+
|
|
219
|
+
if (!phone) return NR.json({ error: 'Phone is required.' }, { status: 422 });
|
|
220
|
+
if (!verification_token) return NR.json({ error: 'Verification token is required.' }, { status: 422 });
|
|
221
|
+
if (!first_name || !last_name || !email) {
|
|
222
|
+
return NR.json({ error: 'First name, last name, and email are required.' }, { status: 422 });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const res = await fetch(`${getApiUrl()}/consumer/auth/passwordless_auth`, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: apiHeaders(request),
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
phone,
|
|
230
|
+
verification_token,
|
|
231
|
+
first_name,
|
|
232
|
+
last_name,
|
|
233
|
+
email,
|
|
234
|
+
}),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const json = await res.json().catch(() => ({}));
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
return NR.json({ error: json.error || 'Authentication failed. Please try again.' }, { status: res.status });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const token = json.data?.token;
|
|
243
|
+
if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
|
|
244
|
+
|
|
245
|
+
const response = NR.json({});
|
|
246
|
+
response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
|
|
247
|
+
httpOnly: true,
|
|
248
|
+
secure: process.env.NODE_ENV === 'production',
|
|
249
|
+
sameSite: 'lax',
|
|
250
|
+
path: '/',
|
|
251
|
+
maxAge: COOKIE_MAX_AGE,
|
|
252
|
+
});
|
|
253
|
+
return response;
|
|
254
|
+
}
|
|
255
|
+
|
|
154
256
|
// POST /api/consumer/logout
|
|
155
257
|
// Clears the JWT cookie.
|
|
156
258
|
async function handleLogout(_request: Request, NR: NextResponseLike): Promise<Response> {
|
|
@@ -173,6 +275,9 @@ export function createConsumerAuthHandlers({ NextResponse }: { NextResponse: Nex
|
|
|
173
275
|
if (action === 'initiate') return handleInitiate(request, NextResponse);
|
|
174
276
|
if (action === 'login') return handleLogin(request, NextResponse);
|
|
175
277
|
if (action === 'signup') return handleSignup(request, NextResponse);
|
|
278
|
+
if (action === 'send_code') return handleSendCode(request, NextResponse);
|
|
279
|
+
if (action === 'verify_code') return handleVerifyCode(request, NextResponse);
|
|
280
|
+
if (action === 'passwordless_auth') return handlePasswordlessAuth(request, NextResponse);
|
|
176
281
|
if (action === 'logout') return handleLogout(request, NextResponse);
|
|
177
282
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
178
283
|
},
|