@orieljs/framework 0.1.0-alpha.5 → 0.1.0-alpha.7

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 (87) hide show
  1. package/NOTICE +7 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-WNJP46B7.js → chunk-4MWG3B7G.js} +1785 -626
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/chunk-7ZQZYDOC.js +3253 -0
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-BSQKOLSK.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.js.map +1 -0
  10. package/dist/{chunk-A5XRLEWM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +535 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +2202 -5952
  16. package/dist/index.js +4900 -944
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-WKTl3EEw.d.ts → jsx-runtime-6um4Mwfb.d.ts} +97 -51
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BErengYY.d.ts → testing-CKwfWJGn.d.ts} +313 -994
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +19 -5
  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-A5XRLEWM.js.map +0 -1
  83. package/dist/chunk-BSQKOLSK.js.map +0 -1
  84. package/dist/chunk-PM5YTG57.js +0 -880
  85. package/dist/chunk-PM5YTG57.js.map +0 -1
  86. package/dist/chunk-WNJP46B7.js.map +0 -1
  87. package/dist/index.d-DOlTeG-8.d.ts +0 -99
@@ -0,0 +1,435 @@
1
+ # Authorization
2
+
3
+ Decide what a signed-in user may do: write gates for single checks and policies for the actions on a model, then check
4
+ them in your controllers, routes and pages.
5
+
6
+ ## Introduction
7
+
8
+ [Authentication](authentication.md) tells you who a user is; authorization tells you what they may do, such as
9
+ whether they may update a post. Oriel has two ways to write those rules, as Laravel does. **Gates** are functions,
10
+ each for one ability, and suit checks that don't belong to a model, such as reaching the admin dashboard. **Policies**
11
+ are classes that group the rules for one model, such as `PostPolicy` for `Post`. Most apps use both.
12
+
13
+ Every check is async, since a rule may read the database: `await` it.
14
+
15
+ ## Gates
16
+
17
+ ### Writing gates
18
+
19
+ Define a gate with `Gate.define()`, in a service provider's `boot()`. A gate receives the signed-in user, then the
20
+ arguments of the check, and returns whether the user may go ahead:
21
+
22
+ ```ts title="app/Providers/AppServiceProvider.ts"
23
+ import { Gate, ServiceProvider } from '@orieljs/framework'
24
+ import type { Post } from '../Models/Post'
25
+ import type { User } from '../Models/User'
26
+
27
+ export class AppServiceProvider extends ServiceProvider {
28
+ override boot(): void {
29
+ Gate.define('update-post', (user, post) => (user as User).id === (post as Post).author_id)
30
+ }
31
+ }
32
+ ```
33
+
34
+ A gate can also be a policy method, given as a class and a method name:
35
+ `Gate.define('update-post', [PostPolicy, 'update'])`. A gate may be async.
36
+
37
+ > [!NOTE]
38
+ > A gate's function receives the user typed as `Authenticatable | undefined`, and the check's arguments as `unknown`,
39
+ > since a check can pass anything: cast them, as above. A policy's methods are yours to type, so a rule that takes a
40
+ > model reads better as a [policy](#writing-policies).
41
+
42
+ ### Authorizing actions
43
+
44
+ `Gate.allows()` and `Gate.denies()` check a gate for the signed-in user. You don't pass the user: Oriel gives the
45
+ gate the current one.
46
+
47
+ ```ts title="app/Http/Controllers/PostController.ts"
48
+ import { abort, Gate, redirect } from '@orieljs/framework'
49
+ import type { Post } from '../../Models/Post'
50
+ import { Controller } from './Controller'
51
+
52
+ export class PostController extends Controller {
53
+ /** Update the given post. */
54
+ async update(post: Post) {
55
+ if (await Gate.denies('update-post', post)) {
56
+ abort(403)
57
+ }
58
+
59
+ // Update the post...
60
+
61
+ return redirect('/posts')
62
+ }
63
+ }
64
+ ```
65
+
66
+ `Gate.forUser(user)` checks for another user. `Gate.any()` is `true` when any of a list of abilities is allowed, and
67
+ `Gate.none()` when none is; `Gate.check()` takes one ability or a list, all of which must be allowed:
68
+
69
+ ```ts
70
+ declare const user: User
71
+ declare const post: Post
72
+ // ---cut---
73
+ if (await Gate.forUser(user).allows('update-post', post)) {
74
+ // The user can update the post...
75
+ }
76
+
77
+ if (await Gate.any(['update-post', 'delete-post'], post)) {
78
+ // The user can update or delete the post...
79
+ }
80
+
81
+ if (await Gate.none(['update-post', 'delete-post'], post)) {
82
+ // The user can't update or delete the post...
83
+ }
84
+ ```
85
+
86
+ An ability that no gate or policy defines is denied.
87
+
88
+ #### Authorizing or throwing exceptions
89
+
90
+ `Gate.authorize()` checks a gate and throws an `AuthorizationError` when it denies, which Oriel renders as a 403
91
+ response, "This action is unauthorized.":
92
+
93
+ ```ts
94
+ declare const post: Post
95
+ // ---cut---
96
+ await Gate.authorize('update-post', post)
97
+
98
+ // The action is authorized...
99
+ ```
100
+
101
+ #### Supplying additional context
102
+
103
+ A gate receives every argument after the ability's name, in order:
104
+
105
+ ```ts
106
+ declare const category: number
107
+ declare const pinned: boolean
108
+ // ---cut---
109
+ Gate.define('create-post', (user, category, pinned) => (user as User).id > 0 && Number(category) > 0 && pinned !== true)
110
+
111
+ if (await Gate.allows('create-post', category, pinned)) {
112
+ // The user can create the post...
113
+ }
114
+ ```
115
+
116
+ ### Gate responses
117
+
118
+ A gate can return a `GateResponse`, with a message, in place of a boolean:
119
+
120
+ ```ts
121
+ Gate.define('edit-settings', (user) =>
122
+ (user as User).email.endsWith('@example.com') ? GateResponse.allow() : GateResponse.deny('You must be an administrator.'),
123
+ )
124
+ ```
125
+
126
+ `Gate.inspect()` returns the response itself, so you can read its message; `Gate.authorize()` throws an
127
+ `AuthorizationError` with it:
128
+
129
+ ```ts
130
+ const response = await Gate.inspect('edit-settings')
131
+
132
+ if (response.allowed()) {
133
+ // The action is authorized...
134
+ } else {
135
+ const message = response.message()
136
+ }
137
+ ```
138
+
139
+ #### Customizing the HTTP response status
140
+
141
+ A denial answers 403. `GateResponse.denyWithStatus()` chooses another status, and `GateResponse.denyAsNotFound()`
142
+ answers 404, which hides from the user that the resource exists:
143
+
144
+ ```ts
145
+ Gate.define('edit-settings', (user) =>
146
+ (user as User).email.endsWith('@example.com') ? GateResponse.allow() : GateResponse.denyAsNotFound(),
147
+ )
148
+ ```
149
+
150
+ ### Intercepting gate checks
151
+
152
+ `Gate.before()` runs before every check. A function that returns `true` or `false` decides the check; one that
153
+ returns nothing lets the check go on:
154
+
155
+ ```ts
156
+ Gate.before((user) => {
157
+ if ((user as User | undefined)?.email === 'admin@example.com') {
158
+ return true
159
+ }
160
+ })
161
+ ```
162
+
163
+ `Gate.after()` runs after every check, with its result. Its own result counts only when the check had none, such as an
164
+ ability nobody defined.
165
+
166
+ ## Creating policies
167
+
168
+ ### Generating policies
169
+
170
+ `make:policy` writes a policy in `app/Policies`. With `--model`, it writes a method for each of the model's usual
171
+ actions, typed with your `User` and the model, each denying until you write it:
172
+
173
+ ```sh
174
+ node oriel make:policy PostPolicy --model=Post
175
+ ```
176
+
177
+ > [!WARNING]
178
+ > Without `--model`, `make:policy` writes `view()`, `create()`, `update()` and `delete()` methods that return `true`,
179
+ > so the policy allows everything until you write them. Since Oriel finds the policy by its name, it applies as soon
180
+ > as the file exists.
181
+
182
+ ### Registering policies
183
+
184
+ #### Policy discovery
185
+
186
+ When your app boots, Oriel reads the files at the top of `app/Policies` whose names end in `Policy`, and uses each
187
+ for the model of the same name: `app/Policies/PostPolicy.ts` authorizes `Post`. The policy is the file's default
188
+ export, or its export named after the file. You don't register it.
189
+
190
+ #### Manually registering policies
191
+
192
+ `Gate.policy()` registers a policy for a model, in a service provider's `boot()`, which wins over discovery:
193
+
194
+ ```ts title="app/Providers/AppServiceProvider.ts"
195
+ // @filename: app/Policies/OrderPolicy.ts
196
+ export declare class OrderPolicy {}
197
+ // ---cut---
198
+ import { Gate, ServiceProvider } from '@orieljs/framework'
199
+ import { Order } from '../Models/Order'
200
+ import { OrderPolicy } from '../Policies/OrderPolicy'
201
+
202
+ export class AppServiceProvider extends ServiceProvider {
203
+ override boot(): void {
204
+ Gate.policy(Order, OrderPolicy)
205
+ }
206
+ }
207
+ ```
208
+
209
+ ## Writing policies
210
+
211
+ ### Policy methods
212
+
213
+ A policy's methods are named after the abilities they check, and receive the user and the model. The ability
214
+ `update` calls `update()`, and a kebab-case ability such as `view-any` calls `viewAny()`:
215
+
216
+ ```ts title="app/Policies/PostPolicy.ts"
217
+ import type { Post } from '../Models/Post'
218
+ import type { User } from '../Models/User'
219
+
220
+ export class PostPolicy {
221
+ /** Determine if the given post can be updated by the user. */
222
+ update(user: User, post: Post): boolean {
223
+ return user.id === post.author_id
224
+ }
225
+ }
226
+ ```
227
+
228
+ Oriel makes the policy through the container for each check, so a method can take a service with `inject()`.
229
+
230
+ ### Policy responses
231
+
232
+ A policy method can return a `GateResponse`, as a gate can:
233
+
234
+ ```ts title="app/Policies/PostPolicy.ts"
235
+ import { GateResponse } from '@orieljs/framework'
236
+ import type { Post } from '../Models/Post'
237
+ import type { User } from '../Models/User'
238
+
239
+ export class PostPolicy {
240
+ /** Determine if the given post can be updated by the user. */
241
+ update(user: User, post: Post): GateResponse {
242
+ return user.id === post.author_id ? GateResponse.allow() : GateResponse.deny('You do not own this post.')
243
+ }
244
+ }
245
+ ```
246
+
247
+ `GateResponse.denyAsNotFound()` and `denyWithStatus()` work here too.
248
+
249
+ ### Methods without models
250
+
251
+ Some actions have no model yet, such as `create`. Check them with the model's class in place of a model: the class
252
+ chooses the policy, and the method receives only the user:
253
+
254
+ ```ts
255
+ declare const user: User
256
+ // ---cut---
257
+ class PostPolicy {
258
+ create(user: User): boolean {
259
+ return user.email_verified_at !== null
260
+ }
261
+ }
262
+
263
+ if (await user.can('create', Post)) {
264
+ // ...
265
+ }
266
+ ```
267
+
268
+ ### Guest users
269
+
270
+ A check made without a signed-in user is denied, and the gate or the policy method isn't called. To let guests
271
+ through a gate, wrap its function in `allowGuests()`; a policy lets them reach all its methods with
272
+ `static allowGuests = true`, or some of them with a list. The user is then `undefined`:
273
+
274
+ ```ts
275
+ class PostPolicy {
276
+ static allowGuests = ['view']
277
+
278
+ view(user: User | undefined, post: Post): boolean {
279
+ return post.published_at !== null || user?.id === post.author_id
280
+ }
281
+ }
282
+
283
+ Gate.define('view-posts', allowGuests(() => true))
284
+ ```
285
+
286
+ > [!NOTE]
287
+ > Laravel lets guests through a gate or a policy method whose user parameter is optional or nullable. TypeScript's
288
+ > types are gone when your app runs, so Oriel asks you to say so: `allowGuests()` or `static allowGuests`.
289
+
290
+ ### Policy filters
291
+
292
+ A policy's `before()` method runs before its other methods. Return `true` or `false` to decide, or nothing to let the
293
+ method decide. It runs for every ability checked with the policy's model, even one the policy has no method for, which
294
+ Laravel skips:
295
+
296
+ ```ts
297
+ class PostPolicy {
298
+ before(user: User, ability: string): boolean | undefined {
299
+ if (user.email === 'admin@example.com') {
300
+ return true
301
+ }
302
+
303
+ return undefined
304
+ }
305
+ }
306
+ ```
307
+
308
+ ## Authorizing actions using policies
309
+
310
+ ### Via the user model
311
+
312
+ The `Authorizable` mixin, which a new app's `User` has, gives the user `can()` and `cannot()`. When the model has a
313
+ policy, the check runs its method:
314
+
315
+ ```ts title="app/Http/Controllers/PostController.ts"
316
+ import { abort, redirect, request } from '@orieljs/framework'
317
+ import type { Post } from '../../Models/Post'
318
+ import type { User } from '../../Models/User'
319
+ import { Controller } from './Controller'
320
+
321
+ export class PostController extends Controller {
322
+ /** Update the given post. */
323
+ async update(post: Post) {
324
+ const user = (await request().user<User>())!
325
+
326
+ if (await user.cannot('update', post)) {
327
+ abort(403)
328
+ }
329
+
330
+ // Update the post...
331
+
332
+ return redirect('/posts')
333
+ }
334
+ }
335
+ ```
336
+
337
+ `cant()` is `cannot()`, and `canAny()` takes a list of abilities. A model without a policy is checked against the
338
+ gate of that ability.
339
+
340
+ ### Via controller helpers
341
+
342
+ The base controller has `authorize()`, which throws an `AuthorizationError`, a 403, when the user may not:
343
+
344
+ ```ts title="app/Http/Controllers/PostController.ts"
345
+ import { redirect } from '@orieljs/framework'
346
+ import type { Post } from '../../Models/Post'
347
+ import { Controller } from './Controller'
348
+
349
+ export class PostController extends Controller {
350
+ /** Update the given blog post. */
351
+ async update(post: Post) {
352
+ await this.authorize('update', post)
353
+
354
+ // The current user can update the blog post...
355
+
356
+ return redirect('/posts')
357
+ }
358
+ }
359
+ ```
360
+
361
+ Pass the model's class for an action without a model: `await this.authorize('create', Post)`.
362
+ `authorizeForUser(user, ability, ...)` checks for another user.
363
+
364
+ > [!NOTE]
365
+ > Laravel's `authorizeResource()`, which authorizes every action of a resource controller at once, isn't in Oriel.
366
+ > Call `authorize()` in each action.
367
+
368
+ ### Via middleware
369
+
370
+ The `can` middleware authorizes a request before it reaches your route. Its first parameter is the ability, and the
371
+ next ones are route parameters, whose bound models it passes on:
372
+
373
+ ```ts title="routes/web.ts"
374
+ import { Route } from '@orieljs/framework'
375
+ import type { Post } from '../app/Models/Post'
376
+
377
+ Route.put('/post/{post}', (post: Post) => {
378
+ // The current user may update the post...
379
+
380
+ return post
381
+ }).middleware(['auth', 'can:update,post'])
382
+ ```
383
+
384
+ A parameter that isn't a route parameter names a model, for an action without one: `can:create,Post`. A quoted one
385
+ is passed as text: `can:publish,post,"draft"`.
386
+
387
+ > [!NOTE]
388
+ > Laravel's routes also take `->can('update', 'post')`, and its gates have `Gate::allowIf()` and `Gate::denyIf()`.
389
+ > Oriel has neither: use the `can:` middleware, and `Gate.authorize()` with a gate you define.
390
+
391
+ ### Via form requests
392
+
393
+ A form request's `authorize()` runs before its rules; returning `false` answers 403:
394
+
395
+ ```ts title="app/Http/Requests/UpdatePostRequest.ts"
396
+ import { FormRequest, Gate, request } from '@orieljs/framework'
397
+ import type { Post } from '../../Models/Post'
398
+
399
+ export class UpdatePostRequest extends FormRequest {
400
+ override async authorize(): Promise<boolean> {
401
+ return Gate.allows('update', request().route<Post>('post'))
402
+ }
403
+
404
+ override rules() {
405
+ return { title: 'required|string|max:255' } as const
406
+ }
407
+ }
408
+ ```
409
+
410
+ ### In views and pages
411
+
412
+ Oriel's views are TSX, so there is no `@can`: work out what the user may do in your route or controller, and pass
413
+ the answers to the view or the page as props:
414
+
415
+ ```ts title="app/Http/Controllers/PostController.ts"
416
+ import { inertia, request } from '@orieljs/framework'
417
+ import type { Post } from '../../Models/Post'
418
+ import type { User } from '../../Models/User'
419
+ import { Controller } from './Controller'
420
+
421
+ export class PostController extends Controller {
422
+ /** Show the given post. */
423
+ async show(post: Post) {
424
+ const user = await request().user<User>()
425
+
426
+ return inertia('Posts/Show', {
427
+ post,
428
+ can: { update: (await user?.can('update', post)) ?? false },
429
+ })
430
+ }
431
+ }
432
+ ```
433
+
434
+ In a Live component, the route's `can` middleware runs again on every update (see
435
+ [Middleware on updates](live.md#middleware-on-updates)).