Skip to content

Commit b2e809f

Browse files
os-zhuangclaude
andauthored
docs(auth): 手机号登录、管理员用户管理与批量导入的文档更新 (#2811)
* docs(auth): document phone sign-in, admin user management, and bulk user import Catches content/docs/permissions/authentication.mdx up with the shipped auth surface (#2766 / PR #2771, SMS follow-up #2780 / PR #2797): - Phone-Number Sign-In section: plugins.phoneNumber flag (and the CLI OS_AUTH_PHONE_NUMBER_ENABLED override), /sign-in/phone-number, the OTP flows' dependency on a wired SMS service (rate-limited), and the placeholder-email semantics for phone-only accounts. - Admin User Management section: /admin/create-user (one-time temporary password + mustChangePassword → 403 PASSWORD_EXPIRED gate), /admin/set-user-password, /admin/import-users (invite/temporary policies incl. the SMS-invite path, upsert profile-field-only rules, 500-row sync cap, no undo). - API endpoint reference: new Phone Number and Admin User Management subsections. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o * docs(auth): baseline better-auth 'role' boundary refs + regen reference docs check-role-word (ADR-0090 D3) flagged the two new mentions in authentication.mdx — both refer to better-auth's literal `role` scalar (a genuine boundary), so the file is baselined rather than reworded. Also picks up the generated reference docs that lag the merged spec: features.phoneNumber/phoneNumberOtp and the auth-config phoneNumber flag (#2766/#2780), plus component.mdx's relationshipValueField row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7254bd0 commit b2e809f

5 files changed

Lines changed: 131 additions & 0 deletions

File tree

content/docs/permissions/authentication.mdx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,59 @@ const response = await fetch('http://localhost:3000/api/v1/auth/magic-link/send'
571571
const response = await fetch(`http://localhost:3000/api/v1/auth/magic-link/verify?token=${token}`);
572572
```
573573

574+
### Phone-Number Sign-In
575+
576+
Enable phone numbers as a first-class sign-in identifier (better-auth
577+
`phone-number` plugin). Opt-in — off by default:
578+
579+
```typescript
580+
new AuthPlugin({
581+
secret: process.env.OS_AUTH_SECRET,
582+
baseUrl: 'http://localhost:3000',
583+
plugins: {
584+
phoneNumber: true, // Enable phone sign-in
585+
}
586+
})
587+
```
588+
589+
With the CLI `serve` command, `OS_AUTH_PHONE_NUMBER_ENABLED=true` flips the
590+
same flag. The plugin adds `sys_user.phone_number` (unique) and
591+
`sys_user.phone_number_verified`; the login UI discovers the capability via
592+
`features.phoneNumber` on `GET /api/v1/auth/config`.
593+
594+
#### Sign In with Phone + Password
595+
596+
```typescript
597+
const response = await fetch('http://localhost:3000/api/v1/auth/sign-in/phone-number', {
598+
method: 'POST',
599+
headers: { 'Content-Type': 'application/json' },
600+
body: JSON.stringify({
601+
phoneNumber: '+8613800000000',
602+
password: 'password123'
603+
})
604+
});
605+
```
606+
607+
#### OTP Flows (Require an SMS Service)
608+
609+
The OTP surfaces — `POST /phone-number/send-otp` + `POST /phone-number/verify`
610+
(sign-in / verification) and `POST /phone-number/request-password-reset` +
611+
`POST /phone-number/reset-password` (self-service reset) — only work when an
612+
SMS service is wired (`@objectstack/service-sms` registers the `sms` kernel
613+
service). Without one, `send-otp` fails loudly instead of silently dropping
614+
the code; phone sign-in is then password-based only (an admin sets the
615+
password — see Admin User Management below). OTP requests are rate-limited
616+
per phone number to prevent SMS-pumping abuse.
617+
618+
#### Placeholder Emails for Phone-Only Accounts
619+
620+
better-auth requires a unique email per user, so accounts created with only a
621+
phone number get a generated placeholder address of the form
622+
`u-<random>@placeholder.invalid`. These are never deliverable (`.invalid` is
623+
an RFC 2606 reserved TLD), never derived from the phone number (so exports
624+
and logs can't leak it), and every auth mail callback (password reset,
625+
invitation, magic link) refuses them with a `PLACEHOLDER_EMAIL` error.
626+
574627
### Organizations (Multi-Tenant)
575628

576629
Enable organization/team support:
@@ -585,6 +638,64 @@ new AuthPlugin({
585638
})
586639
```
587640

641+
### Admin User Management
642+
643+
With `plugins: { admin: true }` (forced on when SCIM is enabled), platform
644+
admins get email-independent account management on the Users list — the
645+
`create_user` / `set_user_password` actions there drive these endpoints. All
646+
three routes are gated server-side on the platform-admin signals
647+
(`isPlatformAdmin`, the `platform_admin` position, or the legacy `role`
648+
scalar) and drive the better-auth pipeline, so created accounts get a real
649+
scrypt-hashed credential and can sign in immediately.
650+
651+
#### Create a User Directly
652+
653+
```typescript
654+
const response = await fetch('http://localhost:3000/api/v1/auth/admin/create-user', {
655+
method: 'POST',
656+
headers: { 'Content-Type': 'application/json' },
657+
body: JSON.stringify({
658+
email: 'new.user@example.com', // and/or phoneNumber (requires plugins.phoneNumber)
659+
name: 'New User',
660+
generatePassword: true, // or pass an explicit `password`
661+
mustChangePassword: true // default true
662+
})
663+
});
664+
// → { success, data: { user, temporaryPassword? } }
665+
```
666+
667+
A generated temporary password is returned **once** in the response (the
668+
Users-list action reveals it in a one-shot dialog) and is never persisted or
669+
logged. While `mustChangePassword` is set, the user is blocked with
670+
`403 PASSWORD_EXPIRED` on every protected route until they complete a
671+
password change — the same gate and Console redirect as password expiry.
672+
673+
#### Set / Reset a Password
674+
675+
`POST /api/v1/auth/admin/set-user-password` accepts
676+
`{ userId, newPassword }` or `{ userId, generatePassword: true }` plus the
677+
same `mustChangePassword` flag. It also provisions a credential account for
678+
SSO-onboarded users who never had a local password.
679+
680+
#### Bulk Import Users
681+
682+
`POST /api/v1/auth/admin/import-users` accepts the same payload shapes as the
683+
generic data import (`rows[]` / `csv` / `xlsxBase64`, `dryRun`), but writes
684+
every row through better-auth so the accounts are login-capable:
685+
686+
- `passwordPolicy: 'invite'` — rows need a reachable identity: a real email
687+
(invitation goes out as a set-your-password email; requires an email
688+
service) or, for phone-only rows, a wired SMS service (invitation SMS +
689+
phone-OTP first sign-in).
690+
- `passwordPolicy: 'temporary'` — per-row generated temporary passwords,
691+
returned once in the response, with `mustChangePassword` enforced.
692+
- `mode: 'insert' | 'upsert'` with `matchBy: 'email' | 'phone'`. Upsert
693+
updates only touch profile fields (`name`, `phone_number`, `role`) — a
694+
re-imported file can never modify an existing user's email or reset their
695+
password.
696+
- Synchronous only, at most 500 rows per request (password hashing is
697+
CPU-bound); split larger files into batches. Imports are not undoable.
698+
588699
---
589700

590701
## Client Integration
@@ -696,6 +807,21 @@ All endpoints are available under `/api/v1/auth/*`:
696807
- `POST /api/v1/auth/magic-link/send` - Send magic link email
697808
- `GET /api/v1/auth/magic-link/verify` - Verify magic link
698809

810+
#### Phone Number (requires `plugins.phoneNumber`)
811+
812+
- `POST /api/v1/auth/sign-in/phone-number` - Sign in with phone + password
813+
- `POST /api/v1/auth/phone-number/send-otp` - Send a sign-in/verification OTP (requires an SMS service; rate-limited)
814+
- `POST /api/v1/auth/phone-number/verify` - Verify an OTP
815+
- `POST /api/v1/auth/phone-number/request-password-reset` - Request an OTP-based password reset (requires an SMS service)
816+
- `POST /api/v1/auth/phone-number/reset-password` - Complete an OTP-based password reset
817+
818+
#### Admin User Management (requires `plugins.admin`; platform-admin gated)
819+
820+
- `POST /api/v1/auth/admin/create-user` - Create a login-capable account (optional one-time temporary password + forced change)
821+
- `POST /api/v1/auth/admin/set-user-password` - Set/reset a user's password (also provisions a credential for SSO-onboarded users)
822+
- `POST /api/v1/auth/admin/import-users` - Bulk import users (CSV/JSON/XLSX; invite or temporary password policy; ≤500 rows, dry-run supported)
823+
- `POST /api/v1/auth/admin/unlock-user` - Clear a brute-force lockout early
824+
699825
For complete API documentation, see the [Better-Auth API Reference](https://www.better-auth.com/docs).
700826

701827
---

content/docs/references/api/auth-endpoints.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ const result = AuthEndpoint.parse(data);
6262
| **magicLink** | `boolean` || Magic link login enabled |
6363
| **organization** | `boolean` || Multi-tenant organization support enabled |
6464
| **ssoEnforced** | `boolean` | optional | SSO-only login enforced: the UI hides the local password form + self-registration (a break-glass "use a password" link remains) |
65+
| **phoneNumber** | `boolean` | optional | Phone-number sign-in enabled (phone + password, #2766 V1.5) |
66+
| **phoneNumberOtp** | `boolean` | optional | Phone-number OTP sign-in and self-service password reset available — requires the phoneNumber plugin plus a deliverable SMS service (#2780) |
6567

6668

6769
---

content/docs/references/system/auth-config.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ Advanced / low-level Better-Auth options
8383
| **dynamicClientRegistration** | `boolean` | optional | Allow unauthenticated RFC 7591 Dynamic Client Registration (default: follows OS_MCP_SERVER_ENABLED) |
8484
| **deviceAuthorization** | `boolean` || Enable RFC 8628 Device Authorization Grant (CLI / TV-style login) |
8585
| **admin** | `boolean` || Enable platform admin operations (ban/unban, set-password, impersonate, set-role) |
86+
| **phoneNumber** | `boolean` || Enable phone-number sign-in (phone + password; OTP sign-in/reset when an SMS service is configured) |
8687

8788

8889
---

content/docs/references/ui/component.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ Type: `string`
346346
| :--- | :--- | :--- | :--- |
347347
| **objectName** | `string` || Related object name (e.g., "task", "opportunity") |
348348
| **relationshipField** | `string` || Field on related object that points to this record (e.g., "account_id") |
349+
| **relationshipValueField** | `string` || Parent-record field whose value relationshipField stores (default 'id'; e.g. 'name' for name-keyed junctions). |
349350
| **columns** | `string[]` | optional | Fields to display in the related list. Optional: when omitted, columns derive from the related object's highlightFields / default list columns (a related list is just another surface that lists that object). Override chain: child highlightFields → field-level relatedListColumns → this inline list. |
350351
| **sort** | `string \| Object[]` | optional | Sort order for related records |
351352
| **limit** | `integer` || Number of records to display initially |

scripts/role-word-baseline.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"content/docs/kernel/runtime-services/examples.mdx": 2,
2121
"content/docs/kernel/runtime-services/sharing-service.mdx": 2,
2222
"content/docs/kernel/services-checklist.mdx": 5,
23+
"content/docs/permissions/authentication.mdx": 2,
2324
"content/docs/permissions/authorization.mdx": 3,
2425
"content/docs/permissions/index.mdx": 1,
2526
"content/docs/permissions/permission-metadata.mdx": 2,

0 commit comments

Comments
 (0)