cauthy-auth 0.11.0 → 0.13.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/README.md +3 -2
- package/dist/cli.js +299 -20
- package/dist/client.d.mts +94 -1
- package/dist/client.d.ts +94 -1
- package/dist/client.js +107 -9
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +106 -9
- package/dist/client.mjs.map +1 -1
- package/dist/index.js +67 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +67 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +123 -1
- package/dist/react.d.ts +123 -1
- package/dist/react.js +358 -17
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +356 -17
- package/dist/react.mjs.map +1 -1
- package/dist/styles.css +5 -0
- package/dist/svelte.js +105 -9
- package/dist/svelte.js.map +1 -1
- package/dist/svelte.mjs +105 -9
- package/dist/svelte.mjs.map +1 -1
- package/dist/vue.js +105 -9
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +105 -9
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react.mjs
CHANGED
|
@@ -7,6 +7,15 @@ import { createContext, useCallback, useContext, useEffect as useEffect2, useMem
|
|
|
7
7
|
function canUsePasskeys() {
|
|
8
8
|
return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
|
|
9
9
|
}
|
|
10
|
+
async function canAutofillPasskeys() {
|
|
11
|
+
if (!canUsePasskeys()) return false;
|
|
12
|
+
const pkc = window.PublicKeyCredential;
|
|
13
|
+
try {
|
|
14
|
+
return typeof pkc.isConditionalMediationAvailable === "function" && await pkc.isConditionalMediationAvailable() === true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
10
19
|
function bytes(text2) {
|
|
11
20
|
const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
|
|
12
21
|
const bin = atob(b64);
|
|
@@ -22,9 +31,9 @@ function text(buf) {
|
|
|
22
31
|
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
23
32
|
}
|
|
24
33
|
var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
|
|
25
|
-
async function getPasskey(options) {
|
|
34
|
+
async function getPasskey(options, how = {}) {
|
|
26
35
|
const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
|
|
27
|
-
const cred = await navigator.credentials.get({ publicKey });
|
|
36
|
+
const cred = await navigator.credentials.get({ publicKey, ...how });
|
|
28
37
|
if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
|
|
29
38
|
const r = cred.response;
|
|
30
39
|
return {
|
|
@@ -168,6 +177,24 @@ function firstProblem(...checks) {
|
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
// src/client.ts
|
|
180
|
+
var googleScript = null;
|
|
181
|
+
function loadGoogleIdentity() {
|
|
182
|
+
const ready = () => window.google?.accounts?.id;
|
|
183
|
+
if (ready()) return Promise.resolve(ready());
|
|
184
|
+
googleScript ??= new Promise((resolve, reject) => {
|
|
185
|
+
const script = document.createElement("script");
|
|
186
|
+
script.src = "https://accounts.google.com/gsi/client";
|
|
187
|
+
script.async = true;
|
|
188
|
+
script.onload = () => ready() ? resolve(ready()) : reject(new Error("google identity services did not load"));
|
|
189
|
+
script.onerror = () => {
|
|
190
|
+
googleScript = null;
|
|
191
|
+
reject(new Error("google identity services did not load"));
|
|
192
|
+
};
|
|
193
|
+
document.head.appendChild(script);
|
|
194
|
+
});
|
|
195
|
+
return googleScript;
|
|
196
|
+
}
|
|
197
|
+
var PASSKEY_SECONDS = 300;
|
|
171
198
|
var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." };
|
|
172
199
|
function createAuthClient(options = {}) {
|
|
173
200
|
const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
|
|
@@ -176,13 +203,13 @@ function createAuthClient(options = {}) {
|
|
|
176
203
|
const said = typeof options.locale === "function" ? options.locale() : options.locale;
|
|
177
204
|
return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
|
|
178
205
|
};
|
|
179
|
-
async function post(path, form, bad) {
|
|
206
|
+
async function post(path, form, bad, captchaToken) {
|
|
180
207
|
if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
|
|
181
208
|
try {
|
|
182
209
|
const res = await doFetch(`${base}${path}`, {
|
|
183
210
|
method: "POST",
|
|
184
211
|
credentials: "same-origin",
|
|
185
|
-
headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {} },
|
|
212
|
+
headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {}, ...captchaToken ? { "x-cauthy-captcha": captchaToken } : {} },
|
|
186
213
|
body: JSON.stringify(form)
|
|
187
214
|
});
|
|
188
215
|
const body = await res.json().catch(() => null);
|
|
@@ -234,9 +261,11 @@ function createAuthClient(options = {}) {
|
|
|
234
261
|
}
|
|
235
262
|
return {
|
|
236
263
|
signIn: {
|
|
237
|
-
email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
|
|
264
|
+
email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password)), f.captchaToken),
|
|
238
265
|
mfa: (f) => post("/sign-in/mfa", { code: f.code }, checkCode(f.code)),
|
|
239
|
-
|
|
266
|
+
mfaMethods: () => post("/sign-in/mfa/methods", {}, null),
|
|
267
|
+
mfaSendCode: () => post("/sign-in/mfa/send", {}, null),
|
|
268
|
+
magicLink: (f) => post("/sign-in/magic-link", { email: f.email }, checkEmail(f.email), f.captchaToken),
|
|
240
269
|
magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
|
|
241
270
|
emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
|
|
242
271
|
passkeyUrl,
|
|
@@ -251,15 +280,75 @@ function createAuthClient(options = {}) {
|
|
|
251
280
|
window.location.assign(passkeyUrl(o));
|
|
252
281
|
return { ok: true, status: "redirected" };
|
|
253
282
|
},
|
|
283
|
+
async passkeyAutofill(o = {}) {
|
|
284
|
+
const cancelled = { ok: false, error: { code: "cancelled", message: "No passkey was chosen." } };
|
|
285
|
+
const m = await methods();
|
|
286
|
+
if (!m.passkeyHere || !await canAutofillPasskeys()) return cancelled;
|
|
287
|
+
for (; ; ) {
|
|
288
|
+
if (o.signal?.aborted) return cancelled;
|
|
289
|
+
const begun = await post("/sign-in/passkey/begin", {}, null);
|
|
290
|
+
if (!begun.ok) return begun;
|
|
291
|
+
const renew = new AbortController();
|
|
292
|
+
const stop = () => renew.abort();
|
|
293
|
+
o.signal?.addEventListener("abort", stop, { once: true });
|
|
294
|
+
const timer = setTimeout(stop, (PASSKEY_SECONDS - 30) * 1e3);
|
|
295
|
+
let credential;
|
|
296
|
+
try {
|
|
297
|
+
credential = await getPasskey(begun.options, { mediation: "conditional", signal: renew.signal });
|
|
298
|
+
} catch (e) {
|
|
299
|
+
if (o.signal?.aborted) return cancelled;
|
|
300
|
+
if (renew.signal.aborted) continue;
|
|
301
|
+
return { ok: false, error: ceremonyError(e) };
|
|
302
|
+
} finally {
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
o.signal?.removeEventListener("abort", stop);
|
|
305
|
+
}
|
|
306
|
+
return post("/sign-in/passkey/finish", { credential }, null);
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
async googleOneTap(o = {}) {
|
|
310
|
+
const cancelled = { ok: false, error: { code: "cancelled", message: "No Google account was chosen." } };
|
|
311
|
+
if (typeof window === "undefined" || o.signal?.aborted) return cancelled;
|
|
312
|
+
const begun = await post("/sign-in/google-one-tap/begin", {}, null);
|
|
313
|
+
if (!begun.ok) return begun;
|
|
314
|
+
let gis;
|
|
315
|
+
try {
|
|
316
|
+
gis = await loadGoogleIdentity();
|
|
317
|
+
} catch {
|
|
318
|
+
return { ok: false, error: { code: "unavailable", message: "Google could not be reached. Try again, or sign in another way." } };
|
|
319
|
+
}
|
|
320
|
+
const credential = await new Promise((done) => {
|
|
321
|
+
o.signal?.addEventListener("abort", () => {
|
|
322
|
+
gis.cancel();
|
|
323
|
+
done(null);
|
|
324
|
+
}, { once: true });
|
|
325
|
+
gis.initialize({
|
|
326
|
+
client_id: begun.clientId,
|
|
327
|
+
nonce: begun.nonce,
|
|
328
|
+
context: o.context ?? "signin",
|
|
329
|
+
callback: (r) => done(r.credential ?? null),
|
|
330
|
+
auto_select: false,
|
|
331
|
+
cancel_on_tap_outside: true,
|
|
332
|
+
itp_support: true,
|
|
333
|
+
use_fedcm_for_prompt: true
|
|
334
|
+
});
|
|
335
|
+
gis.prompt((moment) => {
|
|
336
|
+
if (moment.isNotDisplayed?.() || moment.isSkippedMoment?.()) done(null);
|
|
337
|
+
else if (moment.isDismissedMoment?.() && moment.getDismissedReason?.() !== "credential_returned") done(null);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
if (credential === null) return cancelled;
|
|
341
|
+
return post("/sign-in/google-one-tap", { credential }, null);
|
|
342
|
+
},
|
|
254
343
|
socialUrl,
|
|
255
344
|
social(provider, o) {
|
|
256
345
|
window.location.assign(socialUrl(provider, o));
|
|
257
346
|
}
|
|
258
347
|
},
|
|
259
348
|
signUp: {
|
|
260
|
-
email: (f) => post("/sign-up/email", { email: f.email, password: f.password, name: f.name }, firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password)))
|
|
349
|
+
email: (f) => post("/sign-up/email", { email: f.email, password: f.password, name: f.name }, firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password)), f.captchaToken)
|
|
261
350
|
},
|
|
262
|
-
forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email)),
|
|
351
|
+
forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email), f.captchaToken),
|
|
263
352
|
resetPassword: (f) => post("/reset-password", { token: f.token, password: f.password }, checkNewPassword(f.password)),
|
|
264
353
|
verifyEmail: (f) => post("/verify-email", { token: f.token }, null),
|
|
265
354
|
signOut: () => post("/sign-out", {}, null),
|
|
@@ -277,7 +366,14 @@ function createAuthClient(options = {}) {
|
|
|
277
366
|
setup: (f) => post("/me/mfa/setup", { password: f.password }, null),
|
|
278
367
|
verify: (f) => post("/me/mfa/verify", { code: f.code }, checkCode(f.code)),
|
|
279
368
|
disable: (f) => post("/me/mfa/disable", { password: f.password }, null),
|
|
280
|
-
recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null)
|
|
369
|
+
recoveryCodes: (f) => post("/me/mfa/recovery-codes", { password: f.password }, null),
|
|
370
|
+
async factors() {
|
|
371
|
+
const body = await read("/me/mfa/factors");
|
|
372
|
+
return body && typeof body.totp === "boolean" ? body : null;
|
|
373
|
+
},
|
|
374
|
+
addPhone: (f) => post("/me/mfa/phone", { phone: f.phone, channel: f.channel, password: f.password }, null),
|
|
375
|
+
verifyPhone: (f) => post("/me/mfa/phone/verify", { code: f.code }, checkCode(f.code)),
|
|
376
|
+
removePhone: (f) => post("/me/mfa/phone/remove", { password: f.password }, null)
|
|
281
377
|
},
|
|
282
378
|
changeEmail: (f) => post("/me/email", { email: f.email, password: f.password }, checkEmail(f.email)),
|
|
283
379
|
async identities() {
|
|
@@ -395,6 +491,18 @@ var TEXTS = {
|
|
|
395
491
|
linkSent: "If that address has an account, a link to choose a new password is on its way.",
|
|
396
492
|
codeHelp: "Enter the code from your authenticator app, or a recovery code.",
|
|
397
493
|
verify: "Verify",
|
|
494
|
+
codeHelpPhone: "We send a code to your phone. Ask for it, then type it here, or use a recovery code.",
|
|
495
|
+
sendCodeSms: "Send a code by SMS to {to}",
|
|
496
|
+
sendCodeWhatsapp: "Send a code by WhatsApp to {to}",
|
|
497
|
+
codeSent: "We sent a code to {to}. Type it here.",
|
|
498
|
+
phone: "Phone, with its country code",
|
|
499
|
+
phoneHelp: "Get your codes by SMS or WhatsApp instead of an app.",
|
|
500
|
+
phoneOn: "Codes go to {to}.",
|
|
501
|
+
phoneOff: "No phone yet.",
|
|
502
|
+
addPhone: "Add phone",
|
|
503
|
+
removePhone: "Remove phone",
|
|
504
|
+
bySms: "SMS",
|
|
505
|
+
byWhatsapp: "WhatsApp",
|
|
398
506
|
checkEmail: "Almost there: open the link we sent to confirm your address, then sign in.",
|
|
399
507
|
backToSignIn: "Back to sign in",
|
|
400
508
|
emailMeALink: "Email me a sign-in link",
|
|
@@ -462,6 +570,13 @@ var TEXTS = {
|
|
|
462
570
|
step_up_required: "Enter your current password.",
|
|
463
571
|
weak_password: "Choose a stronger password.",
|
|
464
572
|
unauthenticated: "Sign in first.",
|
|
573
|
+
phone_refused: "The code could not be sent to that number.",
|
|
574
|
+
too_many_codes: "Enough codes were sent. Use the last one, or sign in again.",
|
|
575
|
+
phone_unavailable: "Codes cannot be sent that way here.",
|
|
576
|
+
phone_already_set: "Remove the phone you have first.",
|
|
577
|
+
phone_codes_unavailable: "Codes cannot be sent right now. Try again in a moment.",
|
|
578
|
+
captcha_required: "Complete the check that you are not a bot, then try again.",
|
|
579
|
+
captcha_failed: "The check that you are not a bot did not pass. Try it again.",
|
|
465
580
|
passkey_failed: "The passkey could not be used. Try again, or sign in another way.",
|
|
466
581
|
passkey_exists: "This device already has a passkey for your account.",
|
|
467
582
|
passkey_unavailable: "Passkeys cannot be used here."
|
|
@@ -484,6 +599,18 @@ var TEXTS = {
|
|
|
484
599
|
linkSent: "Si ese correo tiene una cuenta, ya va en camino un enlace para elegir una contrase\xF1a nueva.",
|
|
485
600
|
codeHelp: "Escribe el c\xF3digo de tu aplicaci\xF3n de autenticaci\xF3n, o un c\xF3digo de recuperaci\xF3n.",
|
|
486
601
|
verify: "Verificar",
|
|
602
|
+
codeHelpPhone: "Te enviamos un c\xF3digo a tu tel\xE9fono. P\xEDdelo y escr\xEDbelo aqu\xED, o usa un c\xF3digo de recuperaci\xF3n.",
|
|
603
|
+
sendCodeSms: "Enviar un c\xF3digo por SMS a {to}",
|
|
604
|
+
sendCodeWhatsapp: "Enviar un c\xF3digo por WhatsApp a {to}",
|
|
605
|
+
codeSent: "Te enviamos un c\xF3digo a {to}. Escr\xEDbelo aqu\xED.",
|
|
606
|
+
phone: "Tel\xE9fono, con su c\xF3digo de pa\xEDs",
|
|
607
|
+
phoneHelp: "Recibe tus c\xF3digos por SMS o WhatsApp en vez de una app.",
|
|
608
|
+
phoneOn: "Los c\xF3digos van a {to}.",
|
|
609
|
+
phoneOff: "Todav\xEDa no tienes un tel\xE9fono.",
|
|
610
|
+
addPhone: "Agregar tel\xE9fono",
|
|
611
|
+
removePhone: "Quitar tel\xE9fono",
|
|
612
|
+
bySms: "SMS",
|
|
613
|
+
byWhatsapp: "WhatsApp",
|
|
487
614
|
checkEmail: "Ya casi: abre el enlace que te enviamos para confirmar tu correo y luego entra.",
|
|
488
615
|
backToSignIn: "Volver a entrar",
|
|
489
616
|
emailMeALink: "Env\xEDame un enlace para entrar",
|
|
@@ -551,6 +678,13 @@ var TEXTS = {
|
|
|
551
678
|
step_up_required: "Escribe tu contrase\xF1a actual.",
|
|
552
679
|
weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
|
|
553
680
|
unauthenticated: "Inicia sesi\xF3n primero.",
|
|
681
|
+
phone_refused: "No se pudo enviar el c\xF3digo a ese n\xFAmero.",
|
|
682
|
+
too_many_codes: "Ya se enviaron suficientes c\xF3digos. Usa el \xFAltimo o vuelve a entrar.",
|
|
683
|
+
phone_unavailable: "Aqu\xED no se pueden enviar c\xF3digos de esa forma.",
|
|
684
|
+
phone_already_set: "Primero quita el tel\xE9fono que tienes.",
|
|
685
|
+
phone_codes_unavailable: "Ahora no se pueden enviar c\xF3digos. Int\xE9ntalo en un momento.",
|
|
686
|
+
captcha_required: "Completa la verificaci\xF3n de que no eres un robot y vuelve a intentarlo.",
|
|
687
|
+
captcha_failed: "La verificaci\xF3n de que no eres un robot no pas\xF3. Int\xE9ntalo de nuevo.",
|
|
554
688
|
passkey_failed: "No se pudo usar la llave de acceso. Intenta de nuevo o entra de otra forma.",
|
|
555
689
|
passkey_exists: "Este dispositivo ya tiene una llave de acceso de tu cuenta.",
|
|
556
690
|
passkey_unavailable: "Aqu\xED no se pueden usar llaves de acceso."
|
|
@@ -595,6 +729,63 @@ function useRoot(props) {
|
|
|
595
729
|
if (props.unstyled) return {};
|
|
596
730
|
return { "data-cauthy": "", style: vars };
|
|
597
731
|
}
|
|
732
|
+
var turnstileScript = null;
|
|
733
|
+
function loadTurnstile() {
|
|
734
|
+
const ready = () => window.turnstile;
|
|
735
|
+
if (ready()) return Promise.resolve(ready());
|
|
736
|
+
turnstileScript ??= new Promise((resolve, reject) => {
|
|
737
|
+
const script = document.createElement("script");
|
|
738
|
+
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
739
|
+
script.async = true;
|
|
740
|
+
script.onload = () => ready() ? resolve(ready()) : reject(new Error("turnstile did not load"));
|
|
741
|
+
script.onerror = () => {
|
|
742
|
+
turnstileScript = null;
|
|
743
|
+
reject(new Error("turnstile did not load"));
|
|
744
|
+
};
|
|
745
|
+
document.head.appendChild(script);
|
|
746
|
+
});
|
|
747
|
+
return turnstileScript;
|
|
748
|
+
}
|
|
749
|
+
function useChallenge(siteKey, locale) {
|
|
750
|
+
const box = useRef(null);
|
|
751
|
+
const widget = useRef(null);
|
|
752
|
+
const [token, setToken] = useState("");
|
|
753
|
+
useEffect(() => {
|
|
754
|
+
if (!siteKey || !box.current) return;
|
|
755
|
+
let gone = false;
|
|
756
|
+
void loadTurnstile().then((api) => {
|
|
757
|
+
if (gone || !box.current) return;
|
|
758
|
+
const id = api.render(box.current, {
|
|
759
|
+
sitekey: siteKey,
|
|
760
|
+
language: locale ?? "auto",
|
|
761
|
+
size: "flexible",
|
|
762
|
+
callback: (t) => setToken(t),
|
|
763
|
+
"expired-callback": () => setToken(""),
|
|
764
|
+
"error-callback": () => setToken("")
|
|
765
|
+
});
|
|
766
|
+
widget.current = { api, id };
|
|
767
|
+
}).catch(() => {
|
|
768
|
+
});
|
|
769
|
+
return () => {
|
|
770
|
+
gone = true;
|
|
771
|
+
if (widget.current) widget.current.api.remove(widget.current.id);
|
|
772
|
+
widget.current = null;
|
|
773
|
+
};
|
|
774
|
+
}, [siteKey, locale]);
|
|
775
|
+
const renew = () => {
|
|
776
|
+
setToken("");
|
|
777
|
+
if (widget.current) widget.current.api.reset(widget.current.id);
|
|
778
|
+
};
|
|
779
|
+
const node = siteKey ? /* @__PURE__ */ jsx("div", { ref: box, className: "cauthy-challenge" }) : null;
|
|
780
|
+
return { node, token: token || void 0, renew };
|
|
781
|
+
}
|
|
782
|
+
function useMethods(auth) {
|
|
783
|
+
const [methods, setMethods] = useState(null);
|
|
784
|
+
useEffect(() => {
|
|
785
|
+
void auth.methods().then(setMethods);
|
|
786
|
+
}, []);
|
|
787
|
+
return methods;
|
|
788
|
+
}
|
|
598
789
|
function useForm(props) {
|
|
599
790
|
const t = { ...TEXTS[props.locale ?? "en"], ...props.texts, errors: { ...TEXTS[props.locale ?? "en"].errors, ...props.texts?.errors } };
|
|
600
791
|
const cx = (slot) => props.classNames?.[slot] ?? `cauthy-${slot}`;
|
|
@@ -667,6 +858,25 @@ function SignIn(props) {
|
|
|
667
858
|
const [password, setPassword] = useState("");
|
|
668
859
|
const [code, setCode] = useState("");
|
|
669
860
|
const [step, setStep] = useState("password");
|
|
861
|
+
const challenge = useChallenge(methods?.captchaSiteKey, props.locale);
|
|
862
|
+
const [phoneWay, setPhoneWay] = useState(null);
|
|
863
|
+
const [phoneSent, setPhoneSent] = useState("");
|
|
864
|
+
useEffect(() => {
|
|
865
|
+
if (step !== "code") return;
|
|
866
|
+
void auth.signIn.mfaMethods().then((r) => {
|
|
867
|
+
if (r.ok) setPhoneWay({ totp: r.totp, phone: r.phone });
|
|
868
|
+
});
|
|
869
|
+
}, [step]);
|
|
870
|
+
const sendCode = async () => {
|
|
871
|
+
f.setBusy(true);
|
|
872
|
+
f.setError(null);
|
|
873
|
+
const r = await auth.signIn.mfaSendCode();
|
|
874
|
+
f.setBusy(false);
|
|
875
|
+
if (!r.ok) return f.setError(r.error);
|
|
876
|
+
setPhoneSent(r.sentTo);
|
|
877
|
+
};
|
|
878
|
+
const autofill = useRef(null);
|
|
879
|
+
const [offer, setOffer] = useState(0);
|
|
670
880
|
useEffect(() => {
|
|
671
881
|
void auth.methods().then(setMethods);
|
|
672
882
|
const back = returnedError();
|
|
@@ -681,30 +891,52 @@ function SignIn(props) {
|
|
|
681
891
|
setStep("password");
|
|
682
892
|
});
|
|
683
893
|
});
|
|
894
|
+
useEffect(() => {
|
|
895
|
+
if (step !== "password" || !methods?.passkeyHere) return;
|
|
896
|
+
const ctl = new AbortController();
|
|
897
|
+
autofill.current = ctl;
|
|
898
|
+
void auth.signIn.passkeyAutofill({ signal: ctl.signal }).then((r) => {
|
|
899
|
+
if (ctl.signal.aborted) return;
|
|
900
|
+
if (r.ok) return f.done();
|
|
901
|
+
if (r.error.code === "cancelled") return;
|
|
902
|
+
f.setError(r.error);
|
|
903
|
+
if (r.error.code === "passkey_failed" || r.error.code === "step_expired") setOffer((n) => n + 1);
|
|
904
|
+
});
|
|
905
|
+
return () => ctl.abort();
|
|
906
|
+
}, [step, methods, offer]);
|
|
684
907
|
const submit = async (e) => {
|
|
685
908
|
e.preventDefault();
|
|
909
|
+
autofill.current?.abort();
|
|
686
910
|
f.setBusy(true);
|
|
687
911
|
f.setError(null);
|
|
688
|
-
const r = step === "code" ? await auth.signIn.mfa({ code }) : step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password });
|
|
912
|
+
const r = step === "code" ? await auth.signIn.mfa({ code }) : step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password, captchaToken: challenge.token });
|
|
689
913
|
f.setBusy(false);
|
|
690
|
-
if (
|
|
914
|
+
if (step === "password") challenge.renew();
|
|
915
|
+
if (!r.ok) {
|
|
916
|
+
setOffer((n) => n + 1);
|
|
917
|
+
return f.setError(r.error);
|
|
918
|
+
}
|
|
691
919
|
if (r.status === "mfa_required") return setStep("code");
|
|
692
920
|
f.done();
|
|
693
921
|
};
|
|
694
922
|
const usePasskey = async () => {
|
|
923
|
+
autofill.current?.abort();
|
|
695
924
|
f.setBusy(true);
|
|
696
925
|
f.setError(null);
|
|
697
926
|
const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale });
|
|
698
927
|
if (r.ok && r.status === "redirected") return;
|
|
699
928
|
f.setBusy(false);
|
|
929
|
+
if (!r.ok) setOffer((n) => n + 1);
|
|
700
930
|
if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
|
|
701
931
|
f.done();
|
|
702
932
|
};
|
|
703
933
|
const sendLink = async () => {
|
|
934
|
+
autofill.current?.abort();
|
|
704
935
|
f.setBusy(true);
|
|
705
936
|
f.setError(null);
|
|
706
|
-
const r = await auth.signIn.magicLink({ email });
|
|
937
|
+
const r = await auth.signIn.magicLink({ email, captchaToken: challenge.token });
|
|
707
938
|
f.setBusy(false);
|
|
939
|
+
challenge.renew();
|
|
708
940
|
if (!r.ok) return f.setError(r.error);
|
|
709
941
|
setStep("sent");
|
|
710
942
|
};
|
|
@@ -720,9 +952,11 @@ function SignIn(props) {
|
|
|
720
952
|
] });
|
|
721
953
|
}
|
|
722
954
|
if (step === "code") {
|
|
955
|
+
const to = phoneWay?.phone?.number ?? "";
|
|
723
956
|
return /* @__PURE__ */ jsxs("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
|
|
724
|
-
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.codeHelp }),
|
|
957
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: phoneSent ? f.t.codeSent.replace("{to}", phoneSent) : phoneWay?.phone && !phoneWay.totp ? f.t.codeHelpPhone : f.t.codeHelp }),
|
|
725
958
|
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
959
|
+
phoneWay?.phone && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void sendCode(), children: (phoneWay.phone.channel === "whatsapp" ? f.t.sendCodeWhatsapp : f.t.sendCodeSms).replace("{to}", to) }),
|
|
726
960
|
/* @__PURE__ */ jsx(Field, { f, name: "code", type: "text", label: f.t.code, value: code, autoComplete: "one-time-code", required: true, onChange: setCode }),
|
|
727
961
|
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
|
|
728
962
|
] });
|
|
@@ -736,7 +970,7 @@ function SignIn(props) {
|
|
|
736
970
|
] }, p.id)),
|
|
737
971
|
methods?.passkey && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void usePasskey(), children: f.t.usePasskey }),
|
|
738
972
|
((methods?.providers.length ?? 0) > 0 || methods?.passkey) && /* @__PURE__ */ jsx("p", { className: f.cx("separator"), children: f.t.orWithEmail }),
|
|
739
|
-
/* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
|
|
973
|
+
/* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: methods?.passkeyHere ? "username webauthn" : "username", required: true, onChange: setEmail }),
|
|
740
974
|
/* @__PURE__ */ jsx(
|
|
741
975
|
Field,
|
|
742
976
|
{
|
|
@@ -751,6 +985,7 @@ function SignIn(props) {
|
|
|
751
985
|
aside: props.links?.forgotPassword ? /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.forgotPassword, children: f.t.forgotPassword }) : void 0
|
|
752
986
|
}
|
|
753
987
|
),
|
|
988
|
+
challenge.node,
|
|
754
989
|
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signIn }),
|
|
755
990
|
methods?.magicLink && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void sendLink(), children: f.t.emailMeALink }),
|
|
756
991
|
methods?.signUp !== false && props.links?.signUp && /* @__PURE__ */ jsxs("p", { className: f.cx("footer"), children: [
|
|
@@ -760,6 +995,21 @@ function SignIn(props) {
|
|
|
760
995
|
] })
|
|
761
996
|
] });
|
|
762
997
|
}
|
|
998
|
+
function GoogleOneTap(props) {
|
|
999
|
+
const auth = useAuth();
|
|
1000
|
+
const unauthenticated = auth.status === "unauthenticated";
|
|
1001
|
+
useEffect(() => {
|
|
1002
|
+
if (!unauthenticated) return;
|
|
1003
|
+
const ctl = new AbortController();
|
|
1004
|
+
void auth.signIn.googleOneTap({ signal: ctl.signal, context: props.context }).then((r) => {
|
|
1005
|
+
if (!r.ok || ctl.signal.aborted) return;
|
|
1006
|
+
if (props.onSuccess) props.onSuccess();
|
|
1007
|
+
else if (props.returnTo) window.location.assign(props.returnTo);
|
|
1008
|
+
});
|
|
1009
|
+
return () => ctl.abort();
|
|
1010
|
+
}, [unauthenticated]);
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
763
1013
|
function SignUp(props) {
|
|
764
1014
|
const auth = useAuth();
|
|
765
1015
|
const f = useForm(props);
|
|
@@ -767,12 +1017,14 @@ function SignUp(props) {
|
|
|
767
1017
|
const [email, setEmail] = useState("");
|
|
768
1018
|
const [password, setPassword] = useState("");
|
|
769
1019
|
const [sent, setSent] = useState(false);
|
|
1020
|
+
const challenge = useChallenge(useMethods(auth)?.captchaSiteKey, props.locale);
|
|
770
1021
|
const submit = async (e) => {
|
|
771
1022
|
e.preventDefault();
|
|
772
1023
|
f.setBusy(true);
|
|
773
1024
|
f.setError(null);
|
|
774
|
-
const r = await auth.signUp.email({ name, email, password });
|
|
1025
|
+
const r = await auth.signUp.email({ name, email, password, captchaToken: challenge.token });
|
|
775
1026
|
f.setBusy(false);
|
|
1027
|
+
challenge.renew();
|
|
776
1028
|
if (!r.ok) return f.setError(r.error);
|
|
777
1029
|
if (r.status === "verification_required") return setSent(true);
|
|
778
1030
|
f.done();
|
|
@@ -789,6 +1041,7 @@ function SignUp(props) {
|
|
|
789
1041
|
/* @__PURE__ */ jsx(Field, { f, name: "name", type: "text", label: f.t.name, value: name, autoComplete: "name", onChange: setName }),
|
|
790
1042
|
/* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
|
|
791
1043
|
/* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.password, value: password, autoComplete: "new-password", required: true, onChange: setPassword }),
|
|
1044
|
+
challenge.node,
|
|
792
1045
|
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.signUp }),
|
|
793
1046
|
props.links?.signIn && /* @__PURE__ */ jsxs("p", { className: f.cx("footer"), children: [
|
|
794
1047
|
f.t.haveAccount,
|
|
@@ -802,12 +1055,14 @@ function ForgotPassword(props) {
|
|
|
802
1055
|
const f = useForm(props);
|
|
803
1056
|
const [email, setEmail] = useState("");
|
|
804
1057
|
const [sent, setSent] = useState(false);
|
|
1058
|
+
const challenge = useChallenge(useMethods(auth)?.captchaSiteKey, props.locale);
|
|
805
1059
|
const submit = async (e) => {
|
|
806
1060
|
e.preventDefault();
|
|
807
1061
|
f.setBusy(true);
|
|
808
1062
|
f.setError(null);
|
|
809
|
-
const r = await auth.forgotPassword({ email });
|
|
1063
|
+
const r = await auth.forgotPassword({ email, captchaToken: challenge.token });
|
|
810
1064
|
f.setBusy(false);
|
|
1065
|
+
challenge.renew();
|
|
811
1066
|
if (!r.ok) return f.setError(r.error);
|
|
812
1067
|
setSent(true);
|
|
813
1068
|
};
|
|
@@ -821,6 +1076,7 @@ function ForgotPassword(props) {
|
|
|
821
1076
|
return /* @__PURE__ */ jsxs("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
|
|
822
1077
|
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
823
1078
|
/* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
|
|
1079
|
+
challenge.node,
|
|
824
1080
|
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.sendLink }),
|
|
825
1081
|
props.links?.signIn && /* @__PURE__ */ jsx("p", { className: f.cx("footer"), children: /* @__PURE__ */ jsx("a", { className: f.cx("link"), href: props.links.signIn, children: f.t.backToSignIn }) })
|
|
826
1082
|
] });
|
|
@@ -920,6 +1176,7 @@ function Account(props) {
|
|
|
920
1176
|
account.hasPassword && /* @__PURE__ */ jsx(PasswordPart, { props }),
|
|
921
1177
|
/* @__PURE__ */ jsx(LinkedPart, { props }),
|
|
922
1178
|
/* @__PURE__ */ jsx(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
|
|
1179
|
+
/* @__PURE__ */ jsx(PhonePart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
|
|
923
1180
|
/* @__PURE__ */ jsx(PasskeysPart, { props, account, accountUrl: accountUrl(), when }),
|
|
924
1181
|
/* @__PURE__ */ jsxs("section", { className: cx("section"), children: [
|
|
925
1182
|
/* @__PURE__ */ jsx("h2", { className: cx("heading"), children: t.sessions }),
|
|
@@ -1149,6 +1406,77 @@ function SecondStepPart({ props, account, onChanged }) {
|
|
|
1149
1406
|
account.mfaEnabled && /* @__PURE__ */ jsx(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void renew(), children: f.t.newRecoveryCodes })
|
|
1150
1407
|
] });
|
|
1151
1408
|
}
|
|
1409
|
+
function PhonePart({ props, account, onChanged }) {
|
|
1410
|
+
const auth = useAuth();
|
|
1411
|
+
const { f, run, general } = useSection(props);
|
|
1412
|
+
const [ways, setWays] = useState(null);
|
|
1413
|
+
const [number, setNumber] = useState("");
|
|
1414
|
+
const [channel, setChannel] = useState("sms");
|
|
1415
|
+
const [password, setPassword] = useState("");
|
|
1416
|
+
const [code, setCode] = useState("");
|
|
1417
|
+
const [sentTo, setSentTo] = useState("");
|
|
1418
|
+
const [codes, setCodes] = useState(null);
|
|
1419
|
+
const load = () => auth.account.mfa.factors().then((w) => {
|
|
1420
|
+
setWays(w);
|
|
1421
|
+
if (w && w.phoneChannels.length > 0 && !w.phoneChannels.includes(channel)) setChannel(w.phoneChannels[0]);
|
|
1422
|
+
});
|
|
1423
|
+
useEffect(() => {
|
|
1424
|
+
void load();
|
|
1425
|
+
}, []);
|
|
1426
|
+
if (!ways || ways.phoneChannels.length === 0) return null;
|
|
1427
|
+
const add = async (e) => {
|
|
1428
|
+
e.preventDefault();
|
|
1429
|
+
const r = await run(() => auth.account.mfa.addPhone({ phone: number, channel, password }));
|
|
1430
|
+
if (r.ok) setSentTo(r.sentTo);
|
|
1431
|
+
};
|
|
1432
|
+
const confirm = async (e) => {
|
|
1433
|
+
e.preventDefault();
|
|
1434
|
+
const r = await run(() => auth.account.mfa.verifyPhone({ code }));
|
|
1435
|
+
if (!r.ok) return;
|
|
1436
|
+
setSentTo("");
|
|
1437
|
+
setCode("");
|
|
1438
|
+
setPassword("");
|
|
1439
|
+
if (r.recoveryCodes.length > 0) setCodes(r.recoveryCodes);
|
|
1440
|
+
onChanged(true);
|
|
1441
|
+
await load();
|
|
1442
|
+
};
|
|
1443
|
+
const remove = async (e) => {
|
|
1444
|
+
e.preventDefault();
|
|
1445
|
+
const r = await run(() => auth.account.mfa.removePhone({ password }));
|
|
1446
|
+
if (!r.ok) return;
|
|
1447
|
+
setPassword("");
|
|
1448
|
+
await load();
|
|
1449
|
+
const left = await auth.account.mfa.factors();
|
|
1450
|
+
onChanged(Boolean(left?.totp));
|
|
1451
|
+
};
|
|
1452
|
+
const passwordField = account.hasPassword && /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.currentPassword, value: password, autoComplete: "current-password", required: true, onChange: setPassword });
|
|
1453
|
+
if (sentTo) {
|
|
1454
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("section"), onSubmit: confirm, noValidate: true, children: [
|
|
1455
|
+
/* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.phone }),
|
|
1456
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.codeSent.replace("{to}", sentTo) }),
|
|
1457
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
1458
|
+
/* @__PURE__ */ jsx(Field, { f, name: "code", type: "text", label: f.t.code, value: code, autoComplete: "one-time-code", required: true, onChange: setCode }),
|
|
1459
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.verify })
|
|
1460
|
+
] });
|
|
1461
|
+
}
|
|
1462
|
+
return /* @__PURE__ */ jsxs("form", { className: f.cx("section"), onSubmit: ways.phone ? remove : add, noValidate: true, children: [
|
|
1463
|
+
/* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.phone }),
|
|
1464
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: ways.phone ? f.t.phoneOn.replace("{to}", ways.phone.number) : `${f.t.phoneOff} ${f.t.phoneHelp}` }),
|
|
1465
|
+
codes && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1466
|
+
/* @__PURE__ */ jsx("p", { className: f.cx("notice"), children: f.t.keepCodes }),
|
|
1467
|
+
/* @__PURE__ */ jsx("ul", { className: f.cx("codes"), children: codes.map((c) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("code", { children: c }) }, c)) })
|
|
1468
|
+
] }),
|
|
1469
|
+
general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
1470
|
+
!ways.phone && /* @__PURE__ */ jsx(Field, { f, name: "phone", type: "tel", label: f.t.phone, value: number, autoComplete: "tel", required: true, onChange: setNumber }),
|
|
1471
|
+
!ways.phone && ways.phoneChannels.length > 1 && /* @__PURE__ */ jsx("div", { className: f.cx("field"), role: "radiogroup", children: ways.phoneChannels.map((c) => /* @__PURE__ */ jsxs("label", { className: f.cx("label"), children: [
|
|
1472
|
+
/* @__PURE__ */ jsx("input", { type: "radio", name: "channel", value: c, checked: channel === c, onChange: () => setChannel(c) }),
|
|
1473
|
+
" ",
|
|
1474
|
+
c === "whatsapp" ? f.t.byWhatsapp : f.t.bySms
|
|
1475
|
+
] }, c)) }),
|
|
1476
|
+
passwordField,
|
|
1477
|
+
/* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: ways.phone ? f.t.removePhone : f.t.addPhone })
|
|
1478
|
+
] });
|
|
1479
|
+
}
|
|
1152
1480
|
function SignOutEverywhere({ props }) {
|
|
1153
1481
|
const auth = useAuth();
|
|
1154
1482
|
const { f, run, general } = useSection(props);
|
|
@@ -1251,7 +1579,16 @@ function useAuth() {
|
|
|
1251
1579
|
};
|
|
1252
1580
|
return {
|
|
1253
1581
|
...client,
|
|
1254
|
-
signIn: {
|
|
1582
|
+
signIn: {
|
|
1583
|
+
...client.signIn,
|
|
1584
|
+
email: (f) => then(client.signIn.email(f)),
|
|
1585
|
+
mfa: (f) => then(client.signIn.mfa(f)),
|
|
1586
|
+
magicLinkVerify: (f) => then(client.signIn.magicLinkVerify(f)),
|
|
1587
|
+
emailCode: (f) => then(client.signIn.emailCode(f)),
|
|
1588
|
+
passkey: (o) => then(client.signIn.passkey(o)),
|
|
1589
|
+
passkeyAutofill: (o) => then(client.signIn.passkeyAutofill(o)),
|
|
1590
|
+
googleOneTap: (o) => then(client.signIn.googleOneTap(o))
|
|
1591
|
+
},
|
|
1255
1592
|
signUp: { email: (f) => then(client.signUp.email(f)) },
|
|
1256
1593
|
signOut: () => then(client.signOut()),
|
|
1257
1594
|
account: {
|
|
@@ -1271,11 +1608,13 @@ export {
|
|
|
1271
1608
|
Account,
|
|
1272
1609
|
CauthyProvider,
|
|
1273
1610
|
ForgotPassword,
|
|
1611
|
+
GoogleOneTap,
|
|
1274
1612
|
ResetPassword,
|
|
1275
1613
|
SignIn,
|
|
1276
1614
|
SignUp,
|
|
1277
1615
|
VerifyEmail,
|
|
1278
1616
|
can,
|
|
1617
|
+
canAutofillPasskeys,
|
|
1279
1618
|
canUsePasskeys,
|
|
1280
1619
|
createAuthClient,
|
|
1281
1620
|
definePolicy,
|