@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
  5. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  6. package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
  7. package/dist/chunk-GVNLZZTN.js.map +1 -0
  8. package/dist/{chunk-ZOEPKBVF.js → chunk-SPQUX3C7.js} +1347 -428
  9. package/dist/chunk-SPQUX3C7.js.map +1 -0
  10. package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +515 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +964 -152
  16. package/dist/index.js +4279 -866
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +3 -3
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +651 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +276 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/facades.md +278 -0
  46. package/docs/hashing.md +118 -0
  47. package/docs/helpers.md +1281 -0
  48. package/docs/http-tests.md +386 -0
  49. package/docs/inertia.md +485 -0
  50. package/docs/installation.md +209 -0
  51. package/docs/lifecycle.md +234 -0
  52. package/docs/live-tests.md +226 -0
  53. package/docs/live.md +910 -0
  54. package/docs/logging.md +255 -0
  55. package/docs/mail.md +619 -0
  56. package/docs/middleware.md +384 -0
  57. package/docs/migrations.md +416 -0
  58. package/docs/mocking.md +264 -0
  59. package/docs/navigation.json +273 -0
  60. package/docs/notifications.md +225 -0
  61. package/docs/pagination.md +247 -0
  62. package/docs/passwords.md +207 -0
  63. package/docs/providers.md +187 -0
  64. package/docs/queries.md +600 -0
  65. package/docs/queues.md +330 -0
  66. package/docs/requests.md +533 -0
  67. package/docs/responses.md +444 -0
  68. package/docs/routing.md +738 -0
  69. package/docs/scheduling.md +277 -0
  70. package/docs/seeding.md +122 -0
  71. package/docs/session.md +409 -0
  72. package/docs/starter-kits.md +396 -0
  73. package/docs/storage.md +192 -0
  74. package/docs/structure.md +264 -0
  75. package/docs/testing.md +165 -0
  76. package/docs/time-travel.md +138 -0
  77. package/docs/validation.md +1792 -0
  78. package/docs/verification.md +152 -0
  79. package/docs/views.md +566 -0
  80. package/docs/vite.md +322 -0
  81. package/package.json +34 -32
  82. package/dist/chunk-I63ZFMX6.js.map +0 -1
  83. package/dist/chunk-MFTZQMZM.js.map +0 -1
  84. package/dist/chunk-POEP4B2M.js.map +0 -1
  85. package/dist/chunk-ZOEPKBVF.js.map +0 -1
  86. package/dist/index.d-BJyJx6U1.d.ts +0 -319
@@ -0,0 +1,487 @@
1
+ # Authentication
2
+
3
+ Sign your users in with a session in the browser, or with API tokens, and keep guests out of the routes that need a
4
+ user.
5
+
6
+ ## Introduction
7
+
8
+ Oriel's authentication is built from **guards** and **providers**. A guard decides how a request's user is found: the
9
+ `session` guard reads it from the session, and the `token` guard from an API token in the `Authorization` header. A
10
+ provider decides how users are read from storage: the `eloquent` provider reads them with your `User` model.
11
+
12
+ Both are set in `config/auth.ts`:
13
+
14
+ ```ts title="config/auth.ts"
15
+ import { env } from '@orieljs/framework'
16
+ import { User } from '../app/Models/User'
17
+
18
+ export default {
19
+ defaults: { guard: env('AUTH_GUARD', 'web'), passwords: env('AUTH_PASSWORD_BROKER', 'users') },
20
+ guards: {
21
+ web: { driver: 'session', provider: 'users' },
22
+ token: { driver: 'token', provider: 'users', expiration: null },
23
+ },
24
+ providers: { users: { driver: 'eloquent', model: User } },
25
+ passwords: { users: { provider: 'users', table: 'password_reset_tokens', expire: 60, throttle: 60 } },
26
+ password_timeout: 10800,
27
+ }
28
+ ```
29
+
30
+ The `web` guard is the default. The provider names the model class itself, which the config file imports.
31
+
32
+ ### Starter kits
33
+
34
+ Every [starter kit](starter-kits.md) comes with authentication: the kits with pages have registration, login, email
35
+ verification, password reset and the settings pages, and the API kit has its token endpoints. Their code is in your
36
+ app, so this page explains what it calls.
37
+
38
+ ### Database considerations
39
+
40
+ A new app's `User` model is built on `AuthenticatableModel`, which signs users in by their `password` column, keeps
41
+ "Remember me" tokens in `remember_token`, and uses `email` and `email_verified_at` for password resets and email
42
+ verification. A new app's `users` table has these columns, with a `password` column long enough for a hash.
43
+
44
+ ```ts title="app/Models/User.ts"
45
+ import { AuthenticatableModel, Authorizable, HasApiTokens, Model, Notifiable } from '@orieljs/framework'
46
+
47
+ export class User extends HasApiTokens(Notifiable(Authorizable(AuthenticatableModel(Model)))) {}
48
+ ```
49
+
50
+ > [!NOTE]
51
+ > Laravel's `User` model extends `Illuminate\Foundation\Auth\User`, and adds traits such as `HasApiTokens` and
52
+ > `Notifiable`. Oriel's builds the same abilities from mixins: `AuthenticatableModel` for signing in, `Authorizable`
53
+ > for [authorization](authorization.md), `Notifiable` for [notifications](notifications.md) and `HasApiTokens` for
54
+ > [API tokens](#api-tokens).
55
+
56
+ ## Authentication quickstart
57
+
58
+ ### Retrieving the authenticated user
59
+
60
+ `Auth.user()` returns the signed-in user, or `undefined` for a guest. It is async, since the guard may have to read
61
+ the session or the database the first time it is asked:
62
+
63
+ ```ts
64
+ const user = await Auth.user<User>()
65
+
66
+ const id = await Auth.id()
67
+ ```
68
+
69
+ `request().user()` does the same for the current request, through the request's guard:
70
+
71
+ ```ts title="app/Http/Controllers/FlightController.ts"
72
+ import { request } from '@orieljs/framework'
73
+ import type { User } from '../../Models/User'
74
+ import { Controller } from './Controller'
75
+
76
+ export class FlightController extends Controller {
77
+ /** Update the flight information for an existing flight. */
78
+ async update() {
79
+ const user = await request().user<User>()
80
+
81
+ // ...
82
+
83
+ return { updatedBy: user?.id }
84
+ }
85
+ }
86
+ ```
87
+
88
+ The type you give is a cast, not a check. On a route behind the `auth` middleware, a user is there, so you can write
89
+ `(await request().user<User>())!`.
90
+
91
+ > [!WARNING]
92
+ > `request().user()` returns a promise, so `await` it every time. TypeScript flags `if (request().user())`, but not
93
+ > `!request().user()` or `request().user() === undefined`, which are always `false`.
94
+
95
+ > [!NOTE]
96
+ > Laravel's `Auth::user()` and `$request->user()` return the user at once. Oriel's return a promise: `await` them.
97
+
98
+ #### Determining if the current user is authenticated
99
+
100
+ `Auth.check()` tells you whether a user is signed in, and `Auth.guest()` whether no one is. Both are async:
101
+
102
+ ```ts
103
+ if (await Auth.check()) {
104
+ // The user is logged in...
105
+ }
106
+ ```
107
+
108
+ To keep guests out of a route, use the `auth` middleware rather than checking in each action.
109
+
110
+ ### Protecting routes
111
+
112
+ The `auth` middleware lets only signed-in users reach a route:
113
+
114
+ ```ts title="routes/web.ts"
115
+ import { Route } from '@orieljs/framework'
116
+
117
+ Route.get('/flights', () => {
118
+ // Only authenticated users may access this route...
119
+ return 'Flights'
120
+ }).middleware('auth')
121
+ ```
122
+
123
+ #### Redirecting unauthenticated users
124
+
125
+ A guest who reaches a route behind `auth` is redirected to `/login`, and back to the page they wanted once they sign
126
+ in. A request that wants JSON gets a 401 instead, `{ "message": "Unauthenticated." }`. `redirectGuestsTo()` in
127
+ `bootstrap/app.ts` sends guests elsewhere (see
128
+ [Redirecting guests and signed-in users](middleware.md#redirecting-guests-and-signed-in-users)).
129
+
130
+ #### Redirecting authenticated users
131
+
132
+ The `guest` middleware does the opposite: it sends a signed-in user away from the pages meant for guests, such as the
133
+ login page. It sends them to your `dashboard` route, else your `home` route, else `/`; `redirectUsersTo()` in
134
+ `bootstrap/app.ts` chooses another place.
135
+
136
+ #### Specifying a guard
137
+
138
+ `auth` takes the guards to try, by name. The first one that has a user lets the request through, and becomes the
139
+ request's guard, so `Auth.user()` and `request().user()` read from it:
140
+
141
+ ```ts
142
+ Route.get('/flights', () => 'Flights').middleware('auth:token')
143
+
144
+ Route.get('/account', () => 'Account').middleware('auth:web,token')
145
+ ```
146
+
147
+ ### Login throttling
148
+
149
+ The kits' login stops a user after five failed attempts for the same email address and IP address, for a minute, and
150
+ dispatches the `Lockout` event. The limit is in their `LoginRequest` (in the Live kit, the `Login` component), written
151
+ with the `RateLimiter` (see [Rate limiting](cache.md#rate-limiting)).
152
+
153
+ ## Manually authenticating users
154
+
155
+ You don't have to use the kits' code. `Auth.attempt()` signs a user in with their credentials:
156
+
157
+ ```ts title="app/Http/Controllers/LoginController.ts"
158
+ import { Auth, back, redirect, request } from '@orieljs/framework'
159
+ import { Controller } from './Controller'
160
+
161
+ export class LoginController extends Controller {
162
+ /** Handle an authentication attempt. */
163
+ async authenticate() {
164
+ const credentials = await request().validate({
165
+ email: 'required|email',
166
+ password: 'required',
167
+ })
168
+
169
+ if (await Auth.attempt(credentials)) {
170
+ await request().session().regenerate()
171
+
172
+ return redirect().intended('dashboard')
173
+ }
174
+
175
+ return back()
176
+ .withErrors({ email: 'The provided credentials do not match our records.' })
177
+ .withInput(request().only('email'))
178
+ }
179
+ }
180
+ ```
181
+
182
+ `attempt()` finds the user by every credential except those whose key contains `password`, here by `email`, then
183
+ checks the password against the user's hash. When it matches, it signs the user in and returns `true`; otherwise it
184
+ returns `false`. `redirect().intended()` sends the user to the page they wanted before `auth` sent them to the login
185
+ page, or to the path you give.
186
+
187
+ `attempt()` changes the session's id as it signs the user in, which protects against session fixation; the kits also
188
+ call `regenerate()`, as above.
189
+
190
+ #### Specifying additional conditions
191
+
192
+ Add any condition to the credentials. A list matches any of its values, and a function receives the query:
193
+
194
+ ```ts
195
+ declare const email: string
196
+ declare const password: string
197
+ // ---cut---
198
+ if (await Auth.attempt({ email, password, active: 1 })) {
199
+ // Authentication was successful...
200
+ }
201
+
202
+ if (await Auth.attempt({ email, password, role: ['admin', 'editor'] })) {
203
+ // ...
204
+ }
205
+
206
+ if (await Auth.attempt({ email, password, verified: (query: { whereNotNull(column: string): unknown }) => query.whereNotNull('email_verified_at') })) {
207
+ // ...
208
+ }
209
+ ```
210
+
211
+ #### Accessing specific guard instances
212
+
213
+ `Auth.guard()` returns a guard by name, with the same methods:
214
+
215
+ ```ts
216
+ const user = await Auth.guard('token').user()
217
+ ```
218
+
219
+ ### Remembering users
220
+
221
+ Pass `true` as `attempt()`'s second argument to keep the user signed in after their session ends, until they sign
222
+ out:
223
+
224
+ ```ts
225
+ declare const email: string
226
+ declare const password: string
227
+ declare const remember: boolean
228
+ // ---cut---
229
+ if (await Auth.attempt({ email, password }, remember)) {
230
+ // The user is being remembered...
231
+ }
232
+ ```
233
+
234
+ Oriel then sets a "Remember me" cookie, which lasts 400 days unless the guard's `remember` setting, in minutes, says
235
+ otherwise. `Auth.viaRemember()` tells you whether the current user was signed in by that cookie. Signing out
236
+ changes the user's `remember_token`, so the cookie stops working on every browser.
237
+
238
+ ### Other authentication methods
239
+
240
+ #### Authenticate a user instance
241
+
242
+ `Auth.login()` signs in a user you already have, such as one who has registered. Its second argument remembers the user:
243
+
244
+ ```ts
245
+ declare const user: User
246
+ // ---cut---
247
+ await Auth.login(user)
248
+
249
+ await Auth.login(user, true)
250
+ ```
251
+
252
+ #### Authenticate a user by ID
253
+
254
+ `Auth.loginUsingId()` signs in the user with that primary key, and returns them, or `false` when there is none:
255
+
256
+ ```ts
257
+ await Auth.loginUsingId(1)
258
+ ```
259
+
260
+ #### Authenticate a user once
261
+
262
+ `Auth.once()` signs a user in for the current request only: nothing is written to the session, and no cookie is set:
263
+
264
+ ```ts
265
+ declare const credentials: { email: string; password: string }
266
+ // ---cut---
267
+ if (await Auth.once(credentials)) {
268
+ // ...
269
+ }
270
+ ```
271
+
272
+ `attempt()`, `once()`, `login()`, `loginUsingId()` and `logout()` need a guard that keeps a user, such as `web`, and
273
+ throw on the `token` guard.
274
+
275
+ > [!NOTE]
276
+ > Laravel also has HTTP Basic authentication (`auth.basic`). Oriel doesn't.
277
+
278
+ ## Logging out
279
+
280
+ `Auth.logout()` signs the user out. Invalidate their session and make a new CSRF token too, then redirect them:
281
+
282
+ ```ts title="app/Http/Controllers/LogoutController.ts"
283
+ import { Auth, redirect, request } from '@orieljs/framework'
284
+ import { Controller } from './Controller'
285
+
286
+ export class LogoutController extends Controller {
287
+ /** Log the user out of the application. */
288
+ async logout() {
289
+ await Auth.logout()
290
+
291
+ await request().session().invalidate()
292
+
293
+ request().session().regenerateToken()
294
+
295
+ return redirect('/')
296
+ }
297
+ }
298
+ ```
299
+
300
+ > [!NOTE]
301
+ > Laravel's `logoutOtherDevices()` and its password confirmation pages (`password.confirm`) aren't in Oriel.
302
+
303
+ ## API tokens
304
+
305
+ A client that isn't a browser, such as a mobile app or another service, signs in with an API token. Oriel's tokens are
306
+ Laravel Sanctum's personal access tokens, under the `token` guard: Sanctum's `auth:sanctum` is `auth:token` here. A new
307
+ app has the `personal_access_tokens` table, and its `User` model has `HasApiTokens`.
308
+
309
+ ### Issuing tokens
310
+
311
+ `createToken()` makes a token for the user, with a name. The token's plain text is shown only once, so return it to the
312
+ client and keep nothing else. Here a user signed in in the browser makes one, from a web route:
313
+
314
+ ```ts title="routes/web.ts"
315
+ import { Auth, request, Route } from '@orieljs/framework'
316
+ import type { User } from '../app/Models/User'
317
+
318
+ Route.post('/tokens/create', async () => {
319
+ const user = (await Auth.user<User>())!
320
+ const token = await user.createToken(request().string('token_name').toString())
321
+
322
+ return { token: token.plainTextToken }
323
+ }).middleware('auth')
324
+ ```
325
+
326
+ The client sends it with every request, in the `Authorization` header:
327
+
328
+ ```text
329
+ Authorization: Bearer 1|Zb3qL9vXw2Kc7Ty4NfRm8HpAs6Ud1GeJo5WiYtBn
330
+ ```
331
+
332
+ The token is its row's id, a `|` and a random secret; the table keeps only the secret's SHA-256.
333
+
334
+ ### Protecting routes
335
+
336
+ Put the routes behind `auth:token`. The API kit does, for its routes in `routes/api.ts`:
337
+
338
+ ```ts title="routes/api.ts"
339
+ import { request, Route } from '@orieljs/framework'
340
+
341
+ Route.middleware('auth:token').group(() => {
342
+ Route.get('/user', () => request().user())
343
+ })
344
+ ```
345
+
346
+ A request with no token, a wrong one or an expired one gets a 401, `{ "message": "Unauthenticated." }`, when it asks for
347
+ JSON, as API clients do; the API kit answers JSON under `/api` whatever the request asks for. Each request with a token
348
+ updates its `last_used_at`.
349
+
350
+ ### Token abilities
351
+
352
+ A token can be limited to some abilities, which you list when you make it. A token made without a list has every
353
+ ability:
354
+
355
+ ```ts
356
+ declare const user: User
357
+ // ---cut---
358
+ const token = await user.createToken('token-name', ['server:update'])
359
+ ```
360
+
361
+ In a request made with a token, `user.tokenCan()` tells you whether the token has an ability, and `tokenCant()`
362
+ whether it hasn't:
363
+
364
+ ```ts
365
+ const user = (await request().user<User>())!
366
+
367
+ if (user.tokenCan('server:update')) {
368
+ // ...
369
+ }
370
+ ```
371
+
372
+ ### Token expiration
373
+
374
+ A token can expire: give `createToken()` a date as its third argument. The `token` guard's `expiration`, in minutes,
375
+ ends every token that long after it was made; `null`, in a new app, leaves them to their own date.
376
+
377
+ ```ts
378
+ declare const user: User
379
+ // ---cut---
380
+ const token = await user.createToken('token-name', ['*'], new Date(Date.now() + 7 * 24 * 60 * 60 * 1000))
381
+ ```
382
+
383
+ ### Revoking tokens
384
+
385
+ `currentAccessToken()` is the token of the current request, and `tokens()` gives all of the user's tokens. Delete a
386
+ token to revoke it:
387
+
388
+ ```ts
389
+ const user = (await request().user<User>())!
390
+
391
+ // Revoke the token that was used to authenticate the current request...
392
+ await user.currentAccessToken()?.delete()
393
+
394
+ // Revoke all tokens...
395
+ for (const token of await user.tokens()) {
396
+ await token.delete()
397
+ }
398
+ ```
399
+
400
+ > [!NOTE]
401
+ > Laravel Sanctum's `tokens()` is a relation, so `$user->tokens()->delete()` revokes them all with one query. Oriel's
402
+ > returns the tokens as an array: delete each one. Sanctum's SPA cookie authentication isn't in Oriel: a browser app
403
+ > served by your app signs in with the `web` guard, and a route can accept both with `auth:web,token`.
404
+
405
+ ## Adding custom guards
406
+
407
+ `Auth.extend()` adds a guard driver, in a service provider's `boot()`. Its function receives the guard's name and its
408
+ entry of `config/auth.ts`, and returns an object with the `Guard` contract's methods:
409
+
410
+ ```ts title="app/Providers/AppServiceProvider.ts"
411
+ // @filename: app/Services/Auth/JwtGuard.ts
412
+ import type { Guard } from '@orieljs/framework/contracts'
413
+ export declare class JwtGuard implements Guard {
414
+ constructor(name: string)
415
+ user: Guard['user']
416
+ id: Guard['id']
417
+ check: Guard['check']
418
+ guest: Guard['guest']
419
+ validate: Guard['validate']
420
+ hasUser: Guard['hasUser']
421
+ setUser: Guard['setUser']
422
+ }
423
+ // ---cut---
424
+ import { Auth, ServiceProvider } from '@orieljs/framework'
425
+ import { JwtGuard } from '../Services/Auth/JwtGuard'
426
+
427
+ export class AppServiceProvider extends ServiceProvider {
428
+ override boot(): void {
429
+ Auth.extend('jwt', (name) => new JwtGuard(name))
430
+ }
431
+ }
432
+ ```
433
+
434
+ Then name the driver in a guard of `config/auth.ts`: `api: { driver: 'jwt', provider: 'users' }`.
435
+
436
+ ## Adding custom user providers
437
+
438
+ `Auth.provider()` adds a user provider driver, for users kept somewhere your models don't reach. Its function
439
+ receives the provider's entry of `config/auth.ts` and returns an object with the `UserProvider` contract's methods:
440
+ `retrieveById()`, `retrieveByToken()`, `updateRememberToken()`, `retrieveByCredentials()` and
441
+ `validateCredentials()`:
442
+
443
+ ```ts title="app/Providers/AppServiceProvider.ts"
444
+ // @filename: app/Extensions/MongoUserProvider.ts
445
+ import type { UserProvider } from '@orieljs/framework/contracts'
446
+ export declare class MongoUserProvider implements UserProvider {
447
+ constructor(connection: unknown)
448
+ retrieveById: UserProvider['retrieveById']
449
+ retrieveByToken: UserProvider['retrieveByToken']
450
+ updateRememberToken: UserProvider['updateRememberToken']
451
+ retrieveByCredentials: UserProvider['retrieveByCredentials']
452
+ validateCredentials: UserProvider['validateCredentials']
453
+ }
454
+ // ---cut---
455
+ import { Auth, ServiceProvider } from '@orieljs/framework'
456
+ import { MongoUserProvider } from '../Extensions/MongoUserProvider'
457
+
458
+ export class AppServiceProvider extends ServiceProvider {
459
+ override boot(): void {
460
+ Auth.provider('mongo', (config) => new MongoUserProvider(config.connection))
461
+ }
462
+ }
463
+ ```
464
+
465
+ Then use it in `config/auth.ts`: `providers: { users: { driver: 'mongo', connection: 'mongodb' } }`.
466
+
467
+ ## Automatic password rehashing
468
+
469
+ When a user signs in with `attempt()` and their password's hash was made with other settings than your current ones,
470
+ such as fewer bcrypt rounds, Oriel hashes the password again and saves it. Turn this off with `rehash_on_login: false`
471
+ in `config/hashing.ts` (see [Hashing](hashing.md)).
472
+
473
+ ## Events
474
+
475
+ Signing in and out dispatches these events, which you can [listen](events.md) for:
476
+
477
+ | Event | When |
478
+ | --- | --- |
479
+ | `Attempting` | `attempt()` or `once()` is about to check credentials |
480
+ | `Validated` | The credentials matched |
481
+ | `Failed` | The credentials didn't match |
482
+ | `Login` | A user signed in with `attempt()`, `login()`, `loginUsingId()` or the "Remember me" cookie (`once()` dispatches none) |
483
+ | `Logout` | A user signed out |
484
+ | `Lockout` | The kits' login throttle stopped a user |
485
+ | `Registered` | The kits registered a user (it sends the verification mail) |
486
+ | `Verified` | A user verified their email address |
487
+ | `PasswordReset` | The kits reset a user's password |