diff --git a/content/docs/permissions/authentication.mdx b/content/docs/permissions/authentication.mdx index 5248236f42..e87c422b46 100644 --- a/content/docs/permissions/authentication.mdx +++ b/content/docs/permissions/authentication.mdx @@ -488,6 +488,59 @@ const response = await fetch('http://localhost:3000/api/v1/auth/magic-link/send' const response = await fetch(`http://localhost:3000/api/v1/auth/magic-link/verify?token=${token}`); ``` +### Phone-Number Sign-In + +Enable phone numbers as a first-class sign-in identifier (better-auth +`phone-number` plugin). Opt-in — off by default: + +```typescript +new AuthPlugin({ + secret: process.env.OS_AUTH_SECRET, + baseUrl: 'http://localhost:3000', + plugins: { + phoneNumber: true, // Enable phone sign-in + } +}) +``` + +With the CLI `serve` command, `OS_AUTH_PHONE_NUMBER_ENABLED=true` flips the +same flag. The plugin adds `sys_user.phone_number` (unique) and +`sys_user.phone_number_verified`; the login UI discovers the capability via +`features.phoneNumber` on `GET /api/v1/auth/config`. + +#### Sign In with Phone + Password + +```typescript +const response = await fetch('http://localhost:3000/api/v1/auth/sign-in/phone-number', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + phoneNumber: '+8613800000000', + password: 'password123' + }) +}); +``` + +#### OTP Flows (Require an SMS Service) + +The OTP surfaces — `POST /phone-number/send-otp` + `POST /phone-number/verify` +(sign-in / verification) and `POST /phone-number/request-password-reset` + +`POST /phone-number/reset-password` (self-service reset) — only work when an +SMS service is wired (`@objectstack/service-sms` registers the `sms` kernel +service). Without one, `send-otp` fails loudly instead of silently dropping +the code; phone sign-in is then password-based only (an admin sets the +password — see Admin User Management below). OTP requests are rate-limited +per phone number to prevent SMS-pumping abuse. + +#### Placeholder Emails for Phone-Only Accounts + +better-auth requires a unique email per user, so accounts created with only a +phone number get a generated placeholder address of the form +`u-@placeholder.invalid`. These are never deliverable (`.invalid` is +an RFC 2606 reserved TLD), never derived from the phone number (so exports +and logs can't leak it), and every auth mail callback (password reset, +invitation, magic link) refuses them with a `PLACEHOLDER_EMAIL` error. + ### Organizations (Multi-Tenant) Enable organization/team support: @@ -502,6 +555,64 @@ new AuthPlugin({ }) ``` +### Admin User Management + +With `plugins: { admin: true }` (forced on when SCIM is enabled), platform +admins get email-independent account management on the Users list — the +`create_user` / `set_user_password` actions there drive these endpoints. All +three routes are gated server-side on the platform-admin signals +(`isPlatformAdmin`, the `platform_admin` position, or the legacy `role` +scalar) and drive the better-auth pipeline, so created accounts get a real +scrypt-hashed credential and can sign in immediately. + +#### Create a User Directly + +```typescript +const response = await fetch('http://localhost:3000/api/v1/auth/admin/create-user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'new.user@example.com', // and/or phoneNumber (requires plugins.phoneNumber) + name: 'New User', + generatePassword: true, // or pass an explicit `password` + mustChangePassword: true // default true + }) +}); +// → { success, data: { user, temporaryPassword? } } +``` + +A generated temporary password is returned **once** in the response (the +Users-list action reveals it in a one-shot dialog) and is never persisted or +logged. While `mustChangePassword` is set, the user is blocked with +`403 PASSWORD_EXPIRED` on every protected route until they complete a +password change — the same gate and Console redirect as password expiry. + +#### Set / Reset a Password + +`POST /api/v1/auth/admin/set-user-password` accepts +`{ userId, newPassword }` or `{ userId, generatePassword: true }` plus the +same `mustChangePassword` flag. It also provisions a credential account for +SSO-onboarded users who never had a local password. + +#### Bulk Import Users + +`POST /api/v1/auth/admin/import-users` accepts the same payload shapes as the +generic data import (`rows[]` / `csv` / `xlsxBase64`, `dryRun`), but writes +every row through better-auth so the accounts are login-capable: + +- `passwordPolicy: 'invite'` — rows need a reachable identity: a real email + (invitation goes out as a set-your-password email; requires an email + service) or, for phone-only rows, a wired SMS service (invitation SMS + + phone-OTP first sign-in). +- `passwordPolicy: 'temporary'` — per-row generated temporary passwords, + returned once in the response, with `mustChangePassword` enforced. +- `mode: 'insert' | 'upsert'` with `matchBy: 'email' | 'phone'`. Upsert + updates only touch profile fields (`name`, `phone_number`, `role`) — a + re-imported file can never modify an existing user's email or reset their + password. +- Synchronous only, at most 500 rows per request (password hashing is + CPU-bound); split larger files into batches. Imports are not undoable. + --- ## Client Integration @@ -613,6 +724,21 @@ All endpoints are available under `/api/v1/auth/*`: - `POST /api/v1/auth/magic-link/send` - Send magic link email - `GET /api/v1/auth/magic-link/verify` - Verify magic link +#### Phone Number (requires `plugins.phoneNumber`) + +- `POST /api/v1/auth/sign-in/phone-number` - Sign in with phone + password +- `POST /api/v1/auth/phone-number/send-otp` - Send a sign-in/verification OTP (requires an SMS service; rate-limited) +- `POST /api/v1/auth/phone-number/verify` - Verify an OTP +- `POST /api/v1/auth/phone-number/request-password-reset` - Request an OTP-based password reset (requires an SMS service) +- `POST /api/v1/auth/phone-number/reset-password` - Complete an OTP-based password reset + +#### Admin User Management (requires `plugins.admin`; platform-admin gated) + +- `POST /api/v1/auth/admin/create-user` - Create a login-capable account (optional one-time temporary password + forced change) +- `POST /api/v1/auth/admin/set-user-password` - Set/reset a user's password (also provisions a credential for SSO-onboarded users) +- `POST /api/v1/auth/admin/import-users` - Bulk import users (CSV/JSON/XLSX; invite or temporary password policy; ≤500 rows, dry-run supported) +- `POST /api/v1/auth/admin/unlock-user` - Clear a brute-force lockout early + For complete API documentation, see the [Better-Auth API Reference](https://www.better-auth.com/docs). --- diff --git a/content/docs/references/api/auth-endpoints.mdx b/content/docs/references/api/auth-endpoints.mdx index f51d92ffbd..128e5e54c8 100644 --- a/content/docs/references/api/auth-endpoints.mdx +++ b/content/docs/references/api/auth-endpoints.mdx @@ -62,6 +62,8 @@ const result = AuthEndpoint.parse(data); | **magicLink** | `boolean` | ✅ | Magic link login enabled | | **organization** | `boolean` | ✅ | Multi-tenant organization support enabled | | **ssoEnforced** | `boolean` | optional | SSO-only login enforced: the UI hides the local password form + self-registration (a break-glass "use a password" link remains) | +| **phoneNumber** | `boolean` | optional | Phone-number sign-in enabled (phone + password, #2766 V1.5) | +| **phoneNumberOtp** | `boolean` | optional | Phone-number OTP sign-in and self-service password reset available — requires the phoneNumber plugin plus a deliverable SMS service (#2780) | --- diff --git a/content/docs/references/system/auth-config.mdx b/content/docs/references/system/auth-config.mdx index cafdc821ae..86e69b466c 100644 --- a/content/docs/references/system/auth-config.mdx +++ b/content/docs/references/system/auth-config.mdx @@ -83,6 +83,7 @@ Advanced / low-level Better-Auth options | **dynamicClientRegistration** | `boolean` | optional | Allow unauthenticated RFC 7591 Dynamic Client Registration (default: follows OS_MCP_SERVER_ENABLED) | | **deviceAuthorization** | `boolean` | ✅ | Enable RFC 8628 Device Authorization Grant (CLI / TV-style login) | | **admin** | `boolean` | ✅ | Enable platform admin operations (ban/unban, set-password, impersonate, set-role) | +| **phoneNumber** | `boolean` | ✅ | Enable phone-number sign-in (phone + password; OTP sign-in/reset when an SMS service is configured) | --- diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 7b5f0f053c..f859646173 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -346,6 +346,7 @@ Type: `string` | :--- | :--- | :--- | :--- | | **objectName** | `string` | ✅ | Related object name (e.g., "task", "opportunity") | | **relationshipField** | `string` | ✅ | Field on related object that points to this record (e.g., "account_id") | +| **relationshipValueField** | `string` | ✅ | Parent-record field whose value relationshipField stores (default 'id'; e.g. 'name' for name-keyed junctions). | | **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. | | **sort** | `string \| Object[]` | optional | Sort order for related records | | **limit** | `integer` | ✅ | Number of records to display initially | diff --git a/scripts/role-word-baseline.json b/scripts/role-word-baseline.json index f62b0dc576..4b7a634826 100644 --- a/scripts/role-word-baseline.json +++ b/scripts/role-word-baseline.json @@ -20,6 +20,7 @@ "content/docs/kernel/runtime-services/examples.mdx": 2, "content/docs/kernel/runtime-services/sharing-service.mdx": 2, "content/docs/kernel/services-checklist.mdx": 5, + "content/docs/permissions/authentication.mdx": 2, "content/docs/permissions/authorization.mdx": 3, "content/docs/permissions/index.mdx": 1, "content/docs/permissions/permission-metadata.mdx": 2,