You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: document registration policy hook and email-change endpoints
Document the OnPreUserRegister hook (Go embedding + JS plugin), the opt-in
built-in RegistrationPolicy with AUTH_REG_* config, and the new
/api/auth/local/email/change and /api/auth/local/email/confirm endpoints.
Claude-Session: https://claude.ai/code/session_014noPgZC7LHPJ376yMNMxH8
Starts an authenticated email change. The request re-authenticates the caller with the current password, validates the new address (format, [registration policy](#registration-policy), and uniqueness), and creates a single-use pending change. It then sends two emails: a notification to the **current** address and a confirmation link to the **new** address. The account email is **not** changed until the new address is confirmed.
168
+
169
+
Email change is only available for **local (password) accounts**.
170
+
171
+
**Request:**
172
+
```http
173
+
POST /api/auth/local/email/change HTTP/1.1
174
+
Content-Type: application/json
175
+
Authorization: Bearer <access_token>
176
+
177
+
{
178
+
"new_email": "new@example.com",
179
+
"current_password": "your_current_password"
180
+
}
181
+
```
182
+
183
+
**Response:**
184
+
```json
185
+
{
186
+
"message": "A confirmation link has been sent to your new email address"
187
+
}
188
+
```
189
+
190
+
#### Confirm Email Change
191
+
192
+
Confirms a pending email change using the single-use, time-limited (24 hours) token delivered to the new address. On success, the account email is committed to the new value.
193
+
194
+
**Request:**
195
+
```http
196
+
POST /api/auth/local/email/confirm HTTP/1.1
197
+
Content-Type: application/json
198
+
199
+
{
200
+
"token": "<token_from_new_email>"
201
+
}
202
+
```
203
+
204
+
**Response:**
205
+
```json
206
+
{
207
+
"message": "Your email address has been updated"
208
+
}
209
+
```
210
+
211
+
::: tip Confirmation link
212
+
The confirmation link base defaults to `<base_url>/auth/local/email/confirm`. To point it at a custom page (for example, a dashboard route that calls the confirm endpoint), set `email_change_url` in the local provider configuration.
FastSchema can apply an opt-in, built-in validation policy to **self-service signups**. The policy runs on **both** local email/password registration and OAuth/social signup, just before the user row is created. It does **not** apply to admin-created users (content API). By default (no policy configured) nothing is blocked.
334
+
335
+
The policy supports:
336
+
337
+
| Field | Description |
338
+
|---|---|
339
+
|`allowed_email_domains`| Allowlist. When non-empty, only emails on these domains may register. |
340
+
|`blocked_email_domains`| Denylist of email domains to reject. |
341
+
|`reserved_usernames`| Usernames that may not be registered (e.g. `admin`, `root`, `system`). |
342
+
|`normalize_email`| Lowercases the email domain and converts IDN domains to punycode. |
343
+
344
+
### Configure via environment variables
345
+
346
+
Each variable is a comma-separated list. Setting **any** of them enables the policy with `normalize_email` turned on.
When embedding FastSchema as a Go framework, set the policy on `AuthConfig`:
362
+
363
+
```go
364
+
app, _:= fastschema.New(&fs.Config{
365
+
AuthConfig: &fs.AuthConfig{
366
+
Registration: &fs.RegistrationPolicy{
367
+
AllowedEmailDomains: []string{"example.com"},
368
+
ReservedUsernames: []string{"admin", "root"},
369
+
NormalizeEmail: true,
370
+
},
371
+
},
372
+
})
373
+
```
374
+
375
+
The built-in policy runs as the **first**`OnPreUserRegister` hook, so any custom hooks you register run afterwards on the already-normalized input.
376
+
377
+
### Custom signup rules (`OnPreUserRegister`)
378
+
379
+
For anything beyond the basics above — invite-only gating, blocking disposable-email or free-webmail domains, custom username rules — register an `OnPreUserRegister` hook. The hook fires for both local and OAuth signups, may mutate `Email`/`Username` (for example, to normalize them), and returning an error rejects the registration.
380
+
381
+
```go
382
+
app.OnPreUserRegister(func(ctx context.Context, in *fs.RegistrationInput) error {
383
+
// Block a disposable-email domain
384
+
if strings.HasSuffix(strings.ToLower(in.Email), "@tempmail.example") {
385
+
return errors.BadRequest("Disposable email addresses are not allowed")
|`Provider`|`string`|`local` or the OAuth provider name. |
398
+
|`ProviderID`|`string`| OAuth provider subject id (empty for local). |
399
+
|`Profile`|`map[string]any`| Raw provider profile (OAuth only). |
400
+
|`IsOAuth`|`bool`|`true` for social-login registration. |
401
+
402
+
See the [Hooks](/docs/framework/hooks/#onpreuserregister) reference for more detail, or [Plugin Configuration](/docs/plugins/configuration#add-hooks) to register the hook from a JS plugin.
403
+
404
+
---
405
+
281
406
## Token Usage
282
407
283
408
Clients must send the access token in every authenticated request. Two methods:
@@ -311,6 +436,9 @@ Cookie: token=<access_token>
311
436
|`AUTH_OTP_LENGTH`|`6`| OTP code length |
312
437
|`AUTH_OTP_EXPIRATION`|`300`| OTP expiration in seconds (5 minutes) |
313
438
|`AUTH_OTP_MAX_ATTEMPTS`|`3`| Max verification attempts before OTP expires |
439
+
|`AUTH_REG_ALLOWED_DOMAINS`|_(empty)_| Comma-separated allowlist of email domains for signup (see [Registration Policy](#registration-policy)) |
440
+
|`AUTH_REG_BLOCKED_DOMAINS`|_(empty)_| Comma-separated denylist of email domains for signup |
441
+
|`AUTH_REG_RESERVED_USERNAMES`|_(empty)_| Comma-separated list of usernames that cannot be registered |
The optional `registration` policy validates self-service signups for both local and OAuth providers. It can also be configured with the `AUTH_REG_ALLOWED_DOMAINS`, `AUTH_REG_BLOCKED_DOMAINS`, and `AUTH_REG_RESERVED_USERNAMES` environment variables. See [Registration Policy](/docs/backend/authentication#registration-policy) for details.
92
+
93
+
The `local` provider also accepts an `email_change_url` key, used as the base for the confirmation link sent when a user changes their email address (defaults to `<base_url>/auth/local/email/confirm`). See [Change Email](/docs/backend/authentication#change-email).
94
+
81
95
For example, to enable the `github` and `google` providers:
82
96
83
97
```json
@@ -90,7 +104,8 @@ For example, to enable the `github` and `google` providers:
The `OnPreUserRegister` hook is executed just before a **self-service** user is created. It fires for both local (email/password) registration and OAuth/social signup. It does **not** fire for admin-created users.
133
+
134
+
This is the extension point for custom signup rules such as invite-only gating, or blocking disposable-email / free-webmail domains.
135
+
136
+
**There are two possible behaviors for the `OnPreUserRegister` hook:**
137
+
138
+
- `Return nil` to allow the registration to continue.
139
+
- `Return an error` to reject the registration before the user row is created.
140
+
141
+
The hook receives a pointer to `RegistrationInput` and **may mutate** `Email`/`Username` (for example, to normalize them); the mutated values are applied to the persisted user.
142
+
143
+
::: tip Built-in registration policy
144
+
The opt-in [Registration Policy](/docs/backend/authentication#registration-policy) (allow/block email domains, reserved usernames, email normalization) runs as the **first** `OnPreUserRegister` hook, so your custom hooks run afterwards on the already-normalized input.
145
+
:::
146
+
147
+
**Example:**
148
+
149
+
```go{5-14}
150
+
app, _:= fastschema.New(&fs.Config{
151
+
SystemSchemas: []any{Tag{}, Blog{}},
152
+
})
153
+
154
+
app.OnPreUserRegister(func(ctx context.Context, in *fs.RegistrationInput) error {
155
+
// Invite-only: reject any email that has not been invited
156
+
if !isInvited(in.Email) {
157
+
return errors.BadRequest("Registration is invite-only")
// Runs before a self-service user is created (local + OAuth).
187
+
// Throw to reject registration. Does NOT fire for admin-created users.
188
+
exporttype_FsPreUserRegister= (
189
+
ctx:FsContext,
190
+
input:FsRegistrationInput,
191
+
) =>Promise<void> |void;
192
+
173
193
exporttype_FsPreDBQueryHook= (
174
194
ctx:FsContext,
175
195
option:FsQueryOption,
@@ -235,3 +255,22 @@ export type _FsPostDBDeleteHook = (
235
255
affected:number,
236
256
) =>Promise<void> |void;
237
257
```
258
+
259
+
## Pre user register hook
260
+
261
+
The `OnPreUserRegister` hook runs before a self-service user is created, for both local and OAuth signups. Throw to reject the registration; you may also mutate `input.email` / `input.username` (for example, to normalize them). It does **not** fire for admin-created users.
262
+
263
+
This is the place to implement custom signup rules such as invite-only gating or blocking disposable-email domains. See the [Registration Policy](/docs/backend/authentication#registration-policy) for the built-in basics (allow/block domains, reserved usernames, email normalization).
264
+
265
+
```js [plugin.js]
266
+
consthookPreUserRegister= (ctx, input) => {
267
+
// Block a disposable-email domain
268
+
if (input.email.toLowerCase().endsWith('@tempmail.example')) {
269
+
thrownewError('Disposable email addresses are not allowed');
0 commit comments