Skip to content

Commit 7abe4cd

Browse files
os-zhuangclaude
andauthored
feat(console): user-import wizard defaults to the auto password policy (tracks framework#3236) (#2701)
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: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e7a8de7 commit 7abe4cd

7 files changed

Lines changed: 46 additions & 20 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@object-ui/app-shell": minor
3+
"@object-ui/i18n": patch
4+
---
5+
6+
**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`).
7+
8+
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.

packages/app-shell/src/views/IdentityImportPanels.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,21 @@ import {
2020
} from './identityImport';
2121

2222
const POLICY_FALLBACKS: Record<IdentityPasswordPolicy, { label: string; hint: string }> = {
23-
none: {
24-
label: 'No password (recommended)',
25-
hint: 'Users first sign in with a phone OTP, magic link, or password-reset link, then set their own password.',
23+
auto: {
24+
label: 'Automatic (recommended)',
25+
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.',
2626
},
2727
invite: {
2828
label: 'Send invitations',
29-
hint: 'Each created user gets a set-your-password email (or an invitation SMS for phone-only rows). Requires a configured email/SMS service.',
29+
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.',
3030
},
3131
temporary: {
3232
label: 'Temporary passwords',
33-
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.',
33+
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.',
34+
},
35+
none: {
36+
label: 'No password (identity only)',
37+
hint: 'Users first sign in with a phone OTP, magic link, or password-reset link, then set their own password.',
3438
},
3539
};
3640

packages/app-shell/src/views/ObjectView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,8 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an
388388
const { features } = useExpressionContext();
389389
const isIdentityImport = objectDef.name === IDENTITY_IMPORT_OBJECT;
390390
const identityImportEnabled = isIdentityImport && features?.admin === true && isAdmin;
391-
const [identityPasswordPolicy, setIdentityPasswordPolicy] = useState<IdentityPasswordPolicy>('none');
392-
const identityPolicyRef = useRef<IdentityPasswordPolicy>('none');
391+
const [identityPasswordPolicy, setIdentityPasswordPolicy] = useState<IdentityPasswordPolicy>('auto');
392+
const identityPolicyRef = useRef<IdentityPasswordPolicy>('auto');
393393
identityPolicyRef.current = identityPasswordPolicy;
394394
const identityDataSource = useMemo(
395395
() => (identityImportEnabled

packages/app-shell/src/views/__tests__/identityImport.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('mergeIdentityBatchResults', () => {
8989
});
9090

9191
describe('createIdentityImportDataSource', () => {
92-
const makeAdapter = (fetchImpl: ReturnType<typeof vi.fn>, policy: 'none' | 'invite' | 'temporary' = 'none') =>
92+
const makeAdapter = (fetchImpl: ReturnType<typeof vi.fn>, policy: 'auto' | 'none' | 'invite' | 'temporary' = 'auto') =>
9393
createIdentityImportDataSource({
9494
base: { find: 'passthrough-marker', createImportJob: () => {}, undoImportJob: () => {} },
9595
authFetch: fetchImpl as any,
@@ -118,6 +118,13 @@ describe('createIdentityImportDataSource', () => {
118118
expect(res.results[500].row).toBe(501); // renumbered across batches
119119
});
120120

121+
it('sends the default `auto` policy (framework#3236) when the admin leaves the selector untouched', async () => {
122+
const fetchImpl = vi.fn(async (_url: string, init: any) => okResponse(JSON.parse(init.body).rows) as any);
123+
const ds = makeAdapter(fetchImpl); // default policy
124+
await ds.importRecords('sys_user', { format: 'json', rows: [{ email: 'a@x.co' }] });
125+
expect(JSON.parse(fetchImpl.mock.calls[0][1].body).passwordPolicy).toBe('auto');
126+
});
127+
121128
it('passes dryRun and upsert options through', async () => {
122129
const fetchImpl = vi.fn(async (_url: string, init: any) => okResponse(JSON.parse(init.body).rows) as any);
123130
const ds = makeAdapter(fetchImpl);

packages/app-shell/src/views/identityImport.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
// policy. The endpoint is idempotent on upsert (matched by email/phone), so
1515
// re-running a failed batch is safe.
1616
//
17-
// Password policies (framework#2820): `none` (default — identity only, users
18-
// first sign in via phone OTP / magic link / reset link and set a password
19-
// afterwards), `invite` (adds a set-your-password email / invitation SMS per
20-
// created row), `temporary` (per-row one-time passwords returned ONLY in the
17+
// Password policies: `auto` (default, framework#3236 — per row: deliverable
18+
// rows get an invitation, unreachable rows fall back to a one-time password),
19+
// `invite` (force an invitation — set-your-password email / SMS — for every
20+
// row; unreachable rows fail), `temporary` (force a per-row one-time password
21+
// for every row), `none` (identity only — users first sign in via phone OTP /
22+
// magic link / reset link and set a password afterwards). One-time passwords
23+
// (`auto` fallback rows and all of `temporary`) are returned ONLY in the
2124
// response — the result step must surface them immediately; they are never
22-
// persisted anywhere, client or server).
25+
// persisted anywhere, client or server.
2326

2427
import type { ImportRecordsResult, ImportRequestOptions, ImportRowResult } from '@object-ui/types';
2528

@@ -28,7 +31,7 @@ export const IDENTITY_IMPORT_OBJECT = 'sys_user';
2831
/** Server-side hard cap on rows per identity import request. */
2932
export const IDENTITY_IMPORT_BATCH_SIZE = 500;
3033

31-
export type IdentityPasswordPolicy = 'none' | 'invite' | 'temporary';
34+
export type IdentityPasswordPolicy = 'auto' | 'none' | 'invite' | 'temporary';
3235

3336
/** Row results from the identity endpoint may carry a one-time password. */
3437
export type IdentityImportRowResult = ImportRowResult & {

packages/i18n/src/locales/en.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,14 +1420,16 @@ const en = {
14201420
identityImport: {
14211421
policyTitle: 'Sign-in setup for imported users',
14221422
policy: {
1423-
none: 'No password (recommended)',
1423+
auto: 'Automatic (recommended)',
14241424
invite: 'Send invitations',
14251425
temporary: 'Temporary passwords',
1426+
none: 'No password (identity only)',
14261427
},
14271428
policyHint: {
1429+
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.',
1430+
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.',
1431+
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.',
14281432
none: 'Users first sign in with a phone OTP, magic link, or password-reset link, then set their own password.',
1429-
invite: 'Each created user gets a set-your-password email (or an invitation SMS for phone-only rows). Requires a configured email/SMS service.',
1430-
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.',
14311433
},
14321434
passwordsNote: 'Temporary passwords — shown once, never stored. Save them now; each user must change theirs at first sign-in.',
14331435
passwordsMore: 'More entries omitted — use the download.',

packages/i18n/src/locales/zh.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,14 +1492,16 @@ const zh = {
14921492
identityImport: {
14931493
policyTitle: '导入用户的登录方式',
14941494
policy: {
1495-
none: '不设密码(推荐)',
1495+
auto: '自动(推荐)',
14961496
invite: '发送邀请',
14971497
temporary: '临时密码',
1498+
none: '不设密码(仅身份)',
14981499
},
14991500
policyHint: {
1500-
none: '用户通过手机验证码、魔法链接或重置链接首次登录,之后自行设置密码。',
1501-
invite: '为每个新建用户发送"设置密码"邮件(仅手机号的行发送邀请短信)。需要已配置邮件/短信服务。',
1501+
auto: '可触达的用户收到邀请(邮件或短信);无法触达的行生成一次性密码,仅在结果页显示一次。无论是否配置邮件/短信服务都可用。',
1502+
invite: '为每个新建用户发送"设置密码"邮件(仅手机号的行发送邀请短信)。需要已配置邮件/短信服务——无法触达的行会失败。',
15021503
temporary: '适用于未配置邮件/短信的部署:为每个新建用户生成一次性密码,仅在结果页显示一次,首次登录强制修改。',
1504+
none: '用户通过手机验证码、魔法链接或重置链接首次登录,之后自行设置密码。',
15031505
},
15041506
passwordsNote: '临时密码——仅显示一次,不会被保存。请立即保存;用户首次登录时必须修改。',
15051507
passwordsMore: '更多条目已省略——请使用下载。',

0 commit comments

Comments
 (0)