keystone-design-bootstrap 1.0.101 → 1.0.103
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/package.json
CHANGED
|
@@ -204,7 +204,30 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
|
|
|
204
204
|
body: JSON.stringify({ phone: fullPhone, code }),
|
|
205
205
|
});
|
|
206
206
|
const result = await res.json().catch(() => ({}));
|
|
207
|
-
if (!res.ok
|
|
207
|
+
if (!res.ok) {
|
|
208
|
+
setError(result.error || 'That code is invalid or expired.');
|
|
209
|
+
captureEvent('portal_login_failed', { step: 'signin', reason: result.code || 'verification_failed' });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Compatibility path: verify_code may directly authenticate and set the
|
|
214
|
+
// session cookie via the route handler (no passwordless_auth needed).
|
|
215
|
+
if (result.authenticated) {
|
|
216
|
+
await setPixelUserData({ email: result.email || undefined, phone: fullPhone });
|
|
217
|
+
firePixelEvent('Lead', undefined, result?.eventId);
|
|
218
|
+
captureEvent('portal_login_step_advanced', {
|
|
219
|
+
from_step: 'signin',
|
|
220
|
+
to_step: 'authenticated',
|
|
221
|
+
reason: 'verification_direct_auth',
|
|
222
|
+
});
|
|
223
|
+
captureEvent('portal_login_completed', { flow: 'signin' });
|
|
224
|
+
setResendAvailableAt(null);
|
|
225
|
+
setSecondsUntilResend(0);
|
|
226
|
+
if (onSuccess) onSuccess(); else router.refresh();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!result.verification_token) {
|
|
208
231
|
setError(result.error || 'That code is invalid or expired.');
|
|
209
232
|
captureEvent('portal_login_failed', { step: 'signin', reason: result.code || 'verification_failed' });
|
|
210
233
|
return;
|
|
@@ -47,6 +47,71 @@ function apiHeaders(request?: Request): Record<string, string> {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function responseData(json: Record<string, unknown>): Record<string, unknown> {
|
|
51
|
+
return (json.data as Record<string, unknown>) || json;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readStringField(obj: Record<string, unknown>, key: string): string | null {
|
|
55
|
+
const value = obj[key];
|
|
56
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readNestedStringField(
|
|
60
|
+
obj: Record<string, unknown>,
|
|
61
|
+
parentKey: string,
|
|
62
|
+
key: string
|
|
63
|
+
): string | null {
|
|
64
|
+
const parent = obj[parentKey];
|
|
65
|
+
if (!parent || typeof parent !== 'object') return null;
|
|
66
|
+
const value = (parent as Record<string, unknown>)[key];
|
|
67
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractAuthToken(data: Record<string, unknown>): string | null {
|
|
71
|
+
const direct =
|
|
72
|
+
readStringField(data, 'token') ||
|
|
73
|
+
readStringField(data, 'auth_token') ||
|
|
74
|
+
readStringField(data, 'access_token') ||
|
|
75
|
+
readStringField(data, 'session_token') ||
|
|
76
|
+
readStringField(data, 'jwt') ||
|
|
77
|
+
readNestedStringField(data, 'consumer', 'token') ||
|
|
78
|
+
readNestedStringField(data, 'session', 'token');
|
|
79
|
+
if (direct) return direct;
|
|
80
|
+
|
|
81
|
+
// Compatibility fallback: some backends rename token fields without notice.
|
|
82
|
+
// Walk a few levels and accept any non-empty string on keys containing "token"/"jwt".
|
|
83
|
+
const queue: Array<{ value: unknown; depth: number }> = [{ value: data, depth: 0 }];
|
|
84
|
+
const visited = new Set<object>();
|
|
85
|
+
while (queue.length > 0) {
|
|
86
|
+
const next = queue.shift();
|
|
87
|
+
if (!next) break;
|
|
88
|
+
const { value, depth } = next;
|
|
89
|
+
if (!value || typeof value !== 'object') continue;
|
|
90
|
+
if (visited.has(value as object)) continue;
|
|
91
|
+
visited.add(value as object);
|
|
92
|
+
|
|
93
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
94
|
+
for (const [key, candidate] of entries) {
|
|
95
|
+
// Never treat verification tokens as auth/session JWTs.
|
|
96
|
+
if (/verification[_-]?token/i.test(key)) {
|
|
97
|
+
if (depth < 4 && candidate && typeof candidate === 'object') {
|
|
98
|
+
queue.push({ value: candidate, depth: depth + 1 });
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const isTokenLikeKey = /token|jwt/i.test(key);
|
|
103
|
+
if (isTokenLikeKey && typeof candidate === 'string' && candidate.length > 0) {
|
|
104
|
+
return candidate;
|
|
105
|
+
}
|
|
106
|
+
if (depth < 4 && candidate && typeof candidate === 'object') {
|
|
107
|
+
queue.push({ value: candidate, depth: depth + 1 });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
50
115
|
// POST /api/consumer/initiate
|
|
51
116
|
// Looks up an existing user by email or phone, creating a Contact if needed.
|
|
52
117
|
// Returns { exists, firstName, hasPassword }.
|
|
@@ -68,8 +133,8 @@ async function handleInitiate(request: Request, NR: NextResponseLike): Promise<R
|
|
|
68
133
|
if (res.status === 404) return NR.json({ exists: false });
|
|
69
134
|
if (!res.ok) return NR.json({ exists: null });
|
|
70
135
|
|
|
71
|
-
const json = await res.json().catch(() => ({}))
|
|
72
|
-
const data = json
|
|
136
|
+
const json = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
137
|
+
const data = responseData(json);
|
|
73
138
|
return NR.json({
|
|
74
139
|
exists: true,
|
|
75
140
|
firstName: data?.first_name ?? undefined,
|
|
@@ -104,7 +169,8 @@ async function handleLogin(request: Request, NR: NextResponseLike): Promise<Resp
|
|
|
104
169
|
);
|
|
105
170
|
}
|
|
106
171
|
|
|
107
|
-
const
|
|
172
|
+
const data = responseData(json);
|
|
173
|
+
const token = extractAuthToken(data);
|
|
108
174
|
if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
|
|
109
175
|
|
|
110
176
|
const response = NR.json({});
|
|
@@ -140,15 +206,16 @@ async function handleSignup(request: Request, NR: NextResponseLike): Promise<Res
|
|
|
140
206
|
}),
|
|
141
207
|
});
|
|
142
208
|
|
|
143
|
-
const json = await res.json().catch(() => ({}))
|
|
209
|
+
const json = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
144
210
|
if (!res.ok) {
|
|
145
211
|
return NR.json({ error: json.error || 'Signup failed. Please try again.' }, { status: res.status });
|
|
146
212
|
}
|
|
147
213
|
|
|
148
|
-
const
|
|
214
|
+
const data = responseData(json);
|
|
215
|
+
const token = extractAuthToken(data);
|
|
149
216
|
if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
|
|
150
217
|
|
|
151
|
-
const response = NR.json({ claimed:
|
|
218
|
+
const response = NR.json({ claimed: data?.claimed ?? false });
|
|
152
219
|
response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
|
|
153
220
|
httpOnly: true,
|
|
154
221
|
secure: process.env.NODE_ENV === 'production',
|
|
@@ -172,20 +239,21 @@ async function handleSendCode(request: Request, NR: NextResponseLike): Promise<R
|
|
|
172
239
|
body: JSON.stringify({ phone }),
|
|
173
240
|
});
|
|
174
241
|
|
|
175
|
-
const json = await res.json().catch(() => ({}))
|
|
242
|
+
const json = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
243
|
+
const data = responseData(json);
|
|
176
244
|
if (!res.ok) {
|
|
177
245
|
return NR.json(
|
|
178
246
|
{
|
|
179
|
-
error: json.error || 'Failed to send verification code. Please try again.',
|
|
180
|
-
code: json.code,
|
|
181
|
-
retry_in_seconds: json.retry_in_seconds,
|
|
247
|
+
error: (json as Record<string, unknown>).error || 'Failed to send verification code. Please try again.',
|
|
248
|
+
code: (json as Record<string, unknown>).code,
|
|
249
|
+
retry_in_seconds: (json as Record<string, unknown>).retry_in_seconds,
|
|
182
250
|
},
|
|
183
251
|
{ status: res.status }
|
|
184
252
|
);
|
|
185
253
|
}
|
|
186
254
|
|
|
187
255
|
return NR.json({
|
|
188
|
-
resend_available_at:
|
|
256
|
+
resend_available_at: data?.resend_available_at ?? null,
|
|
189
257
|
});
|
|
190
258
|
}
|
|
191
259
|
|
|
@@ -202,19 +270,43 @@ async function handleVerifyCode(request: Request, NR: NextResponseLike): Promise
|
|
|
202
270
|
body: JSON.stringify({ phone, code }),
|
|
203
271
|
});
|
|
204
272
|
|
|
205
|
-
const json = await res.json().catch(() => ({}))
|
|
273
|
+
const json = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
274
|
+
const data = responseData(json);
|
|
206
275
|
if (!res.ok) {
|
|
207
276
|
return NR.json(
|
|
208
|
-
{
|
|
277
|
+
{
|
|
278
|
+
error: (json as Record<string, unknown>).error || 'Verification failed. Please try again.',
|
|
279
|
+
code: (json as Record<string, unknown>).code,
|
|
280
|
+
},
|
|
209
281
|
{ status: res.status }
|
|
210
282
|
);
|
|
211
283
|
}
|
|
212
284
|
|
|
285
|
+
// Some surfaces return a direct session token on verify.
|
|
286
|
+
const directToken = extractAuthToken(data);
|
|
287
|
+
if (directToken) {
|
|
288
|
+
const response = NR.json({
|
|
289
|
+
authenticated: true,
|
|
290
|
+
first_name: data?.first_name ?? '',
|
|
291
|
+
last_name: data?.last_name ?? '',
|
|
292
|
+
email: data?.email ?? '',
|
|
293
|
+
eventId: data?.event_id ?? undefined,
|
|
294
|
+
});
|
|
295
|
+
response.cookies.set(CONSUMER_TOKEN_COOKIE, directToken, {
|
|
296
|
+
httpOnly: true,
|
|
297
|
+
secure: process.env.NODE_ENV === 'production',
|
|
298
|
+
sameSite: 'lax',
|
|
299
|
+
path: '/',
|
|
300
|
+
maxAge: COOKIE_MAX_AGE,
|
|
301
|
+
});
|
|
302
|
+
return response;
|
|
303
|
+
}
|
|
304
|
+
|
|
213
305
|
return NR.json({
|
|
214
|
-
verification_token:
|
|
215
|
-
first_name:
|
|
216
|
-
last_name:
|
|
217
|
-
email:
|
|
306
|
+
verification_token: data?.verification_token,
|
|
307
|
+
first_name: data?.first_name ?? '',
|
|
308
|
+
last_name: data?.last_name ?? '',
|
|
309
|
+
email: data?.email ?? '',
|
|
218
310
|
});
|
|
219
311
|
}
|
|
220
312
|
|
|
@@ -242,17 +334,21 @@ async function handlePasswordlessAuth(request: Request, NR: NextResponseLike): P
|
|
|
242
334
|
}),
|
|
243
335
|
});
|
|
244
336
|
|
|
245
|
-
const json = await res.json().catch(() => ({}))
|
|
337
|
+
const json = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
338
|
+
const data = responseData(json);
|
|
246
339
|
if (!res.ok) {
|
|
247
|
-
return NR.json(
|
|
340
|
+
return NR.json(
|
|
341
|
+
{ error: (json as Record<string, unknown>).error || 'Authentication failed. Please try again.' },
|
|
342
|
+
{ status: res.status }
|
|
343
|
+
);
|
|
248
344
|
}
|
|
249
345
|
|
|
250
|
-
const token =
|
|
346
|
+
const token = extractAuthToken(data);
|
|
251
347
|
if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
|
|
252
348
|
|
|
253
349
|
// Surface the server-side Lead event_id so the browser can fire a single deduped
|
|
254
350
|
// Lead (fbq track with { eventID }) that Meta matches against the CAPI Lead event.
|
|
255
|
-
const response = NR.json({ eventId:
|
|
351
|
+
const response = NR.json({ eventId: data?.event_id ?? undefined });
|
|
256
352
|
response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
|
|
257
353
|
httpOnly: true,
|
|
258
354
|
secure: process.env.NODE_ENV === 'production',
|