@caffeinebounce/identity 0.12.1 → 0.12.3
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.d.mts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +203 -124
- package/dist/index.mjs +205 -126
- package/dist/server.js +54 -17
- package/dist/server.mjs +54 -17
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { cn, FieldLabel, Input, Button, PasswordInput, defaultPasswordRules, FieldError, PasswordRequirements, Checkbox, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Badge, Tooltip, TooltipTrigger, TooltipContent, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Label, DialogFooter, Spinner, extendedPasswordRules, VerificationCodeInput, Tabs, TabsList, TabsTrigger, TabsContent, InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from '@caffeinebounce/ui/primitives';
|
|
2
|
+
import { cn, FieldLabel, Input, Button, PasswordInput, defaultPasswordRules, FieldError, PasswordRequirements, Checkbox, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Badge, Tooltip, TooltipTrigger, TooltipContent, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Label, DialogFooter, TooltipProvider, Spinner, extendedPasswordRules, VerificationCodeInput, Tabs, TabsList, TabsTrigger, TabsContent, InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from '@caffeinebounce/ui/primitives';
|
|
3
3
|
import { useSearchParams, useRouter } from 'next/navigation';
|
|
4
4
|
import { createContext, useState, useEffect, useCallback, useRef, useMemo, useContext } from 'react';
|
|
5
|
-
import { ArrowLeft, AlertCircle, Loader2, Shield, AlertTriangle, Smartphone, Check, Mail, KeyRound, Info, Trash2, CheckCircle2, Link2, Unlink, Phone, Copy, Download, Lightbulb, X } from 'lucide-react';
|
|
5
|
+
import { ArrowLeft, AlertCircle, Loader2, Shield, AlertTriangle, Smartphone, Check, Mail, KeyRound, Info, Trash2, CheckCircle2, Link2, Unlink, Phone, Copy, Download, Lightbulb, X, PencilLine } from 'lucide-react';
|
|
6
6
|
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
7
7
|
import { getClientOrigin, generateDeviceFingerprint, formatPhoneInput, isPreviewEnvironment, formatDate } from '@caffeinebounce/shared-utils';
|
|
8
8
|
export { generateDeviceFingerprint, generateSecureToken, getClientIP, getGeolocationFromIP, hashString } from '@caffeinebounce/shared-utils';
|
|
@@ -14,6 +14,17 @@ import { REGEXP_ONLY_DIGITS } from 'input-otp';
|
|
|
14
14
|
import { NextResponse } from 'next/server';
|
|
15
15
|
|
|
16
16
|
// src/components/auth/EmailVerificationPending.tsx
|
|
17
|
+
|
|
18
|
+
// src/types.ts
|
|
19
|
+
var defaultAuthLinks = {
|
|
20
|
+
signIn: "/signin",
|
|
21
|
+
signUp: "/signup",
|
|
22
|
+
forgotPassword: "/forgot-password",
|
|
23
|
+
resetPassword: "/reset-password",
|
|
24
|
+
callback: "/callback",
|
|
25
|
+
home: "/",
|
|
26
|
+
defaultRedirect: "/dashboard"
|
|
27
|
+
};
|
|
17
28
|
function AuthFormLayout({
|
|
18
29
|
children,
|
|
19
30
|
footer,
|
|
@@ -50,6 +61,122 @@ function AuthFormLayout({
|
|
|
50
61
|
] }) })
|
|
51
62
|
] });
|
|
52
63
|
}
|
|
64
|
+
|
|
65
|
+
// src/utils/redirect.ts
|
|
66
|
+
var INTERNAL_REDIRECT_BASE = new URL("https://identity.invalid");
|
|
67
|
+
var ENCODED_PATH_SEPARATOR = /%(?:2f|5c)/i;
|
|
68
|
+
function hasControlCharacter(value) {
|
|
69
|
+
return Array.from(value).some((character) => {
|
|
70
|
+
const code = character.charCodeAt(0);
|
|
71
|
+
return code <= 31 || code === 127;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function isStrictInternalRedirect(candidate) {
|
|
75
|
+
if (!candidate?.startsWith("/") || hasControlCharacter(candidate)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const pathEnd = candidate.search(/[?#]/);
|
|
79
|
+
const pathname = pathEnd === -1 ? candidate : candidate.slice(0, pathEnd);
|
|
80
|
+
if (pathname.includes("\\") || ENCODED_PATH_SEPARATOR.test(pathname)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const target = new URL(candidate, INTERNAL_REDIRECT_BASE);
|
|
85
|
+
return target.origin === INTERNAL_REDIRECT_BASE.origin;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function getSafeInternalRedirect(candidate, fallback) {
|
|
91
|
+
if (isStrictInternalRedirect(candidate)) {
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
if (fallback === null) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return isStrictInternalRedirect(fallback) ? fallback : "/";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/components/auth/utils.ts
|
|
101
|
+
var warnedSupabaseRedirectOrigins = /* @__PURE__ */ new Set();
|
|
102
|
+
function hardRedirect(redirectTo) {
|
|
103
|
+
window.location.href = redirectTo;
|
|
104
|
+
}
|
|
105
|
+
function sanitizeAuthError(message) {
|
|
106
|
+
const lowerMessage = message.toLowerCase();
|
|
107
|
+
if (lowerMessage.includes("user already registered") || lowerMessage.includes("already registered") || lowerMessage.includes("already exists") || lowerMessage.includes("email already") || lowerMessage.includes("duplicate")) {
|
|
108
|
+
return "An account with this email already exists. Please sign in instead.";
|
|
109
|
+
}
|
|
110
|
+
if (lowerMessage.includes("invalid login credentials") || lowerMessage.includes("invalid credentials") || lowerMessage.includes("wrong password") || lowerMessage.includes("incorrect password")) {
|
|
111
|
+
return "Invalid email or password. Please try again.";
|
|
112
|
+
}
|
|
113
|
+
if (lowerMessage.includes("invalid email") || lowerMessage.includes("password")) {
|
|
114
|
+
return message;
|
|
115
|
+
}
|
|
116
|
+
if (message === "The string did not match the expected pattern.") {
|
|
117
|
+
return "Please check your email address format and try again.";
|
|
118
|
+
}
|
|
119
|
+
if (lowerMessage.includes("rate limit") || lowerMessage.includes("too many requests") || lowerMessage.includes("try again later")) {
|
|
120
|
+
return "Too many attempts. Please wait a moment and try again.";
|
|
121
|
+
}
|
|
122
|
+
if (lowerMessage.includes("hook") || lowerMessage.includes("authorization") || lowerMessage.includes("token") || lowerMessage.includes("internal") || lowerMessage.includes("server") || lowerMessage.includes("database") || lowerMessage.includes("connection")) {
|
|
123
|
+
return "An error occurred. Please try again.";
|
|
124
|
+
}
|
|
125
|
+
return "An error occurred. Please try again.";
|
|
126
|
+
}
|
|
127
|
+
function sanitizeSignupError(message) {
|
|
128
|
+
const sanitized = sanitizeAuthError(message);
|
|
129
|
+
if (sanitized === "An error occurred. Please try again.") {
|
|
130
|
+
return "An error occurred during sign up. Please try again.";
|
|
131
|
+
}
|
|
132
|
+
return sanitized;
|
|
133
|
+
}
|
|
134
|
+
function buildOAuthRedirectTo(origin, nextPath) {
|
|
135
|
+
const siteUrl = origin.replace(/\/$/, "");
|
|
136
|
+
return `${siteUrl}/callback?next=${encodeURIComponent(nextPath)}`;
|
|
137
|
+
}
|
|
138
|
+
function buildOAuthSignInOptions(provider, redirectTo, options = {}) {
|
|
139
|
+
return {
|
|
140
|
+
redirectTo,
|
|
141
|
+
...provider === "azure" ? { scopes: "email" } : {},
|
|
142
|
+
...options
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function warnAboutSupabaseRedirectAllowlist(origin) {
|
|
146
|
+
const normalizedOrigin = origin.replace(/\/$/, "");
|
|
147
|
+
let hostname = "";
|
|
148
|
+
try {
|
|
149
|
+
hostname = new URL(normalizedOrigin).host;
|
|
150
|
+
} catch {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!isPreviewEnvironment(hostname)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (warnedSupabaseRedirectOrigins.has(normalizedOrigin)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
warnedSupabaseRedirectOrigins.add(normalizedOrigin);
|
|
160
|
+
console.warn(
|
|
161
|
+
`[identity] Supabase redirect allowlist reminder: add "${normalizedOrigin}/**" to Supabase Auth > URL Configuration > Additional Redirect URLs. The auth components already use the active origin for callbacks, but Supabase must allow that origin too or OAuth/email flows can bounce to another host.`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
function clearStalePKCEState() {
|
|
165
|
+
if (typeof window === "undefined") return;
|
|
166
|
+
try {
|
|
167
|
+
const keysToRemove = [];
|
|
168
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
169
|
+
const key = localStorage.key(i);
|
|
170
|
+
if (key?.includes("-code-verifier")) {
|
|
171
|
+
keysToRemove.push(key);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const key of keysToRemove) {
|
|
175
|
+
localStorage.removeItem(key);
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
53
180
|
var POLL_INTERVAL_MS = 6e4;
|
|
54
181
|
var MAX_POLL_COUNT = 60;
|
|
55
182
|
function EmailVerificationPending({
|
|
@@ -120,9 +247,19 @@ function EmailVerificationPending({
|
|
|
120
247
|
setResendStatus("sending");
|
|
121
248
|
try {
|
|
122
249
|
const supabase = createClient();
|
|
250
|
+
const safeRedirectTo = getSafeInternalRedirect(
|
|
251
|
+
redirectTo,
|
|
252
|
+
defaultAuthLinks.defaultRedirect
|
|
253
|
+
);
|
|
123
254
|
const { error } = await supabase.auth.resend({
|
|
124
255
|
type: "signup",
|
|
125
|
-
email
|
|
256
|
+
email,
|
|
257
|
+
options: {
|
|
258
|
+
emailRedirectTo: buildOAuthRedirectTo(
|
|
259
|
+
window.location.origin,
|
|
260
|
+
safeRedirectTo
|
|
261
|
+
)
|
|
262
|
+
}
|
|
126
263
|
});
|
|
127
264
|
if (error) {
|
|
128
265
|
setResendStatus("error");
|
|
@@ -242,17 +379,6 @@ function EmailVerificationPending({
|
|
|
242
379
|
}
|
|
243
380
|
);
|
|
244
381
|
}
|
|
245
|
-
|
|
246
|
-
// src/types.ts
|
|
247
|
-
var defaultAuthLinks = {
|
|
248
|
-
signIn: "/signin",
|
|
249
|
-
signUp: "/signup",
|
|
250
|
-
forgotPassword: "/forgot-password",
|
|
251
|
-
resetPassword: "/reset-password",
|
|
252
|
-
callback: "/callback",
|
|
253
|
-
home: "/",
|
|
254
|
-
defaultRedirect: "/dashboard"
|
|
255
|
-
};
|
|
256
382
|
function AuthHeader({
|
|
257
383
|
logo,
|
|
258
384
|
title,
|
|
@@ -274,82 +400,6 @@ function AuthHeader({
|
|
|
274
400
|
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold tracking-tight text-foreground", children: title })
|
|
275
401
|
] });
|
|
276
402
|
}
|
|
277
|
-
var warnedSupabaseRedirectOrigins = /* @__PURE__ */ new Set();
|
|
278
|
-
function sanitizeAuthError(message) {
|
|
279
|
-
const lowerMessage = message.toLowerCase();
|
|
280
|
-
if (lowerMessage.includes("user already registered") || lowerMessage.includes("already registered") || lowerMessage.includes("already exists") || lowerMessage.includes("email already") || lowerMessage.includes("duplicate")) {
|
|
281
|
-
return "An account with this email already exists. Please sign in instead.";
|
|
282
|
-
}
|
|
283
|
-
if (lowerMessage.includes("invalid login credentials") || lowerMessage.includes("invalid credentials") || lowerMessage.includes("wrong password") || lowerMessage.includes("incorrect password")) {
|
|
284
|
-
return "Invalid email or password. Please try again.";
|
|
285
|
-
}
|
|
286
|
-
if (lowerMessage.includes("invalid email") || lowerMessage.includes("password")) {
|
|
287
|
-
return message;
|
|
288
|
-
}
|
|
289
|
-
if (message === "The string did not match the expected pattern.") {
|
|
290
|
-
return "Please check your email address format and try again.";
|
|
291
|
-
}
|
|
292
|
-
if (lowerMessage.includes("rate limit") || lowerMessage.includes("too many requests") || lowerMessage.includes("try again later")) {
|
|
293
|
-
return "Too many attempts. Please wait a moment and try again.";
|
|
294
|
-
}
|
|
295
|
-
if (lowerMessage.includes("hook") || lowerMessage.includes("authorization") || lowerMessage.includes("token") || lowerMessage.includes("internal") || lowerMessage.includes("server") || lowerMessage.includes("database") || lowerMessage.includes("connection")) {
|
|
296
|
-
return "An error occurred. Please try again.";
|
|
297
|
-
}
|
|
298
|
-
return "An error occurred. Please try again.";
|
|
299
|
-
}
|
|
300
|
-
function sanitizeSignupError(message) {
|
|
301
|
-
const sanitized = sanitizeAuthError(message);
|
|
302
|
-
if (sanitized === "An error occurred. Please try again.") {
|
|
303
|
-
return "An error occurred during sign up. Please try again.";
|
|
304
|
-
}
|
|
305
|
-
return sanitized;
|
|
306
|
-
}
|
|
307
|
-
function buildOAuthRedirectTo(origin, nextPath) {
|
|
308
|
-
const siteUrl = origin.replace(/\/$/, "");
|
|
309
|
-
return `${siteUrl}/callback?next=${encodeURIComponent(nextPath)}`;
|
|
310
|
-
}
|
|
311
|
-
function buildOAuthSignInOptions(provider, redirectTo, options = {}) {
|
|
312
|
-
return {
|
|
313
|
-
redirectTo,
|
|
314
|
-
...provider === "azure" ? { scopes: "email" } : {},
|
|
315
|
-
...options
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
function warnAboutSupabaseRedirectAllowlist(origin) {
|
|
319
|
-
const normalizedOrigin = origin.replace(/\/$/, "");
|
|
320
|
-
let hostname = "";
|
|
321
|
-
try {
|
|
322
|
-
hostname = new URL(normalizedOrigin).host;
|
|
323
|
-
} catch {
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (!isPreviewEnvironment(hostname)) {
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
if (warnedSupabaseRedirectOrigins.has(normalizedOrigin)) {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
warnedSupabaseRedirectOrigins.add(normalizedOrigin);
|
|
333
|
-
console.warn(
|
|
334
|
-
`[identity] Supabase redirect allowlist reminder: add "${normalizedOrigin}/**" to Supabase Auth > URL Configuration > Additional Redirect URLs. The auth components already use the active origin for callbacks, but Supabase must allow that origin too or OAuth/email flows can bounce to another host.`
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
function clearStalePKCEState() {
|
|
338
|
-
if (typeof window === "undefined") return;
|
|
339
|
-
try {
|
|
340
|
-
const keysToRemove = [];
|
|
341
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
342
|
-
const key = localStorage.key(i);
|
|
343
|
-
if (key?.includes("-code-verifier")) {
|
|
344
|
-
keysToRemove.push(key);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
for (const key of keysToRemove) {
|
|
348
|
-
localStorage.removeItem(key);
|
|
349
|
-
}
|
|
350
|
-
} catch {
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
403
|
function ForgotPasswordForm({
|
|
354
404
|
createClient,
|
|
355
405
|
logo,
|
|
@@ -1090,7 +1140,14 @@ function SigninForm({
|
|
|
1090
1140
|
const oauthTimeoutRef = useRef(null);
|
|
1091
1141
|
const router = useRouter();
|
|
1092
1142
|
const searchParams = useSearchParams();
|
|
1093
|
-
const
|
|
1143
|
+
const defaultRedirect = getSafeInternalRedirect(
|
|
1144
|
+
mergedLinks.defaultRedirect,
|
|
1145
|
+
defaultAuthLinks.defaultRedirect
|
|
1146
|
+
);
|
|
1147
|
+
const redirectTo = getSafeInternalRedirect(
|
|
1148
|
+
searchParams.get("redirect"),
|
|
1149
|
+
defaultRedirect
|
|
1150
|
+
);
|
|
1094
1151
|
const message = searchParams.get("message");
|
|
1095
1152
|
const mfaRequired = searchParams.get("mfa") === "required";
|
|
1096
1153
|
const [showMFA, setShowMFA] = useState(mfaRequired);
|
|
@@ -1262,7 +1319,7 @@ function SigninForm({
|
|
|
1262
1319
|
} else {
|
|
1263
1320
|
onAuthEvent?.onSignInSuccess?.(userId, email.trim(), false);
|
|
1264
1321
|
recordSignIn("email", email.trim());
|
|
1265
|
-
|
|
1322
|
+
hardRedirect(redirectTo);
|
|
1266
1323
|
}
|
|
1267
1324
|
} catch (err) {
|
|
1268
1325
|
logError(err, {
|
|
@@ -1422,10 +1479,13 @@ function SigninForm({
|
|
|
1422
1479
|
}
|
|
1423
1480
|
);
|
|
1424
1481
|
}
|
|
1482
|
+
var EMPTY_CONSENT_ITEMS = [];
|
|
1425
1483
|
function SignupForm({
|
|
1426
1484
|
createClient,
|
|
1427
1485
|
logo,
|
|
1428
1486
|
appName = "your account",
|
|
1487
|
+
title,
|
|
1488
|
+
initialEmail = "",
|
|
1429
1489
|
termsUrl = "/terms",
|
|
1430
1490
|
privacyUrl = "/privacy",
|
|
1431
1491
|
links = {},
|
|
@@ -1437,7 +1497,7 @@ function SignupForm({
|
|
|
1437
1497
|
oauthIconMonochromeOnHover = false,
|
|
1438
1498
|
className,
|
|
1439
1499
|
onAuthEvent,
|
|
1440
|
-
consentItems =
|
|
1500
|
+
consentItems = EMPTY_CONSENT_ITEMS,
|
|
1441
1501
|
consentPosition = "above",
|
|
1442
1502
|
consentSize = "default",
|
|
1443
1503
|
showHomeLink = true,
|
|
@@ -1447,7 +1507,11 @@ function SignupForm({
|
|
|
1447
1507
|
const Link = LinkComponent;
|
|
1448
1508
|
const Image = ImageComponent;
|
|
1449
1509
|
const router = useRouter();
|
|
1450
|
-
const
|
|
1510
|
+
const redirectTo = getSafeInternalRedirect(
|
|
1511
|
+
mergedLinks.defaultRedirect,
|
|
1512
|
+
defaultAuthLinks.defaultRedirect
|
|
1513
|
+
);
|
|
1514
|
+
const [email, setEmail] = useState(initialEmail);
|
|
1451
1515
|
const [emailTouched, setEmailTouched] = useState(false);
|
|
1452
1516
|
const [password, setPassword] = useState("");
|
|
1453
1517
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
@@ -1512,7 +1576,7 @@ function SignupForm({
|
|
|
1512
1576
|
provider,
|
|
1513
1577
|
options: buildOAuthSignInOptions(
|
|
1514
1578
|
provider,
|
|
1515
|
-
buildOAuthRedirectTo(siteUrl,
|
|
1579
|
+
buildOAuthRedirectTo(siteUrl, redirectTo)
|
|
1516
1580
|
)
|
|
1517
1581
|
});
|
|
1518
1582
|
if (error) {
|
|
@@ -1547,7 +1611,7 @@ function SignupForm({
|
|
|
1547
1611
|
email: email.trim(),
|
|
1548
1612
|
password,
|
|
1549
1613
|
options: {
|
|
1550
|
-
emailRedirectTo:
|
|
1614
|
+
emailRedirectTo: buildOAuthRedirectTo(siteUrl, redirectTo),
|
|
1551
1615
|
// Pass all consent values as user metadata
|
|
1552
1616
|
data: consentState
|
|
1553
1617
|
}
|
|
@@ -1565,7 +1629,7 @@ function SignupForm({
|
|
|
1565
1629
|
} else if (data.user) {
|
|
1566
1630
|
const userId = data.user.id;
|
|
1567
1631
|
onAuthEvent?.onSignUpSuccess?.(userId, email.trim());
|
|
1568
|
-
router.push(
|
|
1632
|
+
router.push(redirectTo);
|
|
1569
1633
|
router.refresh();
|
|
1570
1634
|
} else {
|
|
1571
1635
|
onAuthEvent?.onSignUpFailure?.(email.trim(), "no_user_data_in_response");
|
|
@@ -1581,7 +1645,7 @@ function SignupForm({
|
|
|
1581
1645
|
logo,
|
|
1582
1646
|
ImageComponent: Image,
|
|
1583
1647
|
createClient,
|
|
1584
|
-
redirectTo
|
|
1648
|
+
redirectTo,
|
|
1585
1649
|
className
|
|
1586
1650
|
}
|
|
1587
1651
|
);
|
|
@@ -1603,7 +1667,7 @@ function SignupForm({
|
|
|
1603
1667
|
AuthHeader,
|
|
1604
1668
|
{
|
|
1605
1669
|
logo: showLogo !== false ? logo : void 0,
|
|
1606
|
-
title: `Sign up for ${appName}`,
|
|
1670
|
+
title: title ?? `Sign up for ${appName}`,
|
|
1607
1671
|
ImageComponent: Image
|
|
1608
1672
|
}
|
|
1609
1673
|
),
|
|
@@ -3717,6 +3781,7 @@ function useVerificationFlow({
|
|
|
3717
3781
|
};
|
|
3718
3782
|
}
|
|
3719
3783
|
function EmailSection({
|
|
3784
|
+
changeButtonPresentation = "label",
|
|
3720
3785
|
createClient,
|
|
3721
3786
|
userId: _userId,
|
|
3722
3787
|
email,
|
|
@@ -3744,6 +3809,26 @@ function EmailSection({
|
|
|
3744
3809
|
const handleStartChange = () => {
|
|
3745
3810
|
openEntryStep();
|
|
3746
3811
|
};
|
|
3812
|
+
const changeButton = changeButtonPresentation === "icon" ? /* @__PURE__ */ jsx(
|
|
3813
|
+
Button,
|
|
3814
|
+
{
|
|
3815
|
+
"aria-label": "Change email",
|
|
3816
|
+
onClick: handleStartChange,
|
|
3817
|
+
size: "icon-sm",
|
|
3818
|
+
type: "button",
|
|
3819
|
+
variant: "outline",
|
|
3820
|
+
children: /* @__PURE__ */ jsx(PencilLine, { "aria-hidden": "true", className: "size-4" })
|
|
3821
|
+
}
|
|
3822
|
+
) : /* @__PURE__ */ jsx(
|
|
3823
|
+
Button,
|
|
3824
|
+
{
|
|
3825
|
+
onClick: handleStartChange,
|
|
3826
|
+
size: "sm",
|
|
3827
|
+
type: "button",
|
|
3828
|
+
variant: "outline",
|
|
3829
|
+
children: "Change"
|
|
3830
|
+
}
|
|
3831
|
+
);
|
|
3747
3832
|
const handleSendCode = async () => {
|
|
3748
3833
|
if (!newEmail || !newEmail.includes("@")) {
|
|
3749
3834
|
setError("Please enter a valid email address");
|
|
@@ -3823,16 +3908,10 @@ function EmailSection({
|
|
|
3823
3908
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: email })
|
|
3824
3909
|
] })
|
|
3825
3910
|
] }),
|
|
3826
|
-
/* @__PURE__ */ jsx(
|
|
3827
|
-
|
|
3828
|
-
{
|
|
3829
|
-
|
|
3830
|
-
variant: "outline",
|
|
3831
|
-
size: "sm",
|
|
3832
|
-
onClick: handleStartChange,
|
|
3833
|
-
children: "Change"
|
|
3834
|
-
}
|
|
3835
|
-
),
|
|
3911
|
+
changeButtonPresentation === "icon" ? /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 300, children: /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
3912
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: changeButton }),
|
|
3913
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "top", children: "Change email" })
|
|
3914
|
+
] }) }) : changeButton,
|
|
3836
3915
|
/* @__PURE__ */ jsx(Dialog, { open: dialogOpen, onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [
|
|
3837
3916
|
step === "enter-email" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3838
3917
|
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
@@ -5530,12 +5609,6 @@ var EMAIL_OTP_TYPES = [
|
|
|
5530
5609
|
"email_change",
|
|
5531
5610
|
"email"
|
|
5532
5611
|
];
|
|
5533
|
-
function getSafeRedirectPath(candidate, fallback) {
|
|
5534
|
-
if (candidate?.startsWith("/") && !candidate.startsWith("//")) {
|
|
5535
|
-
return candidate;
|
|
5536
|
-
}
|
|
5537
|
-
return fallback;
|
|
5538
|
-
}
|
|
5539
5612
|
function getEmailOtpType(value) {
|
|
5540
5613
|
if (!value) {
|
|
5541
5614
|
return null;
|
|
@@ -5626,8 +5699,9 @@ function createRedirectResponse(target, origin) {
|
|
|
5626
5699
|
if (target instanceof URL) {
|
|
5627
5700
|
return NextResponse.redirect(target.toString());
|
|
5628
5701
|
}
|
|
5629
|
-
|
|
5630
|
-
|
|
5702
|
+
const safeTarget = getSafeInternalRedirect(target, null);
|
|
5703
|
+
if (safeTarget) {
|
|
5704
|
+
return NextResponse.redirect(`${origin}${safeTarget}`);
|
|
5631
5705
|
}
|
|
5632
5706
|
return null;
|
|
5633
5707
|
}
|
|
@@ -5689,15 +5763,20 @@ function createAuthCallbackHandler({
|
|
|
5689
5763
|
isLinkingFlow = isDefaultLinkingFlow,
|
|
5690
5764
|
resolveLinkingErrorMessage
|
|
5691
5765
|
}) {
|
|
5766
|
+
const safeDefaultRedirect = getSafeInternalRedirect(
|
|
5767
|
+
defaultRedirect,
|
|
5768
|
+
"/dashboard"
|
|
5769
|
+
);
|
|
5770
|
+
const safeSignInPath = getSafeInternalRedirect(signInPath, "/signin");
|
|
5692
5771
|
return async function GET(request) {
|
|
5693
5772
|
const requestUrl = new URL(request.url);
|
|
5694
5773
|
const code = requestUrl.searchParams.get("code");
|
|
5695
5774
|
const tokenHash = requestUrl.searchParams.get("token_hash");
|
|
5696
5775
|
const rawOtpType = requestUrl.searchParams.get("type");
|
|
5697
5776
|
const otpType = getEmailOtpType(rawOtpType);
|
|
5698
|
-
const next =
|
|
5777
|
+
const next = getSafeInternalRedirect(
|
|
5699
5778
|
requestUrl.searchParams.get("next") ?? requestUrl.searchParams.get("redirect_to"),
|
|
5700
|
-
|
|
5779
|
+
safeDefaultRedirect
|
|
5701
5780
|
);
|
|
5702
5781
|
const origin = requestUrl.origin;
|
|
5703
5782
|
const error = requestUrl.searchParams.get("error");
|
|
@@ -5719,7 +5798,7 @@ function createAuthCallbackHandler({
|
|
|
5719
5798
|
return linkingErrorRedirect;
|
|
5720
5799
|
}
|
|
5721
5800
|
return NextResponse.redirect(
|
|
5722
|
-
`${origin}${
|
|
5801
|
+
`${origin}${safeSignInPath}?error=${encodeURIComponent(message)}`
|
|
5723
5802
|
);
|
|
5724
5803
|
}
|
|
5725
5804
|
if (code) {
|
|
@@ -5734,7 +5813,7 @@ function createAuthCallbackHandler({
|
|
|
5734
5813
|
flow: "oauth",
|
|
5735
5814
|
postAuthHook,
|
|
5736
5815
|
postAuthHookErrorMode,
|
|
5737
|
-
signInPath,
|
|
5816
|
+
signInPath: safeSignInPath,
|
|
5738
5817
|
resolveSuccessRedirect
|
|
5739
5818
|
});
|
|
5740
5819
|
}
|
|
@@ -5754,13 +5833,13 @@ function createAuthCallbackHandler({
|
|
|
5754
5833
|
return linkingErrorRedirect;
|
|
5755
5834
|
}
|
|
5756
5835
|
return NextResponse.redirect(
|
|
5757
|
-
`${origin}${
|
|
5836
|
+
`${origin}${safeSignInPath}?error=${encodeURIComponent(errorMessage)}`
|
|
5758
5837
|
);
|
|
5759
5838
|
}
|
|
5760
5839
|
if (tokenHash || rawOtpType) {
|
|
5761
5840
|
if (!tokenHash || !otpType) {
|
|
5762
5841
|
return NextResponse.redirect(
|
|
5763
|
-
`${origin}${
|
|
5842
|
+
`${origin}${safeSignInPath}?error=${encodeURIComponent("Invalid verification link")}`
|
|
5764
5843
|
);
|
|
5765
5844
|
}
|
|
5766
5845
|
const supabase = await createClient();
|
|
@@ -5778,16 +5857,16 @@ function createAuthCallbackHandler({
|
|
|
5778
5857
|
otpType,
|
|
5779
5858
|
postAuthHook,
|
|
5780
5859
|
postAuthHookErrorMode,
|
|
5781
|
-
signInPath,
|
|
5860
|
+
signInPath: safeSignInPath,
|
|
5782
5861
|
resolveSuccessRedirect
|
|
5783
5862
|
});
|
|
5784
5863
|
}
|
|
5785
5864
|
return NextResponse.redirect(
|
|
5786
|
-
`${origin}${
|
|
5865
|
+
`${origin}${safeSignInPath}?error=${encodeURIComponent(verifyError.message)}`
|
|
5787
5866
|
);
|
|
5788
5867
|
}
|
|
5789
5868
|
return NextResponse.redirect(
|
|
5790
|
-
`${origin}${
|
|
5869
|
+
`${origin}${safeSignInPath}?error=No authorization code received`
|
|
5791
5870
|
);
|
|
5792
5871
|
};
|
|
5793
5872
|
}
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,43 @@
|
|
|
3
3
|
var server = require('next/server');
|
|
4
4
|
var sharedUtils = require('@caffeinebounce/shared-utils');
|
|
5
5
|
|
|
6
|
+
// src/handlers/callback.ts
|
|
7
|
+
|
|
8
|
+
// src/utils/redirect.ts
|
|
9
|
+
var INTERNAL_REDIRECT_BASE = new URL("https://identity.invalid");
|
|
10
|
+
var ENCODED_PATH_SEPARATOR = /%(?:2f|5c)/i;
|
|
11
|
+
function hasControlCharacter(value) {
|
|
12
|
+
return Array.from(value).some((character) => {
|
|
13
|
+
const code = character.charCodeAt(0);
|
|
14
|
+
return code <= 31 || code === 127;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function isStrictInternalRedirect(candidate) {
|
|
18
|
+
if (!candidate?.startsWith("/") || hasControlCharacter(candidate)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const pathEnd = candidate.search(/[?#]/);
|
|
22
|
+
const pathname = pathEnd === -1 ? candidate : candidate.slice(0, pathEnd);
|
|
23
|
+
if (pathname.includes("\\") || ENCODED_PATH_SEPARATOR.test(pathname)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const target = new URL(candidate, INTERNAL_REDIRECT_BASE);
|
|
28
|
+
return target.origin === INTERNAL_REDIRECT_BASE.origin;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function getSafeInternalRedirect(candidate, fallback) {
|
|
34
|
+
if (isStrictInternalRedirect(candidate)) {
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
if (fallback === null) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return isStrictInternalRedirect(fallback) ? fallback : "/";
|
|
41
|
+
}
|
|
42
|
+
|
|
6
43
|
// src/handlers/callback.ts
|
|
7
44
|
var POST_AUTH_HOOK_ERROR_MESSAGE = "Authentication completed, but setup failed. Please try again.";
|
|
8
45
|
var DEFAULT_LINKING_ACCOUNT_ERROR_MESSAGE = "This account is already connected to another account. Each external account can only be linked to one account.";
|
|
@@ -14,12 +51,6 @@ var EMAIL_OTP_TYPES = [
|
|
|
14
51
|
"email_change",
|
|
15
52
|
"email"
|
|
16
53
|
];
|
|
17
|
-
function getSafeRedirectPath(candidate, fallback) {
|
|
18
|
-
if (candidate?.startsWith("/") && !candidate.startsWith("//")) {
|
|
19
|
-
return candidate;
|
|
20
|
-
}
|
|
21
|
-
return fallback;
|
|
22
|
-
}
|
|
23
54
|
function getEmailOtpType(value) {
|
|
24
55
|
if (!value) {
|
|
25
56
|
return null;
|
|
@@ -110,8 +141,9 @@ function createRedirectResponse(target, origin) {
|
|
|
110
141
|
if (target instanceof URL) {
|
|
111
142
|
return server.NextResponse.redirect(target.toString());
|
|
112
143
|
}
|
|
113
|
-
|
|
114
|
-
|
|
144
|
+
const safeTarget = getSafeInternalRedirect(target, null);
|
|
145
|
+
if (safeTarget) {
|
|
146
|
+
return server.NextResponse.redirect(`${origin}${safeTarget}`);
|
|
115
147
|
}
|
|
116
148
|
return null;
|
|
117
149
|
}
|
|
@@ -173,15 +205,20 @@ function createAuthCallbackHandler({
|
|
|
173
205
|
isLinkingFlow = isDefaultLinkingFlow,
|
|
174
206
|
resolveLinkingErrorMessage
|
|
175
207
|
}) {
|
|
208
|
+
const safeDefaultRedirect = getSafeInternalRedirect(
|
|
209
|
+
defaultRedirect,
|
|
210
|
+
"/dashboard"
|
|
211
|
+
);
|
|
212
|
+
const safeSignInPath = getSafeInternalRedirect(signInPath, "/signin");
|
|
176
213
|
return async function GET(request) {
|
|
177
214
|
const requestUrl = new URL(request.url);
|
|
178
215
|
const code = requestUrl.searchParams.get("code");
|
|
179
216
|
const tokenHash = requestUrl.searchParams.get("token_hash");
|
|
180
217
|
const rawOtpType = requestUrl.searchParams.get("type");
|
|
181
218
|
const otpType = getEmailOtpType(rawOtpType);
|
|
182
|
-
const next =
|
|
219
|
+
const next = getSafeInternalRedirect(
|
|
183
220
|
requestUrl.searchParams.get("next") ?? requestUrl.searchParams.get("redirect_to"),
|
|
184
|
-
|
|
221
|
+
safeDefaultRedirect
|
|
185
222
|
);
|
|
186
223
|
const origin = requestUrl.origin;
|
|
187
224
|
const error = requestUrl.searchParams.get("error");
|
|
@@ -203,7 +240,7 @@ function createAuthCallbackHandler({
|
|
|
203
240
|
return linkingErrorRedirect;
|
|
204
241
|
}
|
|
205
242
|
return server.NextResponse.redirect(
|
|
206
|
-
`${origin}${
|
|
243
|
+
`${origin}${safeSignInPath}?error=${encodeURIComponent(message)}`
|
|
207
244
|
);
|
|
208
245
|
}
|
|
209
246
|
if (code) {
|
|
@@ -218,7 +255,7 @@ function createAuthCallbackHandler({
|
|
|
218
255
|
flow: "oauth",
|
|
219
256
|
postAuthHook,
|
|
220
257
|
postAuthHookErrorMode,
|
|
221
|
-
signInPath,
|
|
258
|
+
signInPath: safeSignInPath,
|
|
222
259
|
resolveSuccessRedirect
|
|
223
260
|
});
|
|
224
261
|
}
|
|
@@ -238,13 +275,13 @@ function createAuthCallbackHandler({
|
|
|
238
275
|
return linkingErrorRedirect;
|
|
239
276
|
}
|
|
240
277
|
return server.NextResponse.redirect(
|
|
241
|
-
`${origin}${
|
|
278
|
+
`${origin}${safeSignInPath}?error=${encodeURIComponent(errorMessage)}`
|
|
242
279
|
);
|
|
243
280
|
}
|
|
244
281
|
if (tokenHash || rawOtpType) {
|
|
245
282
|
if (!tokenHash || !otpType) {
|
|
246
283
|
return server.NextResponse.redirect(
|
|
247
|
-
`${origin}${
|
|
284
|
+
`${origin}${safeSignInPath}?error=${encodeURIComponent("Invalid verification link")}`
|
|
248
285
|
);
|
|
249
286
|
}
|
|
250
287
|
const supabase = await createClient();
|
|
@@ -262,16 +299,16 @@ function createAuthCallbackHandler({
|
|
|
262
299
|
otpType,
|
|
263
300
|
postAuthHook,
|
|
264
301
|
postAuthHookErrorMode,
|
|
265
|
-
signInPath,
|
|
302
|
+
signInPath: safeSignInPath,
|
|
266
303
|
resolveSuccessRedirect
|
|
267
304
|
});
|
|
268
305
|
}
|
|
269
306
|
return server.NextResponse.redirect(
|
|
270
|
-
`${origin}${
|
|
307
|
+
`${origin}${safeSignInPath}?error=${encodeURIComponent(verifyError.message)}`
|
|
271
308
|
);
|
|
272
309
|
}
|
|
273
310
|
return server.NextResponse.redirect(
|
|
274
|
-
`${origin}${
|
|
311
|
+
`${origin}${safeSignInPath}?error=No authorization code received`
|
|
275
312
|
);
|
|
276
313
|
};
|
|
277
314
|
}
|