Skip to content

Commit 7b10a30

Browse files
fix(auth): the forgot-password page says a reset link can't be emailed on a deployment that sends no mail (#30366) (#30406)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgFVwD1YUYrxV6tnaLG5nxgi)_ domain:ui#3 · os-bill · session_01Utp6CbPN5UYMnBaNJPJWfG Fixes #30366 Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - 改了什么:忘记密码页在服务端报告 `features.emailDelivery: false`(没接邮件服务或只用 log 传输)时,提交后不再说「已发送重置链接」,改为「此处无法通过邮件发送重置链接,请联系管理员在 设置 → 用户 中为你设置新密码」,十种语言齐全。 - 为什么改:不发邮件的部署上,用户会一直等一封永远不来的邮件;服务端一半(#30107,PR #30365)已经提供这个开关,这里是界面一半。 - 风险与回滚:只有明确的 `false` 才换文案;老服务端不带这个字段、或取配置失败,保持现在的文案。新文案不含地址、与提交结果无关,不会泄露账号是否存在。回滚即撤销本 PR。 - 先看:`ui/packages/auth/src/ForgotPasswordForm.tsx`(成功页分支)、`ui/packages/auth/src/__tests__/forgot-password-success-message.test.tsx`(新 describe 块)、`ui/apps/console/src/pages/auth/ForgotPasswordPage.tsx`(两行 label 传递)。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/auth/src/__tests__/forgot-password-success-message.test.tsx`,预期 10/10 通过。 - 需要你做的:无,席位落地。 ## What changes - `ForgotPasswordForm` reads `features.emailDelivery` from the `/auth/config` answer it already fetches (alongside `phoneNumberOtp`). On an explicit `false` the success screen shows `noDeliveryTitle` / `noDeliveryDescription` — "A reset link can't be emailed here" / "This deployment doesn't send email, so no password reset link can reach you. Ask an administrator: they can set a new password for you from Setup → Users." — with an amber icon instead of the green one. The submit still goes to the server, so a log-transport deployment keeps writing the link to its console. - `ForgotPasswordFormLabels` gains `noDeliveryTitle` and `noDeliveryDescription`; `AuthPublicConfig.features` gains `emailDelivery` (the server half's `AuthFeaturesConfigSchema.emailDelivery`). - New strings `auth.forgotPassword.noDeliveryTitle` and `auth.forgotPassword.noDeliveryDescription` in all ten locale packs, passed through by the Console's forgot-password page (label wiring only, per the claim's file-surface ruling) and by `DefaultForgotPasswordPage` in `@object-ui/app-shell`. - Changeset: `@object-ui/auth`, `@object-ui/i18n`, `@object-ui/app-shell`, `@object-ui/console`, all `patch` (Metadata-protocol none). ## Why On a deployment with no mail transport, every reset request ended on "Check your email — we've sent a password reset link to …", and nothing arrived. The server half (#30107, PR #30365) now says whether mail leaves the deployment; this is the page reading it. **An absent key keeps today's text.** The spec text for the flag says absent means not live, but on the wire an absent key means a server older than the flag, and most of those do send mail; telling their users to find an administrator would be the larger harm. A failed config fetch is the same unknown. So only `emailDelivery === false` swaps the screen, and both unknowns are pinned to keep the "check your email" text. **No account-existence oracle.** The new text names no address and is chosen by the deployment flag alone, never by the reset request's answer (which is the same for known and unknown addresses server-side). A test submits a known and an unknown address on a no-mail deployment and asserts the two screens are byte-identical and carry no address. ## Tests - `forgot-password-success-message.test.tsx` — new top-level describe (objectstack#30366) in the existing harness file: no-mail screen text and no address; known vs unknown address read identically; caller-supplied labels used; `emailDelivery: true`, absent key, and failed fetch each keep the old screen. <details><summary>Local runs (head 74302c6 after merging origin/main; the merge brought no ui/ paths)</summary> - `pnpm --dir ui exec vitest run packages/auth/src/__tests__/forgot-password-success-message.test.tsx …/identifier-trim.test.tsx …/phoneTouchTargets-29080.test.tsx` — Test Files 3 passed, Tests 14 passed. - `pnpm --dir ui exec vitest run packages/i18n/` — Test Files 95 passed, Tests 1443 passed / 13 skipped (key parity included). - `pnpm exec turbo run type-check --filter=@object-ui/auth --filter=@object-ui/app-shell --filter=@object-ui/console --concurrency=2` — Tasks 76 successful, 76 total. - eslint on the changed ts/tsx files — exit 0. `pnpm check:new-line-citations` (ui) — 0 new citations. - Ablation via `scripts/ablation-replace.mjs`: `setEmailDeliveryOff(config?.features?.emailDelivery === false);` replaced by `setEmailDeliveryOff(false);` — 3 of the new tests fail (no-mail text, known/unknown parity, translated labels), 7 pass; restore verified blob == HEAD and `git diff HEAD` empty. (The three keep-old-text cases pass under the mutation by design.) </details> ## Verification <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 35 commands, all run, each exit 0, including `node scripts/check-sdui-manifest.mjs` and `pnpm --dir ui exec vitest run scripts/` (170 files passed). `--ran` reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN; `pnpm check:changeset-fixed` also run (outside the derivation), exit 0. CI type-check lanes are left to CI. </details> ## Acceptance notes - The server half PR #30365 is open at this head. The UI only reads an optional key, so this compiles and runs against today's main (where the key is never sent, and the old text shows). origin/main gets merged again once #30365 lands, before acceptance. - File surface widened by the claim's ruling A: the two label-passing pages, label lines only. `ui/packages/auth/src/types.ts` (the `emailDelivery` declaration on `AuthPublicConfig.features`) is outside the listed surface: without it the read does not type-check. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (eslint on changed files; full run is CI's) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6CYv3DdXJ81i2yQpGKm --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CYv3DdXJ81i2yQpGKm)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent f7afab8 commit 7b10a30

16 files changed

Lines changed: 184 additions & 8 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/auth': patch
3+
'@object-ui/i18n': patch
4+
'@object-ui/app-shell': patch
5+
'@object-ui/console': patch
6+
---
7+
8+
fix(auth): the forgot-password page no longer says a reset link was sent on a deployment that sends no mail (#30366)
9+
10+
Clause-②: yes (widening)
11+
Metadata-protocol: none
12+
13+
`GET /api/v1/auth/config` reports `features.emailDelivery`: `false` when no email service is wired or the stock `log` transport only writes mail to the server console. On such a deployment the forgot-password form used to answer every request with "Check your email — we've sent a password reset link", although no mail leaves. When the server reports `emailDelivery: false`, the success screen now says a reset link can't be emailed here and to ask an administrator, who can set a new password from Setup → Users.
14+
15+
The text describes the deployment, never the address: it carries no address, and a known and an unknown address read the same, so the page adds no account-existence oracle. Only an explicit `false` changes it; a server that predates the flag (no `emailDelivery` key) or a failed config fetch keeps the "check your email" screen, since those deployments may well send mail.
16+
17+
`ForgotPasswordFormLabels` gains `noDeliveryTitle` and `noDeliveryDescription`, and `AuthPublicConfig.features` gains `emailDelivery`. New strings `auth.forgotPassword.noDeliveryTitle` and `auth.forgotPassword.noDeliveryDescription` in all ten locales, passed by the Console's forgot-password page and `DefaultForgotPasswordPage`.

‎ui/apps/console/src/pages/auth/ForgotPasswordPage.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ export function ForgotPasswordPage() {
7676
phoneSuccessDescription: t('auth.forgotPassword.phoneSuccessDescription', {
7777
defaultValue: 'Your password has been reset. You can now sign in with your new password.',
7878
}),
79+
noDeliveryTitle: t('auth.forgotPassword.noDeliveryTitle', {
80+
defaultValue: "A reset link can't be emailed here",
81+
}),
82+
noDeliveryDescription: t('auth.forgotPassword.noDeliveryDescription', {
83+
defaultValue:
84+
"This deployment doesn't send email, so no password reset link can reach you. Ask an administrator: they can set a new password for you from Setup → Users.",
85+
}),
7986
rememberPasswordText: t('auth.forgotPassword.rememberPasswordText', {
8087
defaultValue: 'Remember your password?',
8188
}),

‎ui/packages/app-shell/src/console/auth/ForgotPasswordPage.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export function ForgotPasswordPage() {
2828
submittingButton: t('auth.forgotPassword.submittingButton'),
2929
successTitle: t('auth.forgotPassword.successTitle'),
3030
successDescription: t('auth.forgotPassword.successDescription'),
31+
noDeliveryTitle: t('auth.forgotPassword.noDeliveryTitle'),
32+
noDeliveryDescription: t('auth.forgotPassword.noDeliveryDescription'),
3133
backToSignInText: t('auth.forgotPassword.backToSignInText'),
3234
rememberPasswordText: t('auth.forgotPassword.rememberPasswordText'),
3335
signInText: t('auth.forgotPassword.signInText'),

‎ui/packages/auth/src/ForgotPasswordForm.tsx‎

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ export interface ForgotPasswordFormLabels {
5858
phoneSuccessTitle?: string;
5959
/** Phone reset — success screen description. */
6060
phoneSuccessDescription?: string;
61+
/**
62+
* Success screen title when the server reports `features.emailDelivery: false`
63+
* (defaults to "A reset link can't be emailed here").
64+
*/
65+
noDeliveryTitle?: string;
66+
/**
67+
* Success screen description when the server reports
68+
* `features.emailDelivery: false`. It describes the deployment, never the
69+
* address, so it carries no `{email}` hole.
70+
*/
71+
noDeliveryDescription?: string;
6172
}
6273

6374
export interface ForgotPasswordFormProps {
@@ -139,6 +150,12 @@ export function ForgotPasswordForm({
139150
// deliverable SMS service — so the link never leads to codes that can't
140151
// arrive.
141152
const [phoneOtpEnabled, setPhoneOtpEnabled] = useState(false);
153+
// Whether mail leaves this deployment (`features.emailDelivery`). Only an
154+
// explicit `false` swaps the success screen: an absent key (a server that
155+
// predates the flag) or a failed config fetch is unknown, and keeps the
156+
// "check your email" text rather than telling people on a mail-sending
157+
// deployment to go find an administrator.
158+
const [emailDeliveryOff, setEmailDeliveryOff] = useState(false);
142159
const [mode, setMode] = useState<'email' | 'phone'>('email');
143160
const [phone, setPhone] = useState('');
144161
const [otpCode, setOtpCode] = useState('');
@@ -154,10 +171,13 @@ export function ForgotPasswordForm({
154171
Promise.resolve()
155172
.then(() => getAuthConfig())
156173
.then((config) => {
157-
if (!cancelled) setPhoneOtpEnabled(config?.features?.phoneNumberOtp === true);
174+
if (cancelled) return;
175+
setPhoneOtpEnabled(config?.features?.phoneNumberOtp === true);
176+
setEmailDeliveryOff(config?.features?.emailDelivery === false);
158177
})
159178
.catch(() => {
160-
// Phone reset is an enhancement — a failed config fetch just hides it.
179+
// Phone reset is an enhancement — a failed config fetch just hides it,
180+
// and leaves the email success text as it is (delivery unknown).
161181
});
162182
return () => {
163183
cancelled = true;
@@ -198,6 +218,10 @@ export function ForgotPasswordForm({
198218
phoneSuccessDescription:
199219
labels.phoneSuccessDescription ??
200220
'Your password has been reset. You can now sign in with your new password.',
221+
noDeliveryTitle: labels.noDeliveryTitle ?? "A reset link can't be emailed here",
222+
noDeliveryDescription:
223+
labels.noDeliveryDescription ??
224+
"This deployment doesn't send email, so no password reset link can reach you. Ask an administrator: they can set a new password for you from Setup → Users.",
201225
};
202226

203227
const handleSubmit = async (e: React.FormEvent) => {
@@ -284,18 +308,30 @@ export function ForgotPasswordForm({
284308
}
285309

286310
if (submitted) {
287-
const successMsg = l.successDescription.includes('{email}')
288-
? l.successDescription.replace('{email}', email)
289-
: `${l.successDescription} ${email}`;
311+
// No account-existence oracle: the reset request answers a known and an
312+
// unknown address alike, and which text shows here is decided by the
313+
// deployment's `features.emailDelivery` alone, never by that answer.
314+
// The no-delivery text names no address at all.
315+
const successMsg = emailDeliveryOff
316+
? l.noDeliveryDescription
317+
: l.successDescription.includes('{email}')
318+
? l.successDescription.replace('{email}', email)
319+
: `${l.successDescription} ${email}`;
290320
return (
291321
<div className="mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]">
292322
<AuthFormHeader
293323
icon={
294-
<span className="flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-500/10 text-emerald-600 ring-1 ring-emerald-500/15 dark:text-emerald-400">
324+
<span
325+
className={
326+
emailDeliveryOff
327+
? 'flex h-12 w-12 items-center justify-center rounded-2xl bg-amber-500/10 text-amber-600 ring-1 ring-amber-500/15 dark:text-amber-400'
328+
: 'flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-500/10 text-emerald-600 ring-1 ring-emerald-500/15 dark:text-emerald-400'
329+
}
330+
>
295331
<AuthMailIcon />
296332
</span>
297333
}
298-
title={l.successTitle}
334+
title={emailDeliveryOff ? l.noDeliveryTitle : l.successTitle}
299335
description={successMsg}
300336
/>
301337
{loginUrl && (

‎ui/packages/auth/src/__tests__/forgot-password-success-message.test.tsx‎

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
import React from 'react';
2525
import { describe, it, expect, vi } from 'vitest';
26-
import { render, screen, fireEvent } from '@testing-library/react';
26+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2727
import { AuthProvider } from '../AuthProvider';
2828
import { ForgotPasswordForm } from '../ForgotPasswordForm';
2929
import type { AuthClient, AuthPublicConfig } from '../types';
@@ -110,3 +110,87 @@ describe('ForgotPasswordForm — success message substitution (objectui#4135)',
110110
expect(occurrences(description.textContent ?? '', 'control@example.com')).toBe(1);
111111
});
112112
});
113+
114+
/**
115+
* objectstack#30366 — on a deployment that sends no mail the success screen
116+
* must not say a reset link was sent. The server reports this as
117+
* `features.emailDelivery` on `/auth/config`; only an explicit `false` swaps
118+
* the text, and the swap depends on the deployment alone, so a known and an
119+
* unknown address read the same (no account-existence oracle).
120+
*/
121+
describe('ForgotPasswordForm — success screen on a deployment that sends no mail (objectstack#30366)', () => {
122+
function renderWithConfig(
123+
getConfig: AuthClient['getConfig'],
124+
props: React.ComponentProps<typeof ForgotPasswordForm> = {},
125+
) {
126+
const client = createMockClient({ getConfig });
127+
const view = render(
128+
<AuthProvider authUrl="/api/auth" client={client}>
129+
<ForgotPasswordForm {...props} />
130+
</AuthProvider>,
131+
);
132+
return { client, view };
133+
}
134+
135+
const noMail = () => vi.fn().mockResolvedValue({ features: { emailDelivery: false } } as AuthPublicConfig);
136+
137+
it('says a reset link cannot be emailed and to ask an administrator when emailDelivery is false', async () => {
138+
const getConfig = noMail();
139+
renderWithConfig(getConfig);
140+
await waitFor(() => expect(getConfig).toHaveBeenCalled());
141+
await submit('someone@example.com');
142+
143+
expect(await screen.findByText("A reset link can't be emailed here")).toBeTruthy();
144+
const description = screen.getByText(/doesn't send email/);
145+
expect(description.textContent).toMatch(/administrator/);
146+
expect(description.textContent).toMatch(/Setup → Users/);
147+
expect(description.textContent).not.toContain('someone@example.com');
148+
expect(screen.queryByText('Check your email')).toBeNull();
149+
expect(screen.queryByText(/We've sent a password reset link/)).toBeNull();
150+
});
151+
152+
it('reads a known and an unknown address the same: the text follows the deployment, not the address', async () => {
153+
// The reset request answers both alike (the server's no-oracle rule); the
154+
// screen must add no difference of its own, address substitution included.
155+
const screens: string[] = [];
156+
for (const address of ['known@example.com', 'nobody-here@example.com']) {
157+
const getConfig = noMail();
158+
const { client, view } = renderWithConfig(getConfig);
159+
await waitFor(() => expect(getConfig).toHaveBeenCalled());
160+
await submit(address);
161+
await screen.findByText("A reset link can't be emailed here");
162+
expect(client.forgotPassword).toHaveBeenCalledWith(address);
163+
screens.push(view.container.textContent ?? '');
164+
view.unmount();
165+
}
166+
expect(screens[0]).toBe(screens[1]);
167+
expect(screens[0]).not.toContain('@example.com');
168+
});
169+
170+
it('uses caller-supplied translated labels for the no-delivery screen', async () => {
171+
const getConfig = noMail();
172+
renderWithConfig(getConfig, {
173+
labels: { noDeliveryTitle: 'NO-MAIL TITLE', noDeliveryDescription: 'NO-MAIL BODY' },
174+
});
175+
await waitFor(() => expect(getConfig).toHaveBeenCalled());
176+
await submit('someone@example.com');
177+
178+
expect(await screen.findByText('NO-MAIL TITLE')).toBeTruthy();
179+
expect(screen.getByText('NO-MAIL BODY')).toBeTruthy();
180+
});
181+
182+
it.each([
183+
['emailDelivery is true', () => vi.fn().mockResolvedValue({ features: { emailDelivery: true } } as AuthPublicConfig)],
184+
['the server sends no emailDelivery key (older server)', () => vi.fn().mockResolvedValue({ features: {} } as AuthPublicConfig)],
185+
['the config fetch fails', () => vi.fn().mockRejectedValue(new Error('offline'))],
186+
])('keeps the "check your email" screen when %s', async (_label, makeGetConfig) => {
187+
const getConfig = makeGetConfig();
188+
renderWithConfig(getConfig);
189+
await waitFor(() => expect(getConfig).toHaveBeenCalled());
190+
await submit('someone@example.com');
191+
192+
expect(await screen.findByText('Check your email')).toBeTruthy();
193+
expect(screen.getByText(/We've sent a password reset link to someone@example.com/)).toBeTruthy();
194+
expect(screen.queryByText("A reset link can't be emailed here")).toBeNull();
195+
});
196+
});

‎ui/packages/auth/src/types.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,16 @@ export interface AuthPublicConfig {
333333
* entry point whose code can never arrive.
334334
*/
335335
phoneNumberOtp?: boolean;
336+
/**
337+
* Whether mail leaves this deployment (`AuthFeaturesConfigSchema.emailDelivery`
338+
* in `@objectstack/spec`): `true` only when the wired email service reports
339+
* itself configured; `false` with no email service or the stock `log`
340+
* transport. The forgot-password form swaps its success screen to "a reset
341+
* link can't be emailed here, ask an administrator" on an explicit `false`
342+
* only. It describes the deployment, never an address. Absent (older
343+
* server) ⇒ the form keeps its "check your email" text.
344+
*/
345+
emailDelivery?: boolean;
336346
/**
337347
* better-auth admin plugin mounted server-side (`auth.plugins.admin`;
338348
* SCIM forces it on). Gates the admin user-management surfaces — the

‎ui/packages/i18n/src/locales/ar.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3098,6 +3098,8 @@ const ar = {
30983098
useEmailResetText: "إعادة التعيين بالبريد الإلكتروني بدلاً من ذلك",
30993099
phoneSuccessTitle: "تمت إعادة تعيين كلمة المرور",
31003100
phoneSuccessDescription: "تمت إعادة تعيين كلمة المرور. يمكنك الآن تسجيل الدخول بكلمة المرور الجديدة.",
3101+
noDeliveryTitle: "لا يمكن إرسال رابط إعادة التعيين بالبريد الإلكتروني هنا",
3102+
noDeliveryDescription: "هذا النشر لا يرسل رسائل بريد إلكتروني، لذلك لن يصلك أي رابط لإعادة تعيين كلمة المرور. اطلب من المسؤول: يمكنه تعيين كلمة مرور جديدة لك من الإعداد → المستخدمون.",
31013103
},
31023104
resetPassword: {
31033105
title: "تعيين كلمة مرور جديدة",

‎ui/packages/i18n/src/locales/de.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2844,6 +2844,8 @@ const de = {
28442844
useEmailResetText: "Stattdessen per E-Mail zurücksetzen",
28452845
phoneSuccessTitle: "Passwort zurückgesetzt",
28462846
phoneSuccessDescription: "Ihr Passwort wurde zurückgesetzt. Sie können sich jetzt mit dem neuen Passwort anmelden.",
2847+
noDeliveryTitle: "Hier kann kein Link zum Zurücksetzen per E-Mail gesendet werden",
2848+
noDeliveryDescription: "Diese Bereitstellung versendet keine E-Mails, daher kann Sie kein Link zum Zurücksetzen des Passworts erreichen. Wenden Sie sich an einen Administrator: Er kann unter Einrichtung → Benutzer ein neues Passwort für Sie festlegen.",
28472849
},
28482850
resetPassword: {
28492851
title: "Neues Passwort festlegen",

‎ui/packages/i18n/src/locales/en.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3396,6 +3396,8 @@ const en = {
33963396
useEmailResetText: 'Reset via email instead',
33973397
phoneSuccessTitle: 'Password reset',
33983398
phoneSuccessDescription: 'Your password has been reset. You can now sign in with your new password.',
3399+
noDeliveryTitle: "A reset link can't be emailed here",
3400+
noDeliveryDescription: "This deployment doesn't send email, so no password reset link can reach you. Ask an administrator: they can set a new password for you from Setup → Users.",
33993401
},
34003402
resetPassword: {
34013403
title: 'Set a new password',

‎ui/packages/i18n/src/locales/es.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2909,6 +2909,8 @@ const es = {
29092909
useEmailResetText: "Restablecer por correo electrónico en su lugar",
29102910
phoneSuccessTitle: "Contraseña restablecida",
29112911
phoneSuccessDescription: "Tu contraseña ha sido restablecida. Ahora puedes iniciar sesión con tu nueva contraseña.",
2912+
noDeliveryTitle: "Aquí no se puede enviar un enlace de restablecimiento por correo",
2913+
noDeliveryDescription: "Esta implementación no envía correos, así que ningún enlace para restablecer la contraseña puede llegarle. Pida ayuda a un administrador: puede establecerle una nueva contraseña en Configuración → Usuarios.",
29122914
},
29132915
resetPassword: {
29142916
title: "Establecer nueva contraseña",

0 commit comments

Comments
 (0)