Repository navigation
Commit 10ecafd
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgEwzCoEjW7R3Z9w3U4hkBrX)_
domain:ui#3 · os-bill · session_01Utp6CbPN5UYMnBaNJPJWfG
## 维护者速读
- 改了什么:两步验证关着时,Setup
里编辑组织的表单在标题下直接说明「强制多因素认证」需要先开两步验证、勾上保存会被拒、去哪里开;两步验证开着时,保存成功把它打开后,提示里多一句:哪些成员要去绑定身份验证器、从什么时候起算宽限期(默认
7 天)。注册身份验证器的页面遇到 404(两步验证没开)时,显示本地化的「此部署未开启双重验证,请联系管理员在设置 → 认证中开启」,不再显示
`Two-factor enable failed (404)`。
- 为什么改:服务端半边 #30293
已经拒存并解除锁死,但界面上管理员事先看不到任何说明,被拒后只有一段英文;成员若被引到注册页,看到的是一句未翻译的 404。
- 风险与回滚:只改界面文案与两个认证调用的报错字段(多带 `status`、`code`,原 message 不变),新增 3
个翻译键、十个语言包齐全。没有服务端、元数据或权限改动。回滚即撤本 PR。
-
先看:`ui/packages/app-shell/src/console/organizationMfaNotices.ts`(何时提示、何时不提示);`ui/packages/app-shell/src/console/AppContent.tsx`
的组织表单挂接与保存成功 toast;`RemediationOverlay.tsx` 的 `enrollRefusalMessage`。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/app-shell/src/console/__tests__/organizationMfaNotices-30295.test.tsx
packages/app-shell/src/console/__tests__/RemediationOverlay.enrollUnavailable-30295.test.tsx`,预期
11/11 通过。
- 需要你做的:无,席位落地。
Fixes #30295
Clause-②: yes (widening)
Metadata-protocol: none
## What changes
The three Wanted items of the card, on a zh-CN console against a local
`app-showcase` backend carrying #30293:
- **Before saving (item 1, first half).** While `GET
/api/v1/auth/config` answers `features.twoFactor: false`, the
`sys_organization` edit form (the Console's global record-form modal in
`AppContent.tsx`; the object declares no `editMode: 'page'`) shows a
notice as the dialog's description: Require Multi-Factor Auth needs
two-factor, a save with it ticked is refused, and how to turn two-factor
on (Require multi-factor authentication in Setup → Authentication, which
applies to every organization, or `OS_AUTH_TWO_FACTOR=true`). The notice
is judged by `organizationMfaFormNotice` in the new
`organizationMfaNotices.ts`: only that object, only a form that offers
`require_mfa`, and only when the flag reads `false` (an unread config
says nothing).
- **The refused save (item 1, second half): already held, no change.**
Measured on the before tree: the generic form shows the server's `400
VALIDATION_FAILED` text on the `require_mfa` field, not a generic
failure (the `fields[]` entry #30293 sends). See Acceptance notes for
its language.
- **The accepted save (item 2).** A save that turns `require_mfa` on for
a row that stored it off adds a line to the saved toast (the existing
`notice` slot of `toastRecordSave`): every member without an
authenticator app must set one up, and is blocked from data once the MFA
grace period ends (7 days unless changed in Setup → Authentication),
counted from their first use after the change; members already enrolled
are not affected. The stored value comes from one `findOne` when the
organization form opens (`useStoredRequireMfa`, keyed on the object, the
record id and the open flag); a create counts as off, an unread value as
off, a row already on says nothing.
- **The enrollment screen (item 3).** `enrollTotp` / `verifyTotp` in
`@object-ui/auth` now reject with the HTTP `status` and the server's
`code` beside the unchanged message (`twoFactorEnrollError`). The
authenticator step of `RemediationOverlay` reads `404` (the
`/two-factor/*` endpoints are mounted only while two-factor is on) as
`auth.remediation.mfa.unavailable`, saying enrollment is off and that an
administrator turns it on in Setup → Authentication, and
`INVALID_PASSWORD` as the password screen's existing sentence. Any other
refusal keeps the server's message.
- **i18n (the widening).** Three new keys in all ten packs:
`auth.remediation.mfa.unavailable`,
`console.organizationMfa.needsTwoFactor`,
`console.organizationMfa.nowRequired`.
- Changeset `.changeset/30295-org-mfa-notice-and-enroll-refusal.md`
(`patch` for `@object-ui/app-shell`, `@object-ui/auth`,
`@object-ui/i18n`).
## Why
#30293 made the server refuse `require_mfa = true` while two-factor is
off and stopped the gate from holding anyone it cannot enroll. Nothing
in the console said so: the form offered the checkbox with the object's
help ("When true, every member … must enroll"), the administrator
learned the dependency only from the refusal, a successful save said
nothing about who is now held or when, and a member sent to enroll on a
deployment without two-factor read `Two-factor enable failed (404)`.
## Tests
-
`ui/packages/app-shell/src/console/__tests__/organizationMfaNotices-30295.test.tsx`
(new, 7): the notice's three conditions (two-factor off; on or unread;
another object or no field), the saved line's (turned on; already on,
left off, another object; unread counts as off), and the stored-value
hook (one read per open edit, create is off, closed form or another
object reads nothing, a failed read stays unread).
-
`ui/packages/app-shell/src/console/__tests__/RemediationOverlay.enrollUnavailable-30295.test.tsx`
(new, 4): zh and en 404 sentences (and the English `failed (404)` gone),
zh `INVALID_PASSWORD`, and a control where a 429 keeps the server's
message. The other RemediationOverlay files each carry their own
`useAuth` mock and no shared harness exists, so this is its own file.
- `ui/packages/auth/src/__tests__/createAuthClient.test.ts`: a new
top-level describe in the existing harness, three pins through the real
better-auth client and a mocked fetch (404 on enable and on verify
carries `status`, a wrong password carries `code` and `status`, messages
unchanged).
- **Ablations** with `scripts/ablation-replace.mjs`, each restore proven
blob == HEAD (these suites import source, so no build sits between
mutation and run). A, C and D ran on `c4ec9241`; B ran on `c4ec9241` and
again on `9fb2f487`, after its pins moved into the harness file (3 red
of 37):
- A, the overlay's 404 branch deleted: 2 red (both 404 sentences).
- B, `status` no longer copied onto the error: 3 red (`expected
undefined to be 404`).
- C, the notice's two-factor check deleted: 1 red (`expected
'console.organizationMfa.needsTwoFactor' to be undefined`).
- D, the already-on check deleted: 1 red (`expected
'console.organizationMfa.nowRequired' to be undefined`).
## Acceptance notes
- **The refusal under the field is the server's English.** In a zh-CN
session the field shows #30293's English message (it names
`OS_AUTH_TWO_FACTOR` and `auth.mfa_required`). It is server-authored
text; this PR adds the localized notice above it rather than replacing
it. Localizing that refusal belongs to the server's message.
- **The grace period's number is the default.** The console has no read
of `auth.mfa_grace_period_days` (`/api/v1/auth/config` does not publish
it, `@objectstack/client` has no settings read, and the settings page is
admin-only), so the saved line says "7 days unless changed in Setup →
Authentication". The server knows the configured value; a write notice
from the server would carry it.
- **The page-mode host is not wired.** `RecordFormPage` would show
neither line for an organization; `sys_organization` declares no
`editMode: 'page'`, so the modal is the only host a user reaches today.
- The object's field help (`packages/platform-objects`, server metadata)
still says only "When true, every member … must enroll"; this PR leaves
it alone (out of this card's file surface).
- Two names for two switches: the field is 强制多因素认证
(`sys_organization.require_mfa`, every member of one organization) and
the setting is 要求多因素认证 (`auth.mfa_required`, every organization, and it
turns two-factor on). The notice names both in each pack's own spelling.
## Verification
Suites, type-check and gates ran on `30cfab99` (main `9afcd6a6`, which
carries #30293, plus this branch); the final head `fc268ce2` merges
`main` again (no `ui/` file moved) and re-ran the scoped suites.
<details><summary>Browser check (zh-CN, local app-showcase backend,
Playwright)</summary>
The screenshots are in the project files (`ui-seat3/30295/`); no image
host is reachable from this session. What each shows:
- `before-enroll.png` / `after-enroll.png`: the MFA overlay raised by
answering one data read with the server's `403 MFA_REQUIRED` (two-factor
is off, so the real gate no longer holds anyone), then the password step
against the real backend, whose `POST /two-factor/enable` answered 404.
Before: `Two-factor enable failed (404)`. After:
此部署未开启双重验证,暂时无法绑定身份验证器。请联系管理员在“设置 → 认证”中开启后再试。
- `before-form.png` / `after-form.png`: the 编辑组织 dialog with two-factor
off. Before: no notice. After: the notice under the title.
- `before-saved.png` / `after-saved.png`: the box ticked and 更新 pressed
with two-factor off. Both show the server's refusal on the field; the
row stays `require_mfa: false` (read back over REST).
- `after-2fa-saved.png`: backend restarted with
`OS_AUTH_TWO_FACTOR=true`, the box ticked and saved: the toast reads
组织“Default Organization”已更新 plus
本组织所有尚未绑定身份验证器的成员现在都必须绑定。每位成员从此次变更后第一次使用起开始计算 MFA 宽限期(默认 7 天,可在“设置 →
认证”中修改)… The row reads `require_mfa: true`.
</details>
<details><summary>Suites and type-check (under
scripts/pm/os-verify-lock.sh)</summary>
- `pnpm --dir ui exec vitest run` over the three changed test files plus
the files importing the changed modules' behaviour (the five other
`RemediationOverlay.*` files, `changePassword-code-27869`, and the i18n
parity files `all-locales-key-parity`, `authRemediation-locale-parity`,
`en-zh-key-parity`, `console-namespace-3546`): 14 files, 158 tests
passed (the auth pins then moved into `createAuthClient.test.ts`:
37/37).
- `pnpm exec turbo run type-check --filter=@object-ui/app-shell
--filter=@object-ui/auth --filter=@object-ui/i18n --concurrency=2`:
Tasks 70 successful, 70 total. VERDICT command-exit 0.
</details>
<details><summary>Gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `30cfab99`: 35 derived, each run with its
exit code captured before any pipe: 35 exit 0 (`check:type-check-debt`
under the lock). `node scripts/check-sdui-manifest.mjs` exit 0. `--ran`
over the record (`COMMAND :: exit N`): 35 derived, 35 run, 0
NOT-MEASURED, 0 UNRUN, exit 0 ("a DERIVED zero — all 35 recorded an exit
code and none of them is 3"). The merge of `main` (`fc268ce2`) brought
no file under `ui/` or `.changeset/`; the derivation on the merged head
is byte-identical, and the scoped suites re-ran green there (13 files,
158 tests). `pnpm lint` is CI's run.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic
(`packages/spec` not touched).
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` is CI's run.
- [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 (the 维护者速读 block is
Chinese by the Communication rule).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CYeUM6wKr7VK5xLHXn)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1e6f542 commit 10ecafd
19 files changed
Lines changed: 501 additions & 10 deletions
File tree
- .changeset
- ui/packages
- app-shell/src/console
- __tests__
- auth/src
- __tests__
- i18n/src
- __tests__
- locales
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| 75 | + | |
75 | 76 | | |
76 | 77 | | |
77 | 78 | | |
| |||
964 | 965 | | |
965 | 966 | | |
966 | 967 | | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
| 973 | + | |
| 974 | + | |
| 975 | + | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
| 979 | + | |
| 980 | + | |
| 981 | + | |
| 982 | + | |
| 983 | + | |
| 984 | + | |
967 | 985 | | |
968 | 986 | | |
969 | 987 | | |
| |||
1038 | 1056 | | |
1039 | 1057 | | |
1040 | 1058 | | |
1041 | | - | |
| 1059 | + | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
1042 | 1072 | | |
1043 | 1073 | | |
1044 | 1074 | | |
| |||
1080 | 1110 | | |
1081 | 1111 | | |
1082 | 1112 | | |
1083 | | - | |
| 1113 | + | |
1084 | 1114 | | |
1085 | 1115 | | |
1086 | 1116 | | |
| |||
1718 | 1748 | | |
1719 | 1749 | | |
1720 | 1750 | | |
| 1751 | + | |
1721 | 1752 | | |
1722 | 1753 | | |
1723 | 1754 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
248 | 248 | | |
249 | 249 | | |
250 | 250 | | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
251 | 275 | | |
252 | 276 | | |
253 | 277 | | |
| |||
268 | 292 | | |
269 | 293 | | |
270 | 294 | | |
271 | | - | |
| 295 | + | |
272 | 296 | | |
273 | 297 | | |
274 | 298 | | |
| |||
282 | 306 | | |
283 | 307 | | |
284 | 308 | | |
285 | | - | |
| 309 | + | |
286 | 310 | | |
287 | 311 | | |
288 | 312 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
0 commit comments