@orieljs/framework 0.1.0-alpha.6 → 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.
- package/NOTICE +4 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-ZOEPKBVF.js → chunk-4MWG3B7G.js} +1347 -428
- package/dist/chunk-4MWG3B7G.js.map +1 -0
- package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +515 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +964 -152
- package/dist/index.js +4279 -866
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/docs/authentication.md +487 -0
- package/docs/authorization.md +435 -0
- package/docs/cache.md +647 -0
- package/docs/collections.md +2270 -0
- package/docs/configuration.md +334 -0
- package/docs/console.md +651 -0
- package/docs/container.md +430 -0
- package/docs/controllers.md +406 -0
- package/docs/csrf.md +126 -0
- package/docs/database-testing.md +144 -0
- package/docs/database.md +236 -0
- package/docs/deployment.md +276 -0
- package/docs/eloquent-collections.md +218 -0
- package/docs/eloquent-factories.md +255 -0
- package/docs/eloquent-mutators.md +254 -0
- package/docs/eloquent-observers.md +145 -0
- package/docs/eloquent-relationships.md +557 -0
- package/docs/eloquent.md +494 -0
- package/docs/encryption.md +104 -0
- package/docs/errors.md +318 -0
- package/docs/events.md +336 -0
- package/docs/facades.md +278 -0
- package/docs/hashing.md +118 -0
- package/docs/helpers.md +1281 -0
- package/docs/http-tests.md +386 -0
- package/docs/inertia.md +485 -0
- package/docs/installation.md +209 -0
- package/docs/lifecycle.md +234 -0
- package/docs/live-tests.md +226 -0
- package/docs/live.md +910 -0
- package/docs/logging.md +255 -0
- package/docs/mail.md +619 -0
- package/docs/middleware.md +384 -0
- package/docs/migrations.md +416 -0
- package/docs/mocking.md +264 -0
- package/docs/navigation.json +273 -0
- package/docs/notifications.md +225 -0
- package/docs/pagination.md +247 -0
- package/docs/passwords.md +207 -0
- package/docs/providers.md +187 -0
- package/docs/queries.md +600 -0
- package/docs/queues.md +330 -0
- package/docs/requests.md +533 -0
- package/docs/responses.md +444 -0
- package/docs/routing.md +738 -0
- package/docs/scheduling.md +277 -0
- package/docs/seeding.md +122 -0
- package/docs/session.md +409 -0
- package/docs/starter-kits.md +396 -0
- package/docs/storage.md +192 -0
- package/docs/structure.md +264 -0
- package/docs/testing.md +165 -0
- package/docs/time-travel.md +138 -0
- package/docs/validation.md +1792 -0
- package/docs/verification.md +152 -0
- package/docs/views.md +566 -0
- package/docs/vite.md +322 -0
- package/package.json +34 -32
- package/dist/chunk-I63ZFMX6.js.map +0 -1
- package/dist/chunk-MFTZQMZM.js.map +0 -1
- package/dist/chunk-POEP4B2M.js.map +0 -1
- package/dist/chunk-ZOEPKBVF.js.map +0 -1
- package/dist/index.d-BJyJx6U1.d.ts +0 -319
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Database: pagination
|
|
2
|
+
|
|
3
|
+
Split a long list into pages with one call on a query or a model, and show the page links in a view, an Inertia page
|
|
4
|
+
or a Live component.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
`paginate()` runs a query for one page of rows, reads the page number from the request's `page` parameter, and gives
|
|
9
|
+
back a paginator: the rows of that page, with the page numbers and the URLs of the other pages.
|
|
10
|
+
|
|
11
|
+
## Basic usage
|
|
12
|
+
|
|
13
|
+
### Paginating query builder results
|
|
14
|
+
|
|
15
|
+
`paginate()` takes the number of rows per page:
|
|
16
|
+
|
|
17
|
+
```ts title="app/Http/Controllers/UserController.ts"
|
|
18
|
+
import { DB, view } from '@orieljs/framework'
|
|
19
|
+
import { Controller } from './Controller'
|
|
20
|
+
|
|
21
|
+
export class UserController extends Controller {
|
|
22
|
+
/** Show all application users. */
|
|
23
|
+
async index() {
|
|
24
|
+
const users = await DB.table('users').paginate(15)
|
|
25
|
+
|
|
26
|
+
return view('user.index', { users })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
It runs two queries: one counts the rows, so the paginator knows the last page, and one reads the page.
|
|
32
|
+
|
|
33
|
+
A page number that isn't a whole number of 1 or more is page 1. A page past the last one is empty.
|
|
34
|
+
|
|
35
|
+
#### Simple pagination
|
|
36
|
+
|
|
37
|
+
`simplePaginate()` reads one row more than a page, to tell whether there is a next page, and doesn't count the rows.
|
|
38
|
+
Use it for long lists where you show only "Previous" and "Next":
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const users = await DB.table('users').where('votes', '>', 100).simplePaginate(15)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Paginating model results
|
|
45
|
+
|
|
46
|
+
Your models paginate in the same way, as a static method or at the end of a query. Without a number, a model's
|
|
47
|
+
`perPage`, 15, is the page size:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
declare const user: User
|
|
51
|
+
// ---cut---
|
|
52
|
+
const users = await User.paginate(15)
|
|
53
|
+
|
|
54
|
+
const verified = await User.whereNotNull('email_verified_at').paginate(15)
|
|
55
|
+
|
|
56
|
+
const posts = await user.posts().paginate(10)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### Multiple paginator instances per page
|
|
60
|
+
|
|
61
|
+
Two paginators on one page need their own parameter. The third argument names it, after the columns:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const users = await User.whereNotNull('email_verified_at').paginate(15, ['*'], 'users')
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The fourth argument sets the page number yourself, in place of the request's: `paginate(15, ['*'], 'page', 2)`.
|
|
68
|
+
|
|
69
|
+
### Cursor pagination
|
|
70
|
+
|
|
71
|
+
`cursorPaginate()` pages with a cursor, the values of the ordering columns of the last row, rather than with an offset.
|
|
72
|
+
The query needs an `orderBy()`; a model's query orders by its key when it has none:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const users = await DB.table('users').orderBy('id').cursorPaginate(15)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The next page's URL carries the cursor in the `cursor` parameter, such as
|
|
79
|
+
`http://localhost/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0`.
|
|
80
|
+
|
|
81
|
+
#### Cursor vs. offset pagination
|
|
82
|
+
|
|
83
|
+
An offset query, `limit 15 offset 15000`, makes the database read and skip the rows before the page, which gets slow on
|
|
84
|
+
large tables, and a row added while someone pages can show twice or be skipped. A cursor query,
|
|
85
|
+
`where id > 15000 limit 15`, uses an index and is steady while rows change. In exchange, a cursor paginator has no page
|
|
86
|
+
numbers and no total, only next and previous, and its ordering columns must be unique together. `orderByRaw()` and
|
|
87
|
+
`inRandomOrder()` can't be used with it.
|
|
88
|
+
|
|
89
|
+
### Manually creating a paginator
|
|
90
|
+
|
|
91
|
+
To paginate rows you already have, make a `LengthAwarePaginator` (`paginate()`'s), a `Paginator`
|
|
92
|
+
(`simplePaginate()`'s) or a `CursorPaginator` yourself, with the page's items, the total for a length-aware one, the
|
|
93
|
+
page size and the page:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
declare const pageOfItems: { id: number }[]
|
|
97
|
+
// ---cut---
|
|
98
|
+
const paginator = new LengthAwarePaginator(pageOfItems, 100, 15, 2)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Customizing pagination URLs
|
|
102
|
+
|
|
103
|
+
The links point at the current URL, without its query string. `withPath()` points them elsewhere:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const users = (await User.paginate(15)).withPath('/admin/users')
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Appending query string values
|
|
110
|
+
|
|
111
|
+
`appends()` adds values to every link, and `withQueryString()` keeps the current request's:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const users = (await User.paginate(15)).appends('sort', 'votes')
|
|
115
|
+
|
|
116
|
+
const filtered = (await User.paginate(15)).withQueryString()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### Appending hash fragments
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const users = (await User.paginate(15)).fragment('users')
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Displaying pagination results
|
|
126
|
+
|
|
127
|
+
A paginator is iterable, so `for...of` goes through its items, and `items()` returns them as an array. Its other
|
|
128
|
+
methods describe the page:
|
|
129
|
+
|
|
130
|
+
| Method | Returns |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `currentPage()` | The current page number |
|
|
133
|
+
| `firstItem()`, `lastItem()` | The number of the page's first and last item, or `null` for an empty page |
|
|
134
|
+
| `perPage()` | The page size |
|
|
135
|
+
| `count()` | The number of items on this page |
|
|
136
|
+
| `onFirstPage()`, `onLastPage()` | Whether this is the first or the last page (`onLastPage()`: not `cursorPaginate()`) |
|
|
137
|
+
| `hasPages()`, `hasMorePages()` | Whether there are pages to show (not `cursorPaginate()`), and pages after this one |
|
|
138
|
+
| `previousPageUrl()`, `nextPageUrl()` | The URL of the previous and next pages, or `null` |
|
|
139
|
+
| `url(page)` | The URL of a page |
|
|
140
|
+
| `total()`, `lastPage()` | The number of rows and of pages (`paginate()` only) |
|
|
141
|
+
| `cursor()`, `nextCursor()`, `previousCursor()` | The cursors (`cursorPaginate()` only) |
|
|
142
|
+
| `getCollection()`, `through(fn)` | The items as a collection, and a paginator of the items passed through `fn` |
|
|
143
|
+
|
|
144
|
+
### In views
|
|
145
|
+
|
|
146
|
+
Oriel's views are TSX, so there is no `links()` that prints the links for you. `linkCollection()` gives them as objects,
|
|
147
|
+
with a `url` (`null` for the "..." gaps, and for Previous or Next when there is no such page), a `label` and whether the
|
|
148
|
+
link is `active`, from "« Previous" to "Next »":
|
|
149
|
+
|
|
150
|
+
```tsx title="resources/views/user/index.tsx"
|
|
151
|
+
import type { LengthAwarePaginator } from '@orieljs/framework'
|
|
152
|
+
|
|
153
|
+
interface User {
|
|
154
|
+
id: number
|
|
155
|
+
name: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export default function Index({ users }: { users: LengthAwarePaginator<User> }) {
|
|
159
|
+
return (
|
|
160
|
+
<div>
|
|
161
|
+
<ul>
|
|
162
|
+
{users.items().map((user) => (
|
|
163
|
+
<li>{user.name}</li>
|
|
164
|
+
))}
|
|
165
|
+
</ul>
|
|
166
|
+
<nav>
|
|
167
|
+
{users.linkCollection().map((link) =>
|
|
168
|
+
link.url === null ? <span>{link.label}</span> : <a href={link.url} class={link.active ? 'active' : undefined}>{link.label}</a>,
|
|
169
|
+
)}
|
|
170
|
+
</nav>
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`onEachSide(count)` sets how many page links show on each side of the current one, 3 unless you set it.
|
|
177
|
+
|
|
178
|
+
### In Inertia pages
|
|
179
|
+
|
|
180
|
+
Pass the paginator as a prop. It reaches the page as Laravel's JSON shape, which `oriel inertia:types` types: the
|
|
181
|
+
`data`, the page numbers and URLs, and the `links`. `Inertia.scroll(paginator)` makes it a prop for infinite scrolling
|
|
182
|
+
(see [More Inertia features](inertia.md#more-inertia-features)).
|
|
183
|
+
|
|
184
|
+
### In Live components
|
|
185
|
+
|
|
186
|
+
A Live component pages with the `WithPagination` mixin, and shows its links with the `Pagination` component:
|
|
187
|
+
|
|
188
|
+
```tsx title="app/Live/ShowPosts.tsx"
|
|
189
|
+
import { Component, Pagination, WithPagination } from '@orieljs/framework'
|
|
190
|
+
import { Post } from '../Models/Post'
|
|
191
|
+
|
|
192
|
+
export default class ShowPosts extends WithPagination(Component) {
|
|
193
|
+
search = ''
|
|
194
|
+
|
|
195
|
+
async updatedSearch() {
|
|
196
|
+
await this.resetPage()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async render() {
|
|
200
|
+
const posts = await Post.whereLike('title', `%${this.search}%`).paginate(10)
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div>
|
|
204
|
+
<ul>
|
|
205
|
+
{posts.items().map((post) => (
|
|
206
|
+
<li>{post.title}</li>
|
|
207
|
+
))}
|
|
208
|
+
</ul>
|
|
209
|
+
<Pagination paginator={posts} />
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The page number is the component's `page`: its actions `nextPage`, `previousPage`, `gotoPage(n)` and `resetPage`
|
|
217
|
+
move it. It isn't written to the address bar, so a reload shows page 1. `Pagination` takes `paginate()` and
|
|
218
|
+
`simplePaginate()` paginators, not cursor ones.
|
|
219
|
+
|
|
220
|
+
## Converting results to JSON
|
|
221
|
+
|
|
222
|
+
A paginator returned from a route, or given to `JSON.stringify()`, becomes Laravel's JSON shape:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"current_page": 1,
|
|
227
|
+
"data": [{ "id": 1, "name": "Taylor" }],
|
|
228
|
+
"first_page_url": "http://localhost/users?page=1",
|
|
229
|
+
"from": 1,
|
|
230
|
+
"last_page": 4,
|
|
231
|
+
"last_page_url": "http://localhost/users?page=4",
|
|
232
|
+
"links": [{ "url": null, "label": "« Previous", "active": false }],
|
|
233
|
+
"next_page_url": "http://localhost/users?page=2",
|
|
234
|
+
"path": "http://localhost/users",
|
|
235
|
+
"per_page": 15,
|
|
236
|
+
"prev_page_url": null,
|
|
237
|
+
"to": 15,
|
|
238
|
+
"total": 50
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
A simple paginator has no `last_page`, `last_page_url`, `links` or `total`, and a cursor paginator has `next_cursor`
|
|
243
|
+
and `prev_cursor` in place of page numbers.
|
|
244
|
+
|
|
245
|
+
> [!NOTE]
|
|
246
|
+
> Laravel renders the links with Blade views you can publish and theme (`links()`, Tailwind and Bootstrap views).
|
|
247
|
+
> Oriel builds them in your TSX from `linkCollection()`, or with Live's `Pagination` component.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Resetting passwords
|
|
2
|
+
|
|
3
|
+
A user who forgot their password asks for a reset link by email, and sets a new password on the page it opens. The
|
|
4
|
+
`Password` facade does the work, and the starter kits come with the pages.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
Every starter kit lets a user reset their password: the kits with pages have a "Forgot password" page and a page to set
|
|
9
|
+
the new password, and the API kit has the two endpoints (see [Starter kits](starter-kits.md#authentication)). This page
|
|
10
|
+
explains what they do, so you can change them or write your own.
|
|
11
|
+
|
|
12
|
+
### Model preparation
|
|
13
|
+
|
|
14
|
+
The user model must be able to give its email address and to receive the reset mail. A model built on
|
|
15
|
+
`AuthenticatableModel`, as a new app's `User` is, does both: `getEmailForPasswordReset()` returns its `email`
|
|
16
|
+
attribute, and `sendPasswordResetNotification(token)` sends it the `ResetPassword` notification by mail.
|
|
17
|
+
|
|
18
|
+
### Database preparation
|
|
19
|
+
|
|
20
|
+
The reset tokens are stored in the `password_reset_tokens` table, which a new app's first migration creates: one row
|
|
21
|
+
per email address, with the token and when it was made. A new token for an address replaces the one before it.
|
|
22
|
+
|
|
23
|
+
### Configuring trusted hosts
|
|
24
|
+
|
|
25
|
+
> [!WARNING]
|
|
26
|
+
> The reset link in the mail starts with the scheme and host of the request that asked for it, as Laravel's does.
|
|
27
|
+
> Laravel can restrict the hosts it answers (`trustHosts()`); Oriel can't. Have your web server or proxy answer only
|
|
28
|
+
> your app's own host names, or build the link from `APP_URL` yourself, as
|
|
29
|
+
> [Reset link customization](#reset-link-customization) shows, so that a request with a forged `Host` header can't
|
|
30
|
+
> send your users a link to another site.
|
|
31
|
+
|
|
32
|
+
## Routing
|
|
33
|
+
|
|
34
|
+
### Requesting the password reset link
|
|
35
|
+
|
|
36
|
+
#### The password reset link request form
|
|
37
|
+
|
|
38
|
+
The kits with pages show the form at `/forgot-password`, the `password.request` route, behind the `guest` middleware.
|
|
39
|
+
It asks for the email address and posts it to the same URI.
|
|
40
|
+
|
|
41
|
+
#### Handling the form submission
|
|
42
|
+
|
|
43
|
+
`Password.sendResetLink()` takes the credentials that find the user, the email address here, and sends the user a
|
|
44
|
+
reset link:
|
|
45
|
+
|
|
46
|
+
```ts title="app/Http/Controllers/Auth/PasswordResetLinkController.ts"
|
|
47
|
+
import { __, back, Password, request } from '@orieljs/framework'
|
|
48
|
+
import { Controller } from '../Controller'
|
|
49
|
+
|
|
50
|
+
export class PasswordResetLinkController extends Controller {
|
|
51
|
+
/** Handle an incoming password reset link request. */
|
|
52
|
+
async store() {
|
|
53
|
+
await request().validate({
|
|
54
|
+
email: 'required|email',
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
await Password.sendResetLink(request().only('email'))
|
|
58
|
+
|
|
59
|
+
return back().with('status', __('A reset link will be sent if the account exists.'))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
It returns a status, which `__()` turns into a message:
|
|
65
|
+
|
|
66
|
+
| Status | Message |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `passwords.sent` | "We have emailed your password reset link." |
|
|
69
|
+
| `passwords.user` | "We can't find a user with that email address." |
|
|
70
|
+
| `passwords.throttled` | "Please wait before retrying." |
|
|
71
|
+
|
|
72
|
+
The kits answer with the same message whatever the status, as above, so the form can't be used to find out who has an
|
|
73
|
+
account.
|
|
74
|
+
|
|
75
|
+
A new link for an address is refused while the last one is younger than `throttle` seconds, 60 in a new app, so the
|
|
76
|
+
form can't be used to flood a user's inbox. With a function as the second argument, `sendResetLink()` calls it with
|
|
77
|
+
the user and the token instead of sending the mail.
|
|
78
|
+
|
|
79
|
+
### Resetting the password
|
|
80
|
+
|
|
81
|
+
#### The password reset form
|
|
82
|
+
|
|
83
|
+
The link in the mail opens `/reset-password/{token}?email=…`, the `password.reset` route. The page there asks for the
|
|
84
|
+
new password, twice, and posts it with the token and the email address to `/reset-password`, the `password.store`
|
|
85
|
+
route.
|
|
86
|
+
|
|
87
|
+
#### Handling the form submission
|
|
88
|
+
|
|
89
|
+
`Password.reset()` checks the token against the user's, then calls your function with the user and the new password,
|
|
90
|
+
and deletes the token. Your function saves the password, hashed:
|
|
91
|
+
|
|
92
|
+
```ts title="app/Http/Controllers/Auth/NewPasswordController.ts"
|
|
93
|
+
import { randomBytes } from 'node:crypto'
|
|
94
|
+
import { __, event, Hash, Password, PasswordReset, redirect, request, Rule, ValidationException } from '@orieljs/framework'
|
|
95
|
+
import type { User } from '../../../Models/User'
|
|
96
|
+
import { Controller } from '../Controller'
|
|
97
|
+
|
|
98
|
+
export class NewPasswordController extends Controller {
|
|
99
|
+
/** Handle an incoming new password request. */
|
|
100
|
+
async store() {
|
|
101
|
+
await request().validate({
|
|
102
|
+
token: 'required',
|
|
103
|
+
email: 'required|email',
|
|
104
|
+
password: ['required', 'confirmed', Rule.password()],
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const status = await Password.reset(
|
|
108
|
+
request().only('email', 'password', 'password_confirmation', 'token'),
|
|
109
|
+
async (user, password) => {
|
|
110
|
+
await (user as User)
|
|
111
|
+
.forceFill({
|
|
112
|
+
password: await Hash.make(password),
|
|
113
|
+
remember_token: randomBytes(30).toString('hex'),
|
|
114
|
+
})
|
|
115
|
+
.save()
|
|
116
|
+
|
|
117
|
+
await event(new PasswordReset(user))
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if (status === 'passwords.reset') {
|
|
122
|
+
return redirect().route('login').with('status', __(status))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// An unknown email answers like a bad token, so the reset can't reveal whether an account exists.
|
|
126
|
+
throw ValidationException.withMessages({ email: [__(status === 'passwords.user' ? 'passwords.token' : status)] })
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`reset()` returns `passwords.reset` ("Your password has been reset."), `passwords.token` ("This password reset token is
|
|
132
|
+
invalid.") for a wrong or expired token, or `passwords.user` when no user has the address. A token lasts `expire`
|
|
133
|
+
minutes, 60 in a new app. The new `remember_token` signs the user out of the browsers where they chose "Remember me".
|
|
134
|
+
|
|
135
|
+
In the Live kit, the two pages are the `ForgotPassword` and `ResetPassword` components, which do the same in their
|
|
136
|
+
actions. The API kit's endpoints answer JSON, and its reset also revokes every API token of the user.
|
|
137
|
+
|
|
138
|
+
A model with the `hashed` cast on `password` can take the plain password in the callback, in place of
|
|
139
|
+
`Hash.make()`.
|
|
140
|
+
|
|
141
|
+
## Deleting expired tokens
|
|
142
|
+
|
|
143
|
+
An expired token can't be used, but its row stays until the user asks for another link. Laravel's
|
|
144
|
+
`auth:clear-resets` command isn't in Oriel; the token repository's `deleteExpired()` deletes them, so schedule it in
|
|
145
|
+
`routes/console.ts` if you want the table kept small:
|
|
146
|
+
|
|
147
|
+
```ts title="routes/console.ts"
|
|
148
|
+
import { Password, Schedule } from '@orieljs/framework'
|
|
149
|
+
|
|
150
|
+
Schedule.call(() => Password.broker().tokens.deleteExpired())
|
|
151
|
+
.everyFifteenMinutes()
|
|
152
|
+
.name('clear-password-resets')
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Customization
|
|
156
|
+
|
|
157
|
+
The broker's settings are under `passwords` in `config/auth.ts`:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const auth = {
|
|
161
|
+
passwords: { users: { provider: 'users', table: 'password_reset_tokens', expire: 60, throttle: 60 } },
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`expire` is in minutes and `throttle` in seconds. `Password.broker(name)` uses another entry of `passwords`, with its
|
|
166
|
+
own table or user provider.
|
|
167
|
+
|
|
168
|
+
#### Reset link customization
|
|
169
|
+
|
|
170
|
+
`ResetPassword.createUrlUsing()` builds the link yourself, in a service provider's `boot()`. This one starts it with
|
|
171
|
+
`APP_URL`, whatever host the request came to:
|
|
172
|
+
|
|
173
|
+
```ts title="app/Providers/AppServiceProvider.ts"
|
|
174
|
+
import { config, ResetPassword, ServiceProvider } from '@orieljs/framework'
|
|
175
|
+
|
|
176
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
177
|
+
override boot(): void {
|
|
178
|
+
ResetPassword.createUrlUsing((user, token) => {
|
|
179
|
+
const email = encodeURIComponent(user.getEmailForPasswordReset())
|
|
180
|
+
|
|
181
|
+
return `${String(config('app.url'))}/reset-password/${token}?email=${email}`
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A client app on another domain, such as one using the API kit, points the link at its own page the same way.
|
|
188
|
+
|
|
189
|
+
#### Reset email customization
|
|
190
|
+
|
|
191
|
+
`ResetPassword.toMailUsing()` builds the mail yourself: return a `MailMessage` with your own lines, or a
|
|
192
|
+
[mailable](mail.md). Its function receives the user and the token:
|
|
193
|
+
|
|
194
|
+
```ts title="app/Providers/AppServiceProvider.ts"
|
|
195
|
+
import { MailMessage, ResetPassword, ServiceProvider, url } from '@orieljs/framework'
|
|
196
|
+
|
|
197
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
198
|
+
override boot(): void {
|
|
199
|
+
ResetPassword.toMailUsing((user, token) =>
|
|
200
|
+
new MailMessage()
|
|
201
|
+
.subject('Reset your password')
|
|
202
|
+
.line('Someone asked to reset the password of your account.')
|
|
203
|
+
.action('Choose a new password', url(`/reset-password/${token}`, { email: user.getEmailForPasswordReset() })),
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Service providers
|
|
2
|
+
|
|
3
|
+
Service providers set up your app and every part of Oriel: they bind services in the container, then configure them
|
|
4
|
+
once every provider has registered.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
Service providers are the central place of your app's setup. Your own app, and each of Oriel's components, start
|
|
9
|
+
through them. Setting up here means registering things: bindings in the [service container](container.md), event
|
|
10
|
+
listeners, view composers, route patterns and bindings.
|
|
11
|
+
|
|
12
|
+
Oriel uses 22 service providers of its own, for the mailer, the queue, the cache and the rest. [Request
|
|
13
|
+
lifecycle](lifecycle.md#service-providers) lists them, and shows where providers fit in how your app starts. Your own
|
|
14
|
+
providers are listed in `bootstrap/providers.ts`. This page shows how to write them and register them.
|
|
15
|
+
|
|
16
|
+
## Writing service providers
|
|
17
|
+
|
|
18
|
+
A service provider extends the `ServiceProvider` class. Most have a `register()` method and a `boot()` method. In
|
|
19
|
+
`register()`, only bind things into the service container: never listen for events or set anything else up there, as
|
|
20
|
+
the providers that offer what you would use may not have registered yet.
|
|
21
|
+
|
|
22
|
+
`make:provider` writes a new provider in `app/Providers`:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
node oriel make:provider Riak
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It adds `ServiceProvider` to the name when the name doesn't end with it, so this writes
|
|
29
|
+
`app/Providers/RiakServiceProvider.ts`, with an empty `register()` and `boot()`:
|
|
30
|
+
|
|
31
|
+
```ts title="app/Providers/RiakServiceProvider.ts"
|
|
32
|
+
import { ServiceProvider } from '@orieljs/framework'
|
|
33
|
+
|
|
34
|
+
export class RiakServiceProvider extends ServiceProvider {
|
|
35
|
+
override register(): void {
|
|
36
|
+
//
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override boot(): void {
|
|
40
|
+
//
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`make:provider` doesn't register the provider: add it to `bootstrap/providers.ts` yourself, as [Registering
|
|
46
|
+
providers](#registering-providers) shows. `--force` overwrites a provider that exists.
|
|
47
|
+
|
|
48
|
+
> [!NOTE]
|
|
49
|
+
> Laravel's `make:provider` adds the new provider to `bootstrap/providers.php` for you. Oriel's writes the class only.
|
|
50
|
+
|
|
51
|
+
### The register method
|
|
52
|
+
|
|
53
|
+
In `register()`, bind things into the service container, and nothing else. Every provider method has the
|
|
54
|
+
application, which is also the container, in `this.app`:
|
|
55
|
+
|
|
56
|
+
```ts title="app/Providers/RiakServiceProvider.ts"
|
|
57
|
+
// @filename: app/Services/Riak/Connection.ts
|
|
58
|
+
export declare class Connection {
|
|
59
|
+
constructor(host: string, port: number)
|
|
60
|
+
}
|
|
61
|
+
// ---cut---
|
|
62
|
+
import { Config, ServiceProvider } from '@orieljs/framework'
|
|
63
|
+
import { Connection } from '../Services/Riak/Connection'
|
|
64
|
+
|
|
65
|
+
export class RiakServiceProvider extends ServiceProvider {
|
|
66
|
+
override register(): void {
|
|
67
|
+
this.app.singleton(Connection, () => new Connection(Config.string('riak.host'), Config.integer('riak.port')))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This provider only has a `register()` method, which tells the container how to build a `Connection`. [Service
|
|
73
|
+
container](container.md) covers the bindings.
|
|
74
|
+
|
|
75
|
+
`register()` is synchronous: Oriel doesn't wait for a promise it returns, and the next provider registers at once. Do
|
|
76
|
+
async work in `boot()`, which Oriel awaits. Your configuration is loaded before any provider registers, so
|
|
77
|
+
`register()` can read it.
|
|
78
|
+
|
|
79
|
+
> [!NOTE]
|
|
80
|
+
> Laravel's providers can list simple bindings in `$bindings` and `$singletons` properties. Oriel's have no such
|
|
81
|
+
> properties: call `this.app.bind()` and `this.app.singleton()` in `register()`.
|
|
82
|
+
|
|
83
|
+
#### Default configuration
|
|
84
|
+
|
|
85
|
+
A provider that ships with its own settings, as a package's does, gives them to your app with `mergeConfigFrom()` in
|
|
86
|
+
`register()`. It takes the defaults and the name of the configuration file they belong to:
|
|
87
|
+
|
|
88
|
+
```ts title="app/Providers/RiakServiceProvider.ts"
|
|
89
|
+
import { ServiceProvider } from '@orieljs/framework'
|
|
90
|
+
|
|
91
|
+
export class RiakServiceProvider extends ServiceProvider {
|
|
92
|
+
override register(): void {
|
|
93
|
+
this.mergeConfigFrom({ host: '127.0.0.1', port: 8087, options: { timeout: 5 } }, 'riak')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`config('riak.port')` is then 8087 unless your app's `config/riak.ts` sets a `port`, which wins. The merge is shallow,
|
|
99
|
+
as Laravel's is: each key your app sets replaces the default's whole value. If `config/riak.ts` sets
|
|
100
|
+
`options: { retries: 3 }`, the `options` are `{ retries: 3 }`, and `timeout` is gone.
|
|
101
|
+
|
|
102
|
+
### The boot method
|
|
103
|
+
|
|
104
|
+
`boot()` runs once every provider has registered, so every binding of the framework and of your app is in place.
|
|
105
|
+
Set things up there, such as a [view composer](views.md#view-composers):
|
|
106
|
+
|
|
107
|
+
```ts title="app/Providers/ComposerServiceProvider.ts"
|
|
108
|
+
import { ServiceProvider, View } from '@orieljs/framework'
|
|
109
|
+
|
|
110
|
+
export class ComposerServiceProvider extends ServiceProvider {
|
|
111
|
+
override boot(): void {
|
|
112
|
+
View.composer('profile', (view) => {
|
|
113
|
+
// ...
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`boot()` may be async: Oriel awaits it before it boots the next provider. The providers boot in the order they
|
|
120
|
+
registered, the framework's first, and your routes load after the last one has booted (see [Request
|
|
121
|
+
lifecycle](lifecycle.md#service-providers)).
|
|
122
|
+
|
|
123
|
+
An error thrown in `register()` or `boot()` stops your app from starting. Oriel throws a `ProviderRegisterError` or a
|
|
124
|
+
`ProviderBootError` that names the provider, with the original error as its `cause`: "Service provider
|
|
125
|
+
[RiakServiceProvider] failed to boot: Riak is down".
|
|
126
|
+
|
|
127
|
+
#### Boot method dependency injection
|
|
128
|
+
|
|
129
|
+
`boot()` takes no arguments. Resolve what it needs from the container, with `this.app.make()`:
|
|
130
|
+
|
|
131
|
+
```ts title="app/Providers/AppServiceProvider.ts"
|
|
132
|
+
import { ServiceProvider } from '@orieljs/framework'
|
|
133
|
+
import { ConfigRepository } from '@orieljs/framework/contracts'
|
|
134
|
+
|
|
135
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
136
|
+
override boot(): void {
|
|
137
|
+
const config = this.app.make(ConfigRepository)
|
|
138
|
+
|
|
139
|
+
if (config.get('app.debug')) {
|
|
140
|
+
// ...
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
> [!NOTE]
|
|
147
|
+
> Laravel injects the services that `boot()`'s parameters type-hint. TypeScript types are gone when your app runs,
|
|
148
|
+
> so Oriel calls `boot()` with no arguments: take what you need from `this.app`, or with `inject()`.
|
|
149
|
+
|
|
150
|
+
## Registering providers
|
|
151
|
+
|
|
152
|
+
Your providers are listed in `bootstrap/providers.ts`. It exports a list of the provider classes, which
|
|
153
|
+
`bootstrap/app.ts` gives to `withProviders()`:
|
|
154
|
+
|
|
155
|
+
```ts title="bootstrap/providers.ts"
|
|
156
|
+
import { AppServiceProvider } from '../app/Providers/AppServiceProvider'
|
|
157
|
+
|
|
158
|
+
export default [AppServiceProvider]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
To register a provider you wrote, add it to the list:
|
|
162
|
+
|
|
163
|
+
```ts title="bootstrap/providers.ts"
|
|
164
|
+
// @filename: app/Providers/ComposerServiceProvider.ts
|
|
165
|
+
import { ServiceProvider } from '@orieljs/framework'
|
|
166
|
+
export class ComposerServiceProvider extends ServiceProvider {}
|
|
167
|
+
// ---cut---
|
|
168
|
+
import { AppServiceProvider } from '../app/Providers/AppServiceProvider'
|
|
169
|
+
import { ComposerServiceProvider } from '../app/Providers/ComposerServiceProvider'
|
|
170
|
+
|
|
171
|
+
export default [AppServiceProvider, ComposerServiceProvider]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Oriel registers the framework's providers first, then yours, in the order of the list. A provider listed twice is
|
|
175
|
+
registered once.
|
|
176
|
+
|
|
177
|
+
`this.app.register()` registers a provider while your app runs, as a package's provider might for another one it
|
|
178
|
+
needs: `await this.app.register(RiakServiceProvider)`. If the app has booted already, the new provider boots at once.
|
|
179
|
+
|
|
180
|
+
## Deferred providers
|
|
181
|
+
|
|
182
|
+
Every provider registers and boots when your app starts: Oriel has no deferred providers.
|
|
183
|
+
|
|
184
|
+
> [!NOTE]
|
|
185
|
+
> Laravel can defer a provider that only binds services until one of them is needed, through `DeferrableProvider` and
|
|
186
|
+
> `provides()`, so that a request doesn't load providers it never uses. An Oriel app boots once per process, not once
|
|
187
|
+
> per request, so each provider's cost is paid once: Oriel loads them all.
|