@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.
- package/NOTICE +7 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-WNJP46B7.js → chunk-4MWG3B7G.js} +1785 -626
- package/dist/chunk-4MWG3B7G.js.map +1 -0
- package/dist/chunk-7ZQZYDOC.js +3253 -0
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-BSQKOLSK.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-A5XRLEWM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +535 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +2202 -5952
- package/dist/index.js +4900 -944
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-WKTl3EEw.d.ts → jsx-runtime-6um4Mwfb.d.ts} +97 -51
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BErengYY.d.ts → testing-CKwfWJGn.d.ts} +313 -994
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +19 -5
- 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-A5XRLEWM.js.map +0 -1
- package/dist/chunk-BSQKOLSK.js.map +0 -1
- package/dist/chunk-PM5YTG57.js +0 -880
- package/dist/chunk-PM5YTG57.js.map +0 -1
- package/dist/chunk-WNJP46B7.js.map +0 -1
- package/dist/index.d-DOlTeG-8.d.ts +0 -99
package/docs/facades.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Facades
|
|
2
|
+
|
|
3
|
+
Facades give you Oriel's services by name, as `Cache.get('key')`, resolved from the current request or the
|
|
4
|
+
application each time you use them.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
Code throughout these pages reaches Oriel's features through facades. A facade gives you a service of the [service
|
|
9
|
+
container](container.md) under a short name, as if its methods were static, and forwards each call to the service the
|
|
10
|
+
container holds. Oriel has a facade for most of its features, and exports them all from `@orieljs/framework`:
|
|
11
|
+
|
|
12
|
+
```ts title="routes/web.ts"
|
|
13
|
+
import { Cache, Route } from '@orieljs/framework'
|
|
14
|
+
|
|
15
|
+
Route.get('/cache', () => Cache.get('key'))
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
#### Helper functions
|
|
19
|
+
|
|
20
|
+
Alongside the facades, Oriel has helper functions for the features you use most: `view()`, `response()`, `config()`,
|
|
21
|
+
`request()`, `session()` and others. Each feature's page documents its helper, and [Helpers](helpers.md) lists them all.
|
|
22
|
+
A helper and its facade reach the same service, so these two are the same:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const fromFacade = View.make('profile')
|
|
26
|
+
|
|
27
|
+
const fromHelper = view('profile')
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## When to use facades
|
|
31
|
+
|
|
32
|
+
Facades are short to write: a feature's name, then its method, with nothing to inject or configure. Because they ask
|
|
33
|
+
for nothing, a class can come to use many of them, and grow past one job without you noticing. When a class reaches
|
|
34
|
+
for many facades, consider splitting it into smaller classes.
|
|
35
|
+
|
|
36
|
+
### Facades vs. dependency injection
|
|
37
|
+
|
|
38
|
+
A service you inject can be replaced in a test by a stand-in. A facade's service can be replaced the same way, since the
|
|
39
|
+
facade resolves it from the container at each call: your tests give the container a fake, and the code under test uses
|
|
40
|
+
it through the facade.
|
|
41
|
+
|
|
42
|
+
The facades of the services you most often check in a test have a `fake()`: `Mail.fake()`, `Queue.fake()`,
|
|
43
|
+
`Bus.fake()`, `Event.fake()`, `Storage.fake()` and `Notification.fake()`. Each puts a fake in place of the service
|
|
44
|
+
and returns it, and you check what your code did on the fake it returned:
|
|
45
|
+
|
|
46
|
+
```ts title="tests/Feature/ShipOrder.test.ts"
|
|
47
|
+
// @filename: tests/TestCase.ts
|
|
48
|
+
import { TestCase as BaseTestCase } from '@orieljs/framework/testing'
|
|
49
|
+
export class TestCase extends BaseTestCase {}
|
|
50
|
+
// @filename: app/Mail/OrderShipped.ts
|
|
51
|
+
import { Mailable } from '@orieljs/framework'
|
|
52
|
+
export class OrderShipped extends Mailable {}
|
|
53
|
+
// ---cut---
|
|
54
|
+
import { Mail } from '@orieljs/framework'
|
|
55
|
+
import { test } from 'vitest'
|
|
56
|
+
import { OrderShipped } from '../../app/Mail/OrderShipped'
|
|
57
|
+
import { TestCase } from '../TestCase'
|
|
58
|
+
|
|
59
|
+
const t = TestCase.boot()
|
|
60
|
+
|
|
61
|
+
test('shipping an order sends the mail', async () => {
|
|
62
|
+
const mail = Mail.fake()
|
|
63
|
+
|
|
64
|
+
await t.post('/orders/1/ship')
|
|
65
|
+
|
|
66
|
+
mail.assertSent(OrderShipped)
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`Storage.fake('avatars')` returns the fake disk. [Fakes](mocking.md) lists every fake and its assertions.
|
|
71
|
+
|
|
72
|
+
For any other facade, `swap()` puts an object of your own in place of the service:
|
|
73
|
+
|
|
74
|
+
```ts title="tests/Feature/PublishPodcast.test.ts"
|
|
75
|
+
// @filename: tests/TestCase.ts
|
|
76
|
+
import { TestCase as BaseTestCase } from '@orieljs/framework/testing'
|
|
77
|
+
export class TestCase extends BaseTestCase {}
|
|
78
|
+
// @filename: app/Facades/Podcasts.ts
|
|
79
|
+
import { facade } from '@orieljs/framework'
|
|
80
|
+
export declare class PodcastPublisher {
|
|
81
|
+
publish(podcast: import('../Models/Podcast').Podcast): Promise<void>
|
|
82
|
+
}
|
|
83
|
+
export const Podcasts = facade(PodcastPublisher)
|
|
84
|
+
// ---cut---
|
|
85
|
+
import { RefreshDatabase } from '@orieljs/framework/testing'
|
|
86
|
+
import { expect, test } from 'vitest'
|
|
87
|
+
import { Podcasts } from '../../app/Facades/Podcasts'
|
|
88
|
+
import { Podcast } from '../../app/Models/Podcast'
|
|
89
|
+
import { TestCase } from '../TestCase'
|
|
90
|
+
|
|
91
|
+
const t = TestCase.boot({ uses: [RefreshDatabase] })
|
|
92
|
+
|
|
93
|
+
test('a podcast can be published', async () => {
|
|
94
|
+
const podcast = await Podcast.create({ title: 'Episode 1', audio_path: 'podcasts/1.mp3' })
|
|
95
|
+
const published: number[] = []
|
|
96
|
+
Podcasts.swap({ publish: async (episode) => void published.push(episode.id) })
|
|
97
|
+
|
|
98
|
+
await t.post(`/podcasts/${podcast.id}/publish`)
|
|
99
|
+
|
|
100
|
+
expect(published).toEqual([podcast.id])
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`swap()` binds the object as an instance on the current container. In a test, outside any request, that is the test's
|
|
105
|
+
application, so every request the test makes uses it. Inside a request, it replaces the service for that request
|
|
106
|
+
only.
|
|
107
|
+
|
|
108
|
+
> [!NOTE]
|
|
109
|
+
> Laravel's facades can be mocked with Mockery: `Cache::shouldReceive('get')`, `spy()`, `partialMock()`. Oriel's
|
|
110
|
+
> have no mocking methods. Use the facade's `fake()` where it has one, and `swap()` with a stand-in of your own
|
|
111
|
+
> otherwise.
|
|
112
|
+
|
|
113
|
+
### Facades vs. helper functions
|
|
114
|
+
|
|
115
|
+
A helper does what its facade does: `view('profile')` and `View.make('profile')` both ask the view factory for the
|
|
116
|
+
view. Both resolve the same service from the container, so a fake or a `swap()` reaches the code that uses the
|
|
117
|
+
helper too:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
Route.get('/cache', () => cache('key'))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`cache('key')` calls `get()` on the cache manager the `Cache` facade stands for, so a test that swaps the `Cache`
|
|
124
|
+
facade's service changes what this route returns.
|
|
125
|
+
|
|
126
|
+
## How facades work
|
|
127
|
+
|
|
128
|
+
A facade is an object that `facade()` makes from a class or a
|
|
129
|
+
[token](container.md#binding-interfaces-to-implementations). Each time you read one of its properties, it resolves the
|
|
130
|
+
service from the current container and returns the service's property, its methods bound to the service. The current
|
|
131
|
+
container is the one `inject()` uses (see [Injecting with `inject()`](container.md#injecting-with-inject)): inside a
|
|
132
|
+
request, a queued job or a command, its scope; otherwise, the application. So `Session` reaches the current request's
|
|
133
|
+
session, and `Mail` the mail manager your app shares.
|
|
134
|
+
|
|
135
|
+
You can make a facade for a service of your own:
|
|
136
|
+
|
|
137
|
+
```ts title="app/Facades/Podcasts.ts"
|
|
138
|
+
// @filename: app/Services/PodcastPublisher.ts
|
|
139
|
+
export declare class PodcastPublisher {
|
|
140
|
+
publish(podcast: import('../Models/Podcast').Podcast): Promise<void>
|
|
141
|
+
}
|
|
142
|
+
// ---cut---
|
|
143
|
+
import { facade } from '@orieljs/framework'
|
|
144
|
+
import { PodcastPublisher } from '../Services/PodcastPublisher'
|
|
145
|
+
|
|
146
|
+
export const Podcasts = facade(PodcastPublisher)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
From then on, `Podcasts.publish(podcast)` calls `publish()` on the `PodcastPublisher` the container gives you, a new
|
|
150
|
+
one each time unless you bind it as a singleton. `facade()` takes a second argument, an object of extra methods,
|
|
151
|
+
which is how `Mail` has `fake()` and `Hash` has `driver()`.
|
|
152
|
+
|
|
153
|
+
> [!NOTE]
|
|
154
|
+
> A Laravel facade is a class whose `getFacadeAccessor()` names a binding, and whose static calls `__callStatic()`
|
|
155
|
+
> forwards to it. Oriel's is an object made by `facade()`, which forwards every property you read, and needs no
|
|
156
|
+
> class of its own.
|
|
157
|
+
|
|
158
|
+
### Typing
|
|
159
|
+
|
|
160
|
+
A facade is typed as the service it stands for, with its extra methods and two of its own: `getFacadeRoot()`, which
|
|
161
|
+
returns the service, and `swap()`. TypeScript checks every call:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
Route.get('/podcasts', () => 'Podcasts')
|
|
165
|
+
|
|
166
|
+
// @ts-expect-error: the router has no post2()
|
|
167
|
+
Route.post2('/podcasts', () => 'Created')
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`Route` is typed as Oriel's router, so a misspelt method or a wrong argument fails the type check, and your editor
|
|
171
|
+
completes the facade's methods. The `Facade<T>` type, exported from `@orieljs/framework`, names the type of a facade
|
|
172
|
+
you write yourself.
|
|
173
|
+
|
|
174
|
+
### Facades at a module's top level
|
|
175
|
+
|
|
176
|
+
A facade resolves its service when you use it, so it works only while your app is running. Don't use one at a
|
|
177
|
+
module's top level, outside any function:
|
|
178
|
+
|
|
179
|
+
```ts title="app/Services/Greeting.ts"
|
|
180
|
+
import { Config } from '@orieljs/framework'
|
|
181
|
+
|
|
182
|
+
// Runs when the module is first imported: before your app exists, this throws.
|
|
183
|
+
const appName = Config.string('app.name')
|
|
184
|
+
|
|
185
|
+
export function greeting(): string {
|
|
186
|
+
return `Welcome to ${appName}`
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
A module's top level runs once, when the module is first imported. Your providers, and what they import, are imported
|
|
191
|
+
with `bootstrap/app.ts`, before `createApplication()` has made the app, and a test file's imports run before its first
|
|
192
|
+
test has an app. Then no container is available, and the facade throws a `ContainerNotAvailableError`: "Cannot resolve
|
|
193
|
+
[Config.Repository]: no container is available. inject(), helpers and facades only work while the application is
|
|
194
|
+
running (inside a request, job, command, provider or app.run()). Don't call them at module top level."
|
|
195
|
+
|
|
196
|
+
When an app does exist, the top level resolves that app's service, once, and keeps it: your tests make a new app for
|
|
197
|
+
each test, and every test would see the first test's value. Use the facade inside the function instead:
|
|
198
|
+
|
|
199
|
+
```ts title="app/Services/Greeting.ts"
|
|
200
|
+
import { Config } from '@orieljs/framework'
|
|
201
|
+
|
|
202
|
+
export function greeting(): string {
|
|
203
|
+
return `Welcome to ${Config.string('app.name')}`
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The route files are the exception: the app imports each one itself, afresh for every app and inside it, so a route
|
|
208
|
+
file calls the `Route` facade at its top level.
|
|
209
|
+
|
|
210
|
+
## Real-time facades
|
|
211
|
+
|
|
212
|
+
Oriel has no real-time facades, which Laravel makes from any class you import with a `Facades\` namespace prefix.
|
|
213
|
+
Make a facade for your class with `facade()`, as in [How facades work](#how-facades-work).
|
|
214
|
+
|
|
215
|
+
## Facade class reference
|
|
216
|
+
|
|
217
|
+
Every facade, and what it resolves. The bindings are the tokens of `@orieljs/framework/contracts`, which `inject()`
|
|
218
|
+
and `app().make()` also take:
|
|
219
|
+
|
|
220
|
+
| Facade | Resolves to | Binding |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| `Artisan` | The console kernel: `call()` runs a command | `ConsoleKernel` |
|
|
223
|
+
| `Auth` | The auth manager: `guard()`, and the default guard's `user()`, `check()`, `attempt()`… | `AuthFactory` |
|
|
224
|
+
| `Bus` | The command bus: `dispatch()`, `chain()`, `batch()` | `BusDispatcher` |
|
|
225
|
+
| `Cache` | The cache manager: `store()`, and the default store's methods | `CacheManager` |
|
|
226
|
+
| `Config` | The configuration repository (see [The `Config` facade](#the-config-facade)) | `ConfigRepository` |
|
|
227
|
+
| `Cookie` | The current request's cookie jar, plus `get()` and `has()` for the request's cookies | `CookieJar` (scoped) |
|
|
228
|
+
| `Crypt` | The encrypter | `Encrypter` |
|
|
229
|
+
| `DB` | The database manager: `connection()`, and the default connection's methods; `DB.raw()` | `DatabaseManager` |
|
|
230
|
+
| `Event` | The event dispatcher | `EventDispatcher` |
|
|
231
|
+
| `Gate` | The authorization gate | `Gate` |
|
|
232
|
+
| `Hash` | The hash manager, which hashes with the default driver; `driver()` | `Hasher` |
|
|
233
|
+
| `Inertia` | The current request's Inertia response factory | `InertiaFactory` (scoped) |
|
|
234
|
+
| `Live` | The Live component manager: `Live.component()` registers a component; `Live.test()` mounts one in a test | `LiveManager` |
|
|
235
|
+
| `Log` | The log manager: `channel()`, and the default channel's methods | `LogManager` |
|
|
236
|
+
| `Mail` | The mail manager: `mailer()`, and the default mailer's methods | `MailManager` |
|
|
237
|
+
| `Notification` | The notification dispatcher, through the statics of the `Notification` class | `NotificationDispatcher` |
|
|
238
|
+
| `Password` | The default password broker; `broker()` | `PasswordBroker` |
|
|
239
|
+
| `Queue` | The queue manager: `connection()`, and the default connection's methods | `QueueManager` |
|
|
240
|
+
| `RateLimiter` | The rate limiter, which `throttle` uses: `hit()`, `tooManyAttempts()`, `clear()` | `RateLimiter` |
|
|
241
|
+
| `Route` | The router | `Router` |
|
|
242
|
+
| `Schedule` | The schedule: `command()`, `job()`, `call()` | `Schedule` |
|
|
243
|
+
| `Schema` | The default database connection's schema builder | `SchemaBuilderToken`, from `@orieljs/framework` |
|
|
244
|
+
| `Session` | The current request's session; `Session.extend()` | `Session` (scoped) |
|
|
245
|
+
| `Storage` | The filesystem manager: `disk()`, and the default disk's methods | `FilesystemManager` |
|
|
246
|
+
| `Validator` | The validation factory: `make()`, `extend()` | `ValidationFactory` |
|
|
247
|
+
| `View` | The view factory: `make()`, `share()`, `composer()` | `ViewFactory` |
|
|
248
|
+
|
|
249
|
+
A scoped facade needs a scope. Outside any, `Cookie` and `Session` throw a `BindingResolutionError` ("Scoped
|
|
250
|
+
binding [Session.Store] was resolved outside a scope. …"), and `Inertia`, outside a request, uses the application's
|
|
251
|
+
own response factory.
|
|
252
|
+
|
|
253
|
+
`Inertia` and `Live` are written by hand rather than with `facade()`, and `Live` is also the `<Live>` element of your
|
|
254
|
+
views. `Notification` is the base class of your notifications, whose statics are the facade (`NotificationFacade`,
|
|
255
|
+
also exported, is a plain facade over the same dispatcher). None of the three has `getFacadeRoot()`, and only
|
|
256
|
+
`Notification` has `swap()`.
|
|
257
|
+
|
|
258
|
+
> [!NOTE]
|
|
259
|
+
> Laravel's `App`, `Request`, `Response`, `Redirect`, `URL`, `Lang` and `Vite` facades are helpers in Oriel: `app()`,
|
|
260
|
+
> `request()`, `response()`, `redirect()`, `url()`, `__()` and `vite()`. Oriel has no facade for Laravel's `Blade`,
|
|
261
|
+
> `Broadcast`, `Context`, `Date`, `Exceptions`, `File`, `Http`, `Pipeline`, `Process` or `Redis`.
|
|
262
|
+
|
|
263
|
+
### The `Config` facade
|
|
264
|
+
|
|
265
|
+
`Config` resolves your app's configuration, as the `config()` helper does. It has the repository's `get()`, `set()`,
|
|
266
|
+
`has()`, `push()` and `all()`, and adds `prepend()`, `getMany()` and typed getters:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const name = Config.string('app.name')
|
|
270
|
+
|
|
271
|
+
const debug = Config.boolean('app.debug', false)
|
|
272
|
+
|
|
273
|
+
const values = Config.getMany(['app.name', 'app.env'])
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`string()`, `integer()`, `float()`, `boolean()` and `array()` return the value typed, or throw a `TypeError` when it
|
|
277
|
+
isn't of that type: "Configuration value for key [app.name] must be a string, undefined given.". `float()` takes any
|
|
278
|
+
number, and `array()` a list or an object. [Configuration](configuration.md) covers your configuration files.
|
package/docs/hashing.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Hashing
|
|
2
|
+
|
|
3
|
+
The `Hash` facade hashes your users' passwords with bcrypt or Argon2, and checks a password against its hash.
|
|
4
|
+
|
|
5
|
+
## Introduction
|
|
6
|
+
|
|
7
|
+
A password is stored as a hash, which can't be turned back into the password: to sign a user in, Oriel hashes what they
|
|
8
|
+
typed and compares the two. Bcrypt, the default, is slow on purpose, and its work factor can rise as computers get
|
|
9
|
+
faster, so guessing passwords from a stolen hash stays costly. The starter kits hash with bcrypt when users register,
|
|
10
|
+
and check the hash when they sign in.
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
`config/hashing.ts` names the driver, `bcrypt` unless `HASH_DRIVER` names another, and each driver's settings:
|
|
15
|
+
|
|
16
|
+
```ts title="config/hashing.ts"
|
|
17
|
+
import { env } from '@orieljs/framework'
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
driver: env('HASH_DRIVER', 'bcrypt'),
|
|
21
|
+
bcrypt: { rounds: Number(env('BCRYPT_ROUNDS', 12)), verify: true },
|
|
22
|
+
argon: { memory: 65536, threads: 1, time: 4, verify: true },
|
|
23
|
+
rehash_on_login: true,
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The drivers are `bcrypt`, `argon` (Argon2i) and `argon2id`. Bcrypt's `rounds` is its work factor; Argon2's `memory`
|
|
28
|
+
(in KiB), `time` and `threads` are its. The settings may be numbers or the strings `env()` gives, such as `'12'`.
|
|
29
|
+
|
|
30
|
+
Your tests run with `BCRYPT_ROUNDS=4`, so the hashing in them is quick.
|
|
31
|
+
|
|
32
|
+
## Basic usage
|
|
33
|
+
|
|
34
|
+
### Hashing passwords
|
|
35
|
+
|
|
36
|
+
`Hash.make()` hashes a value with your driver. It returns a promise:
|
|
37
|
+
|
|
38
|
+
```ts title="app/Http/Controllers/PasswordController.ts"
|
|
39
|
+
import { Hash, redirect, request } from '@orieljs/framework'
|
|
40
|
+
import type { User } from '../../Models/User'
|
|
41
|
+
import { Controller } from './Controller'
|
|
42
|
+
|
|
43
|
+
export class PasswordController extends Controller {
|
|
44
|
+
/** Update the password for the user. */
|
|
45
|
+
async update() {
|
|
46
|
+
// Validate the new password length...
|
|
47
|
+
|
|
48
|
+
const user = (await request().user<User>())!
|
|
49
|
+
|
|
50
|
+
await user.fill({ password: await Hash.make(request().string('newPassword').toString()) }).save()
|
|
51
|
+
|
|
52
|
+
return redirect('/profile')
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A model can do this for you: an attribute with the `hashed` cast is hashed when the model is saved, and a value that is
|
|
58
|
+
already a hash made with your current settings is kept. [Hashed casting](eloquent-mutators.md#hashed-casting) describes
|
|
59
|
+
it.
|
|
60
|
+
|
|
61
|
+
#### Adjusting the work factor
|
|
62
|
+
|
|
63
|
+
`Hash.make()` takes the value only. Set the work factor in `config/hashing.ts`, or, for bcrypt, pass it to the
|
|
64
|
+
`bcrypt()` helper, which hashes with bcrypt whatever your driver is:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const hashed = await bcrypt('password', { rounds: 12 })
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> [!NOTE]
|
|
71
|
+
> Laravel's `Hash::make()` takes the work factor as a second argument. Oriel's `Hash.make()` reads it from your
|
|
72
|
+
> config, and `bcrypt()` takes `rounds`.
|
|
73
|
+
|
|
74
|
+
### Verifying that a password matches a hash
|
|
75
|
+
|
|
76
|
+
`Hash.check()` hashes a password and compares it with a hash. It returns a promise too:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
declare const hashedPassword: string
|
|
80
|
+
// ---cut---
|
|
81
|
+
if (await Hash.check('plain-text', hashedPassword)) {
|
|
82
|
+
// The passwords match...
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
An empty hash never matches.
|
|
87
|
+
|
|
88
|
+
### Determining if a password needs to be rehashed
|
|
89
|
+
|
|
90
|
+
`Hash.needsRehash()` tells you whether a hash was made with other settings than your current ones, such as fewer
|
|
91
|
+
rounds or another algorithm. It is synchronous:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
declare let hashed: string
|
|
95
|
+
// ---cut---
|
|
96
|
+
if (Hash.needsRehash(hashed)) {
|
|
97
|
+
hashed = await Hash.make('plain-text')
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
With `rehash_on_login` on, as in a new app, signing a user in with `Auth.attempt()` does this for you: when the
|
|
102
|
+
password matches and its hash needs rehashing, the user's password is hashed again and saved.
|
|
103
|
+
|
|
104
|
+
### Other methods
|
|
105
|
+
|
|
106
|
+
- `Hash.info(hashed)` gives a hash's algorithm and options, such as `{ algorithm: 'bcrypt', options: { cost: 12 } }`.
|
|
107
|
+
- `Hash.isHashed(value)` tells you whether a value is a bcrypt or Argon2 hash.
|
|
108
|
+
- `Hash.driver('argon2id')` returns another driver, to hash or check with it.
|
|
109
|
+
|
|
110
|
+
Bcrypt hashes start with `$2y$`, as PHP's `password_hash()` writes them, so the hashes of a Laravel app's users check
|
|
111
|
+
in Oriel, and the other way round.
|
|
112
|
+
|
|
113
|
+
## Hash algorithm verification
|
|
114
|
+
|
|
115
|
+
With `verify` on, as in a new app, `Hash.check()` refuses a hash made by another algorithm than the driver's: it
|
|
116
|
+
throws a `HashAlgorithmError`, "This password does not use the Bcrypt algorithm.", rather than return `false`. This
|
|
117
|
+
stops a hash of another kind from being checked in a way it wasn't meant for. When you move your users from one
|
|
118
|
+
algorithm to another, set `verify: false` in the driver's settings while their old hashes remain.
|