Repository navigation
feat(auth): 管理员直建用户 + 手机号登录 + sys_user 批量导入 —— 分阶段实施(#2758 重排) #2766
Copy link
Copy link
Closed
Labels
Description
背景
#2758 提出 sys_user 批量导入。经评估重排为三阶段:先补"管理员后台直接创建/管理用户"这一更根本的产品缺口(今天 Console 上 sys_user 没有任何不依赖邮件的建号入口),再接手机号登录(无邮箱员工),最后做批量导入——此时导入只是"建号原语 × 现有导入框架"的组装。
已拍板决策:
- 手机号只做「phoneNumber 插件 + 手机号+密码登录」;短信 OTP/SMS 基建另立 issue
- 导入密码策略 = 1(邀请/重置邮件)+ 2(临时密码+强制改密);策略 3(预哈希迁移)不做,另立 issue
- 独立身份导入端点,不放开 sys_user 通用 import affordance(ObjectQL 直写会绕过 better-auth 哈希、不生成 sys_account 凭证)
- 导入禁 undo(撤销=批量删用户,不可逆风险过大)
- feat(auth): 支持用户名/工号登录 —— 接入 better-auth username 插件 #2756(username 登录)从前置依赖降级为正交需求,不阻塞本计划
跨阶段安全红线:
- 密码/临时密码绝不进日志(含 console.warn)、审计 payload、sys_import_job 结果采样、任何持久化字段;只允许出现在 HTTP 响应体(一次性)或邮件正文
- 手机号明文绝不进占位邮箱(sys_user exportCsv 开启,会随导出扩散)
- 占位邮箱绝不作为真实收件人(邮件回调前置拦截)
V1 — 管理员直建用户 + 强制改密标记
POST /api/v1/auth/admin/create-user:email/name/role +password或generatePassword,mustChangePassword默认 true;内部走auth.api.createUser()(保证 scrypt 哈希 + sys_account 生成)POST /api/v1/auth/admin/set-user-password:包装 better-auth set-password,附加打戳/生成临时密码/显式审计sys_user.must_change_password字段 +computeAuthGate()新分支(复用PASSWORD_EXPIREDcode,白嫖现有 403 拦截与 Console 改密跳转,ADR-0069 机制);改密成功清标记create_useraction(list_toolbar,纯 schema,resultDialog一次性展示临时密码);现有set_user_passwordaction 改指包装端点- 权限:ADR-0068 平台管理员 gate;显式审计
user.admin_created/user.admin_password_set
V1.5 — phoneNumber 插件 + 占位邮箱(与 V1 可并行)
- better-auth
phoneNumber插件接入(enabled.phoneNumber默认关,opt-in);sendOTP显式抛 NOT_SUPPORTED 封死 OTP 路径;登录走原生/sign-in/phone-number(手机号+密码) sys_user加phone_number(可空 unique)/phone_number_verified列 +auth-schema-config.ts映射- 占位邮箱:
u-<不透明token>@placeholder.invalid(RFC 2606 保留 TLD,物理不可投递;绝不含手机号子串);create-user无 email 时自动生成 - 所有邮件回调对占位邮箱前置拦截(显式错误码,不静默);顺手收敛未配邮件时 console.warn 打印完整 reset/magic 链接的泄露面
features.phoneNumber暴露到/api/v1/auth/config
V2 — 批量导入端点
- 前置重构:
prepareImportRequest(rest-server.ts 内 module-private)抽取为packages/rest/src/import-prepare.ts并导出(CSV/xlsx 解析复用,行为零变化) POST /api/v1/auth/admin/import-users:mode: insert|upsert、matchBy: email|phone、passwordPolicy: invite|temporary、dryRun、async- 复用
runImport()+ 自写IdentityImportProtocol适配器:createData逐行auth.api.createUser+ 打戳 + 行级审计;updateData只更档案字段,upsert 更新行绝不重设密码 - invite 策略:无真实邮箱行(缺失/非法/占位邮箱)校验期报行错误
INVITE_REQUIRES_EMAIL;未配邮件服务整请求 400 - temporary 策略:同步路径临时密码仅响应体逐行一次性返回;异步 Job 强制已配邮件、密码只经邮件下发(Job 结果是持久化面,红线禁入)
- 阈值:同步 ≤500 行(scrypt 每行~百毫秒),大批量走
sys_import_job异步;Job 上限 ~10,000 - 显式审计(better-auth 写入不经 ObjectQL hook,plugin-audit 通用 writer 抓不到):行级
user.imported+ run 级user.import_run
影响面
packages/plugins/plugin-auth(端点/插件接线/authGate/占位邮箱)packages/platform-objects(sys_user 字段与 actions)packages/rest(prepareImportRequest 抽取)packages/spec(如 features flag 类型需要)
相关
- feat(auth): sys_user 批量导入接口 —— 导入用户的密码处理策略 #2758(原始需求,本 issue 为其实施重排)
- feat(auth): 支持用户名/工号登录 —— 接入 better-auth username 插件 #2756(username 登录,正交)
- feat(import): 产品级列表导入(L0–L4:批量/dryRun、Upsert 改记录、导入模板、异步 Job、校验审计) #2504 / 对象列表视图:新增「导出」+ 改进「导入」的校验与数据完整性 #2460(通用导入框架,后端已实现,本计划复用其 runImport/coerceRow/sys_import_job)
后续另立:策略 3 预哈希迁移、短信 OTP/SMS 基建、Console 导入向导 UI、档案字段经引擎 hook 委托放开 edit affordance 的评估。
Reactions are currently unavailable
Activity
chore: add changeset for #2766 V1/V1.5/V2
os-zhuang commentedon Jul 10, 2026
V1/V1.5/V2 已在 PR #2771 实现(CI 全绿,draft 待评审)。计划中标注的后续项已全部立项:
PR 合并后本 issue 的 V1–V2 范围即完成,后续项各自跟踪。
Generated by Claude Code
feat(auth): 管理员直建用户 + 手机号登录 + sys_user 批量导入(#2766 V1/V1.5/V2) (#2771)
feat(cli): OS_AUTH_PHONE_NUMBER_ENABLED switch for the phoneNumber pl…
refactor(sms): plugin-sms 更名为 service-sms + OTP 准入限流修复 + OS_AUTH_PHON…
docs(auth): baseline better-auth 'role' boundary refs + regen referen…
docs(auth): 手机号登录、管理员用户管理与批量导入的文档更新 (#2811)