Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/import-wizard-auto-policy.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 9 additions & 5 deletions packages/app-shell/src/views/IdentityImportPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,21 @@ import {
} from './identityImport';

const POLICY_FALLBACKS: Record<IdentityPasswordPolicy, { label: string; hint: string }> = {
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.',
},
};

Expand Down
4 changes: 2 additions & 2 deletions packages/app-shell/src/views/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<IdentityPasswordPolicy>('none');
const identityPolicyRef = useRef<IdentityPasswordPolicy>('none');
const [identityPasswordPolicy, setIdentityPasswordPolicy] = useState<IdentityPasswordPolicy>('auto');
const identityPolicyRef = useRef<IdentityPasswordPolicy>('auto');
identityPolicyRef.current = identityPasswordPolicy;
const identityDataSource = useMemo(
() => (identityImportEnabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('mergeIdentityBatchResults', () => {
});

describe('createIdentityImportDataSource', () => {
const makeAdapter = (fetchImpl: ReturnType<typeof vi.fn>, policy: 'none' | 'invite' | 'temporary' = 'none') =>
const makeAdapter = (fetchImpl: ReturnType<typeof vi.fn>, policy: 'auto' | 'none' | 'invite' | 'temporary' = 'auto') =>
createIdentityImportDataSource({
base: { find: 'passthrough-marker', createImportJob: () => {}, undoImportJob: () => {} },
authFetch: fetchImpl as any,
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 9 additions & 6 deletions packages/app-shell/src/views/identityImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 & {
Expand Down
8 changes: 5 additions & 3 deletions packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
8 changes: 5 additions & 3 deletions packages/i18n/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1487,14 +1487,16 @@ const zh = {
identityImport: {
policyTitle: '导入用户的登录方式',
policy: {
none: '不设密码(推荐)',
auto: '自动(推荐)',
invite: '发送邀请',
temporary: '临时密码',
none: '不设密码(仅身份)',
},
policyHint: {
none: '用户通过手机验证码、魔法链接或重置链接首次登录,之后自行设置密码。',
invite: '为每个新建用户发送"设置密码"邮件(仅手机号的行发送邀请短信)。需要已配置邮件/短信服务。',
auto: '可触达的用户收到邀请(邮件或短信);无法触达的行生成一次性密码,仅在结果页显示一次。无论是否配置邮件/短信服务都可用。',
invite: '为每个新建用户发送"设置密码"邮件(仅手机号的行发送邀请短信)。需要已配置邮件/短信服务——无法触达的行会失败。',
temporary: '适用于未配置邮件/短信的部署:为每个新建用户生成一次性密码,仅在结果页显示一次,首次登录强制修改。',
none: '用户通过手机验证码、魔法链接或重置链接首次登录,之后自行设置密码。',
},
passwordsNote: '临时密码——仅显示一次,不会被保存。请立即保存;用户首次登录时必须修改。',
passwordsMore: '更多条目已省略——请使用下载。',
Expand Down
Loading