From 98627a63806a0728403e38dd01e39042486cb4b1 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 20 Jul 2026 01:14:44 +0800 Subject: [PATCH] feat(console): user-import wizard defaults to the `auto` password policy (tracks framework#3236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Sign-in setup for imported users" selector in the identity import wizard gains an "Automatic (recommended)" option, now the default (was "No password"). `auto` lets the server decide per row — reachable users get an invitation (email/SMS), unreachable ones get a one-time password shown once — so the wizard works with or without an email/SMS service, and the one-time-password reveal surfaces only the rows that actually fell back (not the whole batch, as under `temporary`). - `IdentityPasswordPolicy` gains `'auto'`; the wizard's default state and the send-time policy ref default to `'auto'`. - POLICY_FALLBACKS + en/zh i18n gain `auto` label/hint; `none` drops its "(recommended)" marker and is relabeled "No password (identity only)". - The batch adapter (splitting, merge, one-time-password reveal) is policy-agnostic, so no adapter changes were needed — `auto` fallback rows carry `temporaryPassword` exactly like `temporary` rows. Co-Authored-By: Claude Opus 4.8 --- .changeset/import-wizard-auto-policy.md | 8 ++++++++ .../app-shell/src/views/IdentityImportPanels.tsx | 14 +++++++++----- packages/app-shell/src/views/ObjectView.tsx | 4 ++-- .../src/views/__tests__/identityImport.test.ts | 9 ++++++++- packages/app-shell/src/views/identityImport.ts | 15 +++++++++------ packages/i18n/src/locales/en.ts | 8 +++++--- packages/i18n/src/locales/zh.ts | 8 +++++--- 7 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 .changeset/import-wizard-auto-policy.md diff --git a/.changeset/import-wizard-auto-policy.md b/.changeset/import-wizard-auto-policy.md new file mode 100644 index 000000000..675bea0cb --- /dev/null +++ b/.changeset/import-wizard-auto-policy.md @@ -0,0 +1,8 @@ +--- +"@object-ui/app-shell": minor +"@object-ui/i18n": patch +--- + +**Console user-import wizard defaults to the `auto` password policy (tracks framework#3236).** The "Sign-in setup for imported users" selector gains an **Automatic (recommended)** option and it is now the default (was "No password"). `auto` decides per row on the server: reachable users get an invitation (email / SMS), anyone who can't be reached gets a one-time password shown once on the result screen — so it works with or without an email/SMS service, and the one-time-password reveal now surfaces only the rows that actually fell back (instead of the whole batch under `temporary`). + +The other three policies are unchanged and still selectable: `invite` (force invitations, unreachable rows fail), `temporary` (force one-time passwords for every row), `none` (identity only). New `console.identityImport.policy.auto` / `policyHint.auto` strings added for `en` and `zh`; the `none` label drops its "(recommended)" marker. diff --git a/packages/app-shell/src/views/IdentityImportPanels.tsx b/packages/app-shell/src/views/IdentityImportPanels.tsx index 03dde76a6..e7865c594 100644 --- a/packages/app-shell/src/views/IdentityImportPanels.tsx +++ b/packages/app-shell/src/views/IdentityImportPanels.tsx @@ -20,17 +20,21 @@ import { } from './identityImport'; const POLICY_FALLBACKS: Record = { - none: { - label: 'No password (recommended)', - hint: 'Users first sign in with a phone OTP, magic link, or password-reset link, then set their own password.', + auto: { + label: 'Automatic (recommended)', + hint: 'Reachable users get an invitation (email or SMS); anyone we can\'t reach gets a one-time password, shown ONCE on the result screen. Works with or without an email/SMS service.', }, invite: { label: 'Send invitations', - hint: 'Each created user gets a set-your-password email (or an invitation SMS for phone-only rows). Requires a configured email/SMS service.', + hint: 'Every created user gets a set-your-password email (or an invitation SMS for phone-only rows). Requires a configured email/SMS service — unreachable rows fail.', }, temporary: { label: 'Temporary passwords', - hint: 'For deployments without email/SMS: each created user gets a one-time password, shown ONCE on the result screen. First sign-in forces a change.', + hint: 'For deployments without email/SMS: every created user gets a one-time password, shown ONCE on the result screen. First sign-in forces a change.', + }, + none: { + label: 'No password (identity only)', + hint: 'Users first sign in with a phone OTP, magic link, or password-reset link, then set their own password.', }, }; diff --git a/packages/app-shell/src/views/ObjectView.tsx b/packages/app-shell/src/views/ObjectView.tsx index 371fe5754..24172ae49 100644 --- a/packages/app-shell/src/views/ObjectView.tsx +++ b/packages/app-shell/src/views/ObjectView.tsx @@ -388,8 +388,8 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an const { features } = useExpressionContext(); const isIdentityImport = objectDef.name === IDENTITY_IMPORT_OBJECT; const identityImportEnabled = isIdentityImport && features?.admin === true && isAdmin; - const [identityPasswordPolicy, setIdentityPasswordPolicy] = useState('none'); - const identityPolicyRef = useRef('none'); + const [identityPasswordPolicy, setIdentityPasswordPolicy] = useState('auto'); + const identityPolicyRef = useRef('auto'); identityPolicyRef.current = identityPasswordPolicy; const identityDataSource = useMemo( () => (identityImportEnabled diff --git a/packages/app-shell/src/views/__tests__/identityImport.test.ts b/packages/app-shell/src/views/__tests__/identityImport.test.ts index 04f7a179b..f62f50c84 100644 --- a/packages/app-shell/src/views/__tests__/identityImport.test.ts +++ b/packages/app-shell/src/views/__tests__/identityImport.test.ts @@ -89,7 +89,7 @@ describe('mergeIdentityBatchResults', () => { }); describe('createIdentityImportDataSource', () => { - const makeAdapter = (fetchImpl: ReturnType, policy: 'none' | 'invite' | 'temporary' = 'none') => + const makeAdapter = (fetchImpl: ReturnType, policy: 'auto' | 'none' | 'invite' | 'temporary' = 'auto') => createIdentityImportDataSource({ base: { find: 'passthrough-marker', createImportJob: () => {}, undoImportJob: () => {} }, authFetch: fetchImpl as any, @@ -118,6 +118,13 @@ describe('createIdentityImportDataSource', () => { expect(res.results[500].row).toBe(501); // renumbered across batches }); + it('sends the default `auto` policy (framework#3236) when the admin leaves the selector untouched', async () => { + const fetchImpl = vi.fn(async (_url: string, init: any) => okResponse(JSON.parse(init.body).rows) as any); + const ds = makeAdapter(fetchImpl); // default policy + await ds.importRecords('sys_user', { format: 'json', rows: [{ email: 'a@x.co' }] }); + expect(JSON.parse(fetchImpl.mock.calls[0][1].body).passwordPolicy).toBe('auto'); + }); + it('passes dryRun and upsert options through', async () => { const fetchImpl = vi.fn(async (_url: string, init: any) => okResponse(JSON.parse(init.body).rows) as any); const ds = makeAdapter(fetchImpl); diff --git a/packages/app-shell/src/views/identityImport.ts b/packages/app-shell/src/views/identityImport.ts index cb38ea115..2e01b1513 100644 --- a/packages/app-shell/src/views/identityImport.ts +++ b/packages/app-shell/src/views/identityImport.ts @@ -14,12 +14,15 @@ // policy. The endpoint is idempotent on upsert (matched by email/phone), so // re-running a failed batch is safe. // -// Password policies (framework#2820): `none` (default — identity only, users -// first sign in via phone OTP / magic link / reset link and set a password -// afterwards), `invite` (adds a set-your-password email / invitation SMS per -// created row), `temporary` (per-row one-time passwords returned ONLY in the +// Password policies: `auto` (default, framework#3236 — per row: deliverable +// rows get an invitation, unreachable rows fall back to a one-time password), +// `invite` (force an invitation — set-your-password email / SMS — for every +// row; unreachable rows fail), `temporary` (force a per-row one-time password +// for every row), `none` (identity only — users first sign in via phone OTP / +// magic link / reset link and set a password afterwards). One-time passwords +// (`auto` fallback rows and all of `temporary`) are returned ONLY in the // response — the result step must surface them immediately; they are never -// persisted anywhere, client or server). +// persisted anywhere, client or server. import type { ImportRecordsResult, ImportRequestOptions, ImportRowResult } from '@object-ui/types'; @@ -28,7 +31,7 @@ export const IDENTITY_IMPORT_OBJECT = 'sys_user'; /** Server-side hard cap on rows per identity import request. */ export const IDENTITY_IMPORT_BATCH_SIZE = 500; -export type IdentityPasswordPolicy = 'none' | 'invite' | 'temporary'; +export type IdentityPasswordPolicy = 'auto' | 'none' | 'invite' | 'temporary'; /** Row results from the identity endpoint may carry a one-time password. */ export type IdentityImportRowResult = ImportRowResult & { diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 5bd089564..b378d76d7 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -1415,14 +1415,16 @@ const en = { identityImport: { policyTitle: 'Sign-in setup for imported users', policy: { - none: 'No password (recommended)', + auto: 'Automatic (recommended)', invite: 'Send invitations', temporary: 'Temporary passwords', + none: 'No password (identity only)', }, policyHint: { + auto: 'Reachable users get an invitation (email or SMS); anyone we can\'t reach gets a one-time password, shown ONCE on the result screen. Works with or without an email/SMS service.', + invite: 'Every created user gets a set-your-password email (or an invitation SMS for phone-only rows). Requires a configured email/SMS service — unreachable rows fail.', + temporary: 'For deployments without email/SMS: every created user gets a one-time password, shown ONCE on the result screen. First sign-in forces a change.', none: 'Users first sign in with a phone OTP, magic link, or password-reset link, then set their own password.', - invite: 'Each created user gets a set-your-password email (or an invitation SMS for phone-only rows). Requires a configured email/SMS service.', - temporary: 'For deployments without email/SMS: each created user gets a one-time password, shown ONCE on the result screen. First sign-in forces a change.', }, passwordsNote: 'Temporary passwords — shown once, never stored. Save them now; each user must change theirs at first sign-in.', passwordsMore: 'More entries omitted — use the download.', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index c5682b6ac..5d80bacd2 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -1487,14 +1487,16 @@ const zh = { identityImport: { policyTitle: '导入用户的登录方式', policy: { - none: '不设密码(推荐)', + auto: '自动(推荐)', invite: '发送邀请', temporary: '临时密码', + none: '不设密码(仅身份)', }, policyHint: { - none: '用户通过手机验证码、魔法链接或重置链接首次登录,之后自行设置密码。', - invite: '为每个新建用户发送"设置密码"邮件(仅手机号的行发送邀请短信)。需要已配置邮件/短信服务。', + auto: '可触达的用户收到邀请(邮件或短信);无法触达的行生成一次性密码,仅在结果页显示一次。无论是否配置邮件/短信服务都可用。', + invite: '为每个新建用户发送"设置密码"邮件(仅手机号的行发送邀请短信)。需要已配置邮件/短信服务——无法触达的行会失败。', temporary: '适用于未配置邮件/短信的部署:为每个新建用户生成一次性密码,仅在结果页显示一次,首次登录强制修改。', + none: '用户通过手机验证码、魔法链接或重置链接首次登录,之后自行设置密码。', }, passwordsNote: '临时密码——仅显示一次,不会被保存。请立即保存;用户首次登录时必须修改。', passwordsMore: '更多条目已省略——请使用下载。',