@authagonal/login 0.3.8 → 0.3.10

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/api.d.ts CHANGED
@@ -10,10 +10,10 @@ export declare function register(email: string, password: string, firstName?: st
10
10
  export declare function logout(): Promise<{
11
11
  success: true;
12
12
  }>;
13
- export declare function forgotPassword(email: string): Promise<{
13
+ export declare function forgotPassword(email: string, turnstileToken?: string): Promise<{
14
14
  success: true;
15
15
  }>;
16
- export declare function resetPassword(token: string, newPassword: string): Promise<{
16
+ export declare function resetPassword(token: string, newPassword: string, turnstileToken?: string): Promise<{
17
17
  success: true;
18
18
  }>;
19
19
  export declare function getSession(): Promise<SessionResponse>;
package/dist/index.js CHANGED
@@ -3523,6 +3523,7 @@ var Er = {
3523
3523
  errorEmailRequired: "Email is required",
3524
3524
  errorPasswordRequired: "Password is required",
3525
3525
  errorUnexpected: "An unexpected error occurred. Please try again.",
3526
+ captchaFailed: "Verification failed. Please try again.",
3526
3527
  resetYourPassword: "Reset your password",
3527
3528
  resetSubtitle: "Enter your email address and we'll send you a link to reset your password.",
3528
3529
  sending: "Sending...",
@@ -3656,6 +3657,7 @@ var Er = {
3656
3657
  errorEmailRequired: "电子邮件为必填项",
3657
3658
  errorPasswordRequired: "密码为必填项",
3658
3659
  errorUnexpected: "发生意外错误,请重试。",
3660
+ captchaFailed: "验证失败,请重试。",
3659
3661
  resetYourPassword: "重置密码",
3660
3662
  resetSubtitle: "输入您的电子邮件地址,我们将向您发送重置密码的链接。",
3661
3663
  sending: "正在发送...",
@@ -3766,6 +3768,7 @@ var Er = {
3766
3768
  errorEmailRequired: "E-Mail ist erforderlich",
3767
3769
  errorPasswordRequired: "Passwort ist erforderlich",
3768
3770
  errorUnexpected: "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
3771
+ captchaFailed: "Verifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
3769
3772
  resetYourPassword: "Passwort zurücksetzen",
3770
3773
  resetSubtitle: "Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen einen Link zum Zurücksetzen Ihres Passworts.",
3771
3774
  sending: "Wird gesendet...",
@@ -3876,6 +3879,7 @@ var Er = {
3876
3879
  errorEmailRequired: "L'e-mail est requis",
3877
3880
  errorPasswordRequired: "Le mot de passe est requis",
3878
3881
  errorUnexpected: "Une erreur inattendue s'est produite. Veuillez réessayer.",
3882
+ captchaFailed: "Échec de la vérification. Veuillez réessayer.",
3879
3883
  resetYourPassword: "Réinitialiser votre mot de passe",
3880
3884
  resetSubtitle: "Entrez votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe.",
3881
3885
  sending: "Envoi en cours...",
@@ -3986,6 +3990,7 @@ var Er = {
3986
3990
  errorEmailRequired: "El correo electrónico es obligatorio",
3987
3991
  errorPasswordRequired: "La contraseña es obligatoria",
3988
3992
  errorUnexpected: "Se produjo un error inesperado. Por favor, inténtelo de nuevo.",
3993
+ captchaFailed: "Error de verificación. Inténtalo de nuevo.",
3989
3994
  resetYourPassword: "Restablecer su contraseña",
3990
3995
  resetSubtitle: "Ingrese su dirección de correo electrónico y le enviaremos un enlace para restablecer su contraseña.",
3991
3996
  sending: "Enviando...",
@@ -4096,6 +4101,7 @@ var Er = {
4096
4101
  errorEmailRequired: "Email là bắt buộc",
4097
4102
  errorPasswordRequired: "Mật khẩu là bắt buộc",
4098
4103
  errorUnexpected: "Đã xảy ra lỗi không mong muốn. Vui lòng thử lại.",
4104
+ captchaFailed: "Xác minh không thành công. Vui lòng thử lại.",
4099
4105
  resetYourPassword: "Đặt lại mật khẩu",
4100
4106
  resetSubtitle: "Nhập địa chỉ email của bạn và chúng tôi sẽ gửi cho bạn liên kết để đặt lại mật khẩu.",
4101
4107
  sending: "Đang gửi...",
@@ -4206,6 +4212,7 @@ var Er = {
4206
4212
  errorEmailRequired: "O e-mail é obrigatório",
4207
4213
  errorPasswordRequired: "A senha é obrigatória",
4208
4214
  errorUnexpected: "Ocorreu um erro inesperado. Tente novamente.",
4215
+ captchaFailed: "Falha na verificação. Tente novamente.",
4209
4216
  resetYourPassword: "Redefinir a sua senha",
4210
4217
  resetSubtitle: "Introduza o seu endereço de e-mail e enviaremos um link para redefinir a sua senha.",
4211
4218
  sending: "A enviar...",
@@ -4316,6 +4323,7 @@ var Er = {
4316
4323
  errorEmailRequired: "QIn nab nIteb nISlu'",
4317
4324
  errorPasswordRequired: "mu'wIj pegh nIteb nISlu'",
4318
4325
  errorUnexpected: "Qagh qaS. yInIDqa'.",
4326
+ captchaFailed: "ngu' luj. yInIDqa'.",
4319
4327
  resetYourPassword: "mu'wIj pegh yIchoH",
4320
4328
  resetSubtitle: "QIn nab yIngu'. mu'wIj pegh choHmeH lIngwI' DangeH.",
4321
4329
  sending: "ngeHmeH...",
@@ -4724,18 +4732,22 @@ function Yr(e, t, n, r, i) {
4724
4732
  function Xr() {
4725
4733
  return $("/api/auth/logout", { method: "POST" });
4726
4734
  }
4727
- function Zr(e) {
4735
+ function Zr(e, t) {
4728
4736
  return $("/api/auth/forgot-password", {
4729
4737
  method: "POST",
4730
- body: JSON.stringify({ email: e })
4738
+ body: JSON.stringify({
4739
+ email: e,
4740
+ turnstileToken: t
4741
+ })
4731
4742
  });
4732
4743
  }
4733
- function Qr(e, t) {
4744
+ function Qr(e, t, n) {
4734
4745
  return $("/api/auth/reset-password", {
4735
4746
  method: "POST",
4736
4747
  body: JSON.stringify({
4737
4748
  token: e,
4738
- newPassword: t
4749
+ newPassword: t,
4750
+ turnstileToken: n
4739
4751
  })
4740
4752
  });
4741
4753
  }
@@ -4964,7 +4976,7 @@ function _i() {
4964
4976
  C(e("errorPasswordRequired"));
4965
4977
  break;
4966
4978
  case "captcha_failed":
4967
- C(e("errorUnexpected"));
4979
+ C(e("captchaFailed"));
4968
4980
  break;
4969
4981
  default: C(t.message || e("errorUnexpected"));
4970
4982
  }
@@ -5204,7 +5216,7 @@ function vi() {
5204
5216
  b(e("errorEmailAndPasswordRequired"));
5205
5217
  break;
5206
5218
  case "captcha_failed":
5207
- b(e("errorRegistrationFailed"));
5219
+ b(e("captchaFailed"));
5208
5220
  break;
5209
5221
  default: b(t.message || e("errorRegistrationFailed"));
5210
5222
  }
@@ -5326,25 +5338,29 @@ function vi() {
5326
5338
  //#endregion
5327
5339
  //#region src/pages/ForgotPasswordPage.tsx
5328
5340
  function yi() {
5329
- let { t: e } = L(), [t] = _(), n = t.get("returnUrl") || "", [r, i] = s(""), [a, o] = s(!1), [c, d] = s(!1), [p, m] = s(""), h = n ? `/login?returnUrl=${encodeURIComponent(n)}` : "/login";
5330
- async function g(t) {
5331
- t.preventDefault(), m(""), o(!0);
5341
+ let { t: e } = L(), [t] = _(), n = t.get("returnUrl") || "", [r, i] = s(""), [o, c] = s(!1), [d, p] = s(!1), [m, h] = s(""), [g, v] = s(void 0), [y, b] = s(null), [x, S] = s(0);
5342
+ a(() => {
5343
+ ti().then((e) => v(e.turnstileSiteKey)).catch(() => {});
5344
+ }, []);
5345
+ let C = n ? `/login?returnUrl=${encodeURIComponent(n)}` : "/login";
5346
+ async function w(t) {
5347
+ t.preventDefault(), h(""), c(!0);
5332
5348
  try {
5333
- await Zr(r), d(!0);
5334
- } catch {
5335
- m(e("errorUnexpected"));
5349
+ await Zr(r, y || void 0), p(!0);
5350
+ } catch (t) {
5351
+ t instanceof Kr && t.error === "captcha_failed" ? h(e("captchaFailed")) : h(e("errorUnexpected")), g && (b(null), S((e) => e + 1));
5336
5352
  } finally {
5337
- o(!1);
5353
+ c(!1);
5338
5354
  }
5339
5355
  }
5340
- return c ? /* @__PURE__ */ u("div", { children: [
5356
+ return d ? /* @__PURE__ */ u("div", { children: [
5341
5357
  /* @__PURE__ */ l(G, { children: e("checkYourEmail") }),
5342
5358
  /* @__PURE__ */ l(Q, {
5343
5359
  variant: "success",
5344
5360
  children: e("resetEmailSent")
5345
5361
  }),
5346
5362
  /* @__PURE__ */ l(K, { children: /* @__PURE__ */ l(f, {
5347
- to: h,
5363
+ to: C,
5348
5364
  className: "text-sm font-medium text-primary hover:underline no-underline",
5349
5365
  children: e("backToSignIn")
5350
5366
  }) })
@@ -5354,12 +5370,12 @@ function yi() {
5354
5370
  className: "mb-5",
5355
5371
  children: e("resetSubtitle")
5356
5372
  }),
5357
- p && /* @__PURE__ */ l(Q, {
5373
+ m && /* @__PURE__ */ l(Q, {
5358
5374
  variant: "error",
5359
- children: p
5375
+ children: m
5360
5376
  }),
5361
5377
  /* @__PURE__ */ u("form", {
5362
- onSubmit: g,
5378
+ onSubmit: w,
5363
5379
  children: [
5364
5380
  /* @__PURE__ */ u("div", {
5365
5381
  className: "mb-4",
@@ -5378,13 +5394,21 @@ function yi() {
5378
5394
  required: !0
5379
5395
  })]
5380
5396
  }),
5397
+ g && /* @__PURE__ */ l("div", {
5398
+ className: "mb-4",
5399
+ children: /* @__PURE__ */ l(mi, {
5400
+ siteKey: g,
5401
+ onToken: b
5402
+ }, x)
5403
+ }),
5381
5404
  /* @__PURE__ */ l(Y, {
5382
5405
  type: "submit",
5383
- loading: a,
5384
- children: e(a ? "sending" : "sendResetLink")
5406
+ loading: o,
5407
+ disabled: !!g && !y,
5408
+ children: e(o ? "sending" : "sendResetLink")
5385
5409
  }),
5386
5410
  /* @__PURE__ */ l(K, { children: /* @__PURE__ */ l(f, {
5387
- to: h,
5411
+ to: C,
5388
5412
  className: "text-sm font-medium text-primary hover:underline no-underline",
5389
5413
  children: e("backToSignIn")
5390
5414
  }) })
@@ -5449,13 +5473,15 @@ function Si(e, t) {
5449
5473
  });
5450
5474
  }
5451
5475
  function Ci() {
5452
- let { t: e } = L(), [t] = _(), n = t.get("p") || "", [r, i] = s(""), [o, c] = s(""), [d, p] = s(!1), [m, h] = s(""), [g, v] = s(!1), [y, b] = s(""), [x, S] = s(xi);
5476
+ let { t: e } = L(), [t] = _(), n = t.get("p") || "", [r, i] = s(""), [o, c] = s(""), [d, p] = s(!1), [m, h] = s(""), [g, v] = s(!1), [y, b] = s(""), [x, S] = s(xi), [C, w] = s(void 0), [T, E] = s(null), [D, O] = s(0);
5453
5477
  a(() => {
5454
5478
  fetch(`${bi}/api/auth/password-policy`).then((e) => e.ok ? e.json() : null).then((e) => {
5455
5479
  e?.rules && S(e.rules);
5456
5480
  }).catch(() => {});
5481
+ }, []), a(() => {
5482
+ ti().then((e) => w(e.turnstileSiteKey)).catch(() => {});
5457
5483
  }, []);
5458
- function C(t) {
5484
+ function k(t) {
5459
5485
  switch (t.rule) {
5460
5486
  case "minLength": return e("ruleMinLength", { count: t.value ?? 8 });
5461
5487
  case "uppercase": return e("ruleUppercase");
@@ -5465,12 +5491,12 @@ function Ci() {
5465
5491
  default: return t.label;
5466
5492
  }
5467
5493
  }
5468
- let w = Si(r, x.map((e) => ({
5494
+ let A = Si(r, x.map((e) => ({
5469
5495
  ...e,
5470
- label: C(e)
5471
- }))), T = w.every((e) => e.met);
5472
- async function E(t) {
5473
- if (t.preventDefault(), h(""), b(""), !T) {
5496
+ label: k(e)
5497
+ }))), j = A.every((e) => e.met);
5498
+ async function M(t) {
5499
+ if (t.preventDefault(), h(""), b(""), !j) {
5474
5500
  b(e("passwordNotMeetRequirements"));
5475
5501
  return;
5476
5502
  }
@@ -5480,7 +5506,7 @@ function Ci() {
5480
5506
  }
5481
5507
  p(!0);
5482
5508
  try {
5483
- await Qr(n, r), v(!0);
5509
+ await Qr(n, r, T || void 0), v(!0);
5484
5510
  } catch (t) {
5485
5511
  if (t instanceof Kr) switch (t.error) {
5486
5512
  case "weak_password":
@@ -5493,9 +5519,13 @@ function Ci() {
5493
5519
  case "password_required":
5494
5520
  h(e("errorPasswordRequired"));
5495
5521
  break;
5522
+ case "captcha_failed":
5523
+ h(e("captchaFailed"));
5524
+ break;
5496
5525
  default: h(t.message || e("errorUnexpected"));
5497
5526
  }
5498
5527
  else h(e("errorUnexpected"));
5528
+ C && (E(null), O((e) => e + 1));
5499
5529
  } finally {
5500
5530
  p(!1);
5501
5531
  }
@@ -5522,7 +5552,7 @@ function Ci() {
5522
5552
  children: y
5523
5553
  }),
5524
5554
  /* @__PURE__ */ u("form", {
5525
- onSubmit: E,
5555
+ onSubmit: M,
5526
5556
  children: [
5527
5557
  /* @__PURE__ */ u("div", {
5528
5558
  className: "mb-4",
@@ -5543,7 +5573,7 @@ function Ci() {
5543
5573
  }),
5544
5574
  r.length > 0 && /* @__PURE__ */ l("ul", {
5545
5575
  className: "list-none mb-4 p-3 bg-gray-50 dark:bg-gray-800/60 rounded-md",
5546
- children: w.map((e) => /* @__PURE__ */ u("li", {
5576
+ children: A.map((e) => /* @__PURE__ */ u("li", {
5547
5577
  className: `text-[13px] py-0.5 flex items-center gap-1.5 ${e.met ? "text-green-800 dark:text-green-400" : "text-red-800 dark:text-red-400"}`,
5548
5578
  children: [e.met ? /* @__PURE__ */ l(Xt, { className: "h-3.5 w-3.5 shrink-0" }) : /* @__PURE__ */ l(tn, { className: "h-3.5 w-3.5 shrink-0" }), e.label]
5549
5579
  }, e.label))
@@ -5564,10 +5594,17 @@ function Ci() {
5564
5594
  required: !0
5565
5595
  })]
5566
5596
  }),
5597
+ C && /* @__PURE__ */ l("div", {
5598
+ className: "mb-4",
5599
+ children: /* @__PURE__ */ l(mi, {
5600
+ siteKey: C,
5601
+ onToken: E
5602
+ }, D)
5603
+ }),
5567
5604
  /* @__PURE__ */ l(Y, {
5568
5605
  type: "submit",
5569
5606
  loading: d,
5570
- disabled: !T,
5607
+ disabled: !j || !!C && !T,
5571
5608
  children: e(d ? "resetting" : "resetPassword")
5572
5609
  }),
5573
5610
  /* @__PURE__ */ l(K, { children: /* @__PURE__ */ l(f, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authagonal/login",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Default login UI for Authagonal — runtime-configurable via branding.json",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/api.ts CHANGED
@@ -66,17 +66,17 @@ export function logout(): Promise<{ success: true }> {
66
66
  });
67
67
  }
68
68
 
69
- export function forgotPassword(email: string): Promise<{ success: true }> {
69
+ export function forgotPassword(email: string, turnstileToken?: string): Promise<{ success: true }> {
70
70
  return api<{ success: true }>('/api/auth/forgot-password', {
71
71
  method: 'POST',
72
- body: JSON.stringify({ email }),
72
+ body: JSON.stringify({ email, turnstileToken }),
73
73
  });
74
74
  }
75
75
 
76
- export function resetPassword(token: string, newPassword: string): Promise<{ success: true }> {
76
+ export function resetPassword(token: string, newPassword: string, turnstileToken?: string): Promise<{ success: true }> {
77
77
  return api<{ success: true }>('/api/auth/reset-password', {
78
78
  method: 'POST',
79
- body: JSON.stringify({ token, newPassword }),
79
+ body: JSON.stringify({ token, newPassword, turnstileToken }),
80
80
  });
81
81
  }
82
82
 
package/src/i18n/de.json CHANGED
@@ -16,6 +16,7 @@
16
16
  "errorEmailRequired": "E-Mail ist erforderlich",
17
17
  "errorPasswordRequired": "Passwort ist erforderlich",
18
18
  "errorUnexpected": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
19
+ "captchaFailed": "Verifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
19
20
  "resetYourPassword": "Passwort zurücksetzen",
20
21
  "resetSubtitle": "Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen einen Link zum Zurücksetzen Ihres Passworts.",
21
22
  "sending": "Wird gesendet...",
package/src/i18n/en.json CHANGED
@@ -16,6 +16,7 @@
16
16
  "errorEmailRequired": "Email is required",
17
17
  "errorPasswordRequired": "Password is required",
18
18
  "errorUnexpected": "An unexpected error occurred. Please try again.",
19
+ "captchaFailed": "Verification failed. Please try again.",
19
20
  "resetYourPassword": "Reset your password",
20
21
  "resetSubtitle": "Enter your email address and we'll send you a link to reset your password.",
21
22
  "sending": "Sending...",
package/src/i18n/es.json CHANGED
@@ -16,6 +16,7 @@
16
16
  "errorEmailRequired": "El correo electrónico es obligatorio",
17
17
  "errorPasswordRequired": "La contraseña es obligatoria",
18
18
  "errorUnexpected": "Se produjo un error inesperado. Por favor, inténtelo de nuevo.",
19
+ "captchaFailed": "Error de verificación. Inténtalo de nuevo.",
19
20
  "resetYourPassword": "Restablecer su contraseña",
20
21
  "resetSubtitle": "Ingrese su dirección de correo electrónico y le enviaremos un enlace para restablecer su contraseña.",
21
22
  "sending": "Enviando...",
package/src/i18n/fr.json CHANGED
@@ -16,6 +16,7 @@
16
16
  "errorEmailRequired": "L'e-mail est requis",
17
17
  "errorPasswordRequired": "Le mot de passe est requis",
18
18
  "errorUnexpected": "Une erreur inattendue s'est produite. Veuillez réessayer.",
19
+ "captchaFailed": "Échec de la vérification. Veuillez réessayer.",
19
20
  "resetYourPassword": "Réinitialiser votre mot de passe",
20
21
  "resetSubtitle": "Entrez votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe.",
21
22
  "sending": "Envoi en cours...",
package/src/i18n/pt.json CHANGED
@@ -16,6 +16,7 @@
16
16
  "errorEmailRequired": "O e-mail é obrigatório",
17
17
  "errorPasswordRequired": "A senha é obrigatória",
18
18
  "errorUnexpected": "Ocorreu um erro inesperado. Tente novamente.",
19
+ "captchaFailed": "Falha na verificação. Tente novamente.",
19
20
  "resetYourPassword": "Redefinir a sua senha",
20
21
  "resetSubtitle": "Introduza o seu endereço de e-mail e enviaremos um link para redefinir a sua senha.",
21
22
  "sending": "A enviar...",
package/src/i18n/tlh.json CHANGED
@@ -16,6 +16,7 @@
16
16
  "errorEmailRequired": "QIn nab nIteb nISlu'",
17
17
  "errorPasswordRequired": "mu'wIj pegh nIteb nISlu'",
18
18
  "errorUnexpected": "Qagh qaS. yInIDqa'.",
19
+ "captchaFailed": "ngu' luj. yInIDqa'.",
19
20
  "resetYourPassword": "mu'wIj pegh yIchoH",
20
21
  "resetSubtitle": "QIn nab yIngu'. mu'wIj pegh choHmeH lIngwI' DangeH.",
21
22
  "sending": "ngeHmeH...",
package/src/i18n/vi.json CHANGED
@@ -16,6 +16,7 @@
16
16
  "errorEmailRequired": "Email là bắt buộc",
17
17
  "errorPasswordRequired": "Mật khẩu là bắt buộc",
18
18
  "errorUnexpected": "Đã xảy ra lỗi không mong muốn. Vui lòng thử lại.",
19
+ "captchaFailed": "Xác minh không thành công. Vui lòng thử lại.",
19
20
  "resetYourPassword": "Đặt lại mật khẩu",
20
21
  "resetSubtitle": "Nhập địa chỉ email của bạn và chúng tôi sẽ gửi cho bạn liên kết để đặt lại mật khẩu.",
21
22
  "sending": "Đang gửi...",
@@ -16,6 +16,7 @@
16
16
  "errorEmailRequired": "电子邮件为必填项",
17
17
  "errorPasswordRequired": "密码为必填项",
18
18
  "errorUnexpected": "发生意外错误,请重试。",
19
+ "captchaFailed": "验证失败,请重试。",
19
20
  "resetYourPassword": "重置密码",
20
21
  "resetSubtitle": "输入您的电子邮件地址,我们将向您发送重置密码的链接。",
21
22
  "sending": "正在发送...",
@@ -1,7 +1,8 @@
1
- import { useState } from 'react';
1
+ import { useState, useEffect } from 'react';
2
2
  import { useSearchParams, Link } from 'react-router-dom';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { forgotPassword } from '../api';
4
+ import { forgotPassword, getProviders, ApiRequestError } from '../api';
5
+ import { Turnstile } from '../components/Turnstile';
5
6
  import { Button } from '@/components/ui/button';
6
7
  import { Input } from '@/components/ui/input';
7
8
  import { Label } from '@/components/ui/label';
@@ -17,6 +18,16 @@ export default function ForgotPasswordPage() {
17
18
  const [loading, setLoading] = useState(false);
18
19
  const [submitted, setSubmitted] = useState(false);
19
20
  const [error, setError] = useState('');
21
+ const [turnstileSiteKey, setTurnstileSiteKey] = useState<string | undefined>(undefined);
22
+ const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
23
+ const [turnstileKey, setTurnstileKey] = useState(0); // bump to re-mount the widget for a fresh challenge
24
+
25
+ // Surface the Turnstile site key (opt-in; empty when not configured for the tenant).
26
+ useEffect(() => {
27
+ getProviders()
28
+ .then((res) => setTurnstileSiteKey(res.turnstileSiteKey))
29
+ .catch(() => {});
30
+ }, []);
20
31
 
21
32
  const loginLink = returnUrl
22
33
  ? `/login?returnUrl=${encodeURIComponent(returnUrl)}`
@@ -28,11 +39,21 @@ export default function ForgotPasswordPage() {
28
39
  setLoading(true);
29
40
 
30
41
  try {
31
- await forgotPassword(email);
42
+ await forgotPassword(email, turnstileToken || undefined);
32
43
  setSubmitted(true);
33
- } catch {
34
- // The API always returns 200 for anti-enumeration, but handle errors just in case
35
- setError(t('errorUnexpected'));
44
+ } catch (err) {
45
+ // The API always returns 200 for anti-enumeration; the only expected error is a
46
+ // failed captcha (handled explicitly), otherwise a generic message.
47
+ if (err instanceof ApiRequestError && err.error === 'captcha_failed') {
48
+ setError(t('captchaFailed'));
49
+ } else {
50
+ setError(t('errorUnexpected'));
51
+ }
52
+ // Turnstile tokens are single-use — reset so a retry gets a fresh challenge.
53
+ if (turnstileSiteKey) {
54
+ setTurnstileToken(null);
55
+ setTurnstileKey((k) => k + 1);
56
+ }
36
57
  } finally {
37
58
  setLoading(false);
38
59
  }
@@ -75,7 +96,13 @@ export default function ForgotPasswordPage() {
75
96
  />
76
97
  </div>
77
98
 
78
- <Button type="submit" loading={loading}>
99
+ {turnstileSiteKey && (
100
+ <div className="mb-4">
101
+ <Turnstile key={turnstileKey} siteKey={turnstileSiteKey} onToken={setTurnstileToken} />
102
+ </div>
103
+ )}
104
+
105
+ <Button type="submit" loading={loading} disabled={!!turnstileSiteKey && !turnstileToken}>
79
106
  {loading ? t('sending') : t('sendResetLink')}
80
107
  </Button>
81
108
 
@@ -224,7 +224,7 @@ export default function LoginPage() {
224
224
  setError(t('errorPasswordRequired'));
225
225
  break;
226
226
  case 'captcha_failed':
227
- setError(t('errorUnexpected'));
227
+ setError(t('captchaFailed'));
228
228
  break;
229
229
  default:
230
230
  setError(err.message || t('errorUnexpected'));
@@ -70,7 +70,7 @@ export default function RegisterPage() {
70
70
  setError(t('errorEmailAndPasswordRequired'));
71
71
  break;
72
72
  case 'captcha_failed':
73
- setError(t('errorRegistrationFailed'));
73
+ setError(t('captchaFailed'));
74
74
  break;
75
75
  default:
76
76
  setError(err.message || t('errorRegistrationFailed'));
@@ -1,7 +1,8 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import { useSearchParams, Link } from 'react-router-dom';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { resetPassword, ApiRequestError } from '../api';
4
+ import { resetPassword, getProviders, ApiRequestError } from '../api';
5
+ import { Turnstile } from '../components/Turnstile';
5
6
  import { Button } from '@/components/ui/button';
6
7
  import { Input } from '@/components/ui/input';
7
8
  import { Label } from '@/components/ui/label';
@@ -57,6 +58,9 @@ export default function ResetPasswordPage() {
57
58
  const [success, setSuccess] = useState(false);
58
59
  const [validationError, setValidationError] = useState('');
59
60
  const [rules, setRules] = useState<PasswordRule[]>(defaultRules);
61
+ const [turnstileSiteKey, setTurnstileSiteKey] = useState<string | undefined>(undefined);
62
+ const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
63
+ const [turnstileKey, setTurnstileKey] = useState(0); // bump to re-mount the widget for a fresh challenge
60
64
 
61
65
  useEffect(() => {
62
66
  fetch(`${API_URL}/api/auth/password-policy`)
@@ -65,6 +69,13 @@ export default function ResetPasswordPage() {
65
69
  .catch(() => { /* use defaults */ });
66
70
  }, []);
67
71
 
72
+ // Surface the Turnstile site key (opt-in; empty when not configured for the tenant).
73
+ useEffect(() => {
74
+ getProviders()
75
+ .then((res) => setTurnstileSiteKey(res.turnstileSiteKey))
76
+ .catch(() => {});
77
+ }, []);
78
+
68
79
  function getRuleLabel(rule: PasswordRule): string {
69
80
  switch (rule.rule) {
70
81
  case 'minLength': return t('ruleMinLength', { count: rule.value ?? 8 });
@@ -102,7 +113,7 @@ export default function ResetPasswordPage() {
102
113
  setLoading(true);
103
114
 
104
115
  try {
105
- await resetPassword(token, newPassword);
116
+ await resetPassword(token, newPassword, turnstileToken || undefined);
106
117
  setSuccess(true);
107
118
  } catch (err) {
108
119
  if (err instanceof ApiRequestError) {
@@ -117,12 +128,20 @@ export default function ResetPasswordPage() {
117
128
  case 'password_required':
118
129
  setError(t('errorPasswordRequired'));
119
130
  break;
131
+ case 'captcha_failed':
132
+ setError(t('captchaFailed'));
133
+ break;
120
134
  default:
121
135
  setError(err.message || t('errorUnexpected'));
122
136
  }
123
137
  } else {
124
138
  setError(t('errorUnexpected'));
125
139
  }
140
+ // Turnstile tokens are single-use — reset so a retry gets a fresh challenge.
141
+ if (turnstileSiteKey) {
142
+ setTurnstileToken(null);
143
+ setTurnstileKey((k) => k + 1);
144
+ }
126
145
  } finally {
127
146
  setLoading(false);
128
147
  }
@@ -204,7 +223,13 @@ export default function ResetPasswordPage() {
204
223
  />
205
224
  </div>
206
225
 
207
- <Button type="submit" loading={loading} disabled={!allRequirementsMet}>
226
+ {turnstileSiteKey && (
227
+ <div className="mb-4">
228
+ <Turnstile key={turnstileKey} siteKey={turnstileSiteKey} onToken={setTurnstileToken} />
229
+ </div>
230
+ )}
231
+
232
+ <Button type="submit" loading={loading} disabled={!allRequirementsMet || (!!turnstileSiteKey && !turnstileToken)}>
208
233
  {loading ? t('resetting') : t('resetPassword')}
209
234
  </Button>
210
235