@rudderjs/auth 5.0.1 → 5.1.1

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/README.md CHANGED
@@ -15,31 +15,13 @@ pnpm add @rudderjs/auth @rudderjs/hash @rudderjs/session
15
15
  import { User } from '../app/Models/User.js'
16
16
 
17
17
  export default {
18
- defaults: {
19
- guard: 'web',
20
- },
21
- guards: {
22
- web: { driver: 'session', provider: 'users' },
23
- },
24
- providers: {
25
- users: { driver: 'eloquent', model: User },
26
- },
18
+ defaults: { guard: 'web' },
19
+ guards: { web: { driver: 'session', provider: 'users' } },
20
+ providers:{ users: { driver: 'eloquent', model: User } },
27
21
  }
28
-
29
- // bootstrap/providers.ts
30
- import { SessionProvider } from '@rudderjs/session'
31
- import { HashProvider } from '@rudderjs/hash'
32
- import { AuthProvider } from '@rudderjs/auth'
33
-
34
- export default [
35
- SessionProvider,
36
- HashProvider,
37
- AuthProvider,
38
- ]
39
22
  ```
40
23
 
41
- > `AuthProvider` is the service-provider class list it directly in the providers array.
42
- > `auth()` (lowercase) is the per-request helper — see below.
24
+ `AuthProvider`, `SessionProvider`, and `HashProvider` are picked up by [auto-discovery](https://github.com/rudderjs/rudder/blob/main/docs/guide/service-providers.md#auto-discovery)`pnpm rudder providers:discover` (or `--install` during scaffold) is all that's needed.
43
25
 
44
26
  ## Usage
45
27
 
@@ -70,6 +52,27 @@ Route.get('/profile', async (req) => {
70
52
  matched by `withRouting({ web })` has the auth context populated before your
71
53
  handler runs.
72
54
 
55
+ ### Reading the user from a view
56
+
57
+ When `@rudderjs/vite` is installed, `AuthProvider.boot()` also registers a
58
+ page-context enhancer that exposes the current user on `pageContext.user` —
59
+ controller views (and any Vike page) can read it directly without a
60
+ `+data.ts` or controller plumbing:
61
+
62
+ ```tsx
63
+ // app/Views/Dashboard.tsx
64
+ import { usePageContext } from 'vike-react/usePageContext'
65
+
66
+ export default function Dashboard() {
67
+ const { user } = usePageContext() // typed via Vike.PageContext augmentation
68
+ return <h1>Hello {user?.name ?? 'guest'}</h1>
69
+ }
70
+ ```
71
+
72
+ `pageContext.user` is `null` for guests. The registration is lazy and silently
73
+ no-ops when `@rudderjs/vite` isn't installed, so the package stays usable
74
+ standalone.
75
+
73
76
  **API routes stay stateless.** `AuthMiddleware` does not run on the `api` group
74
77
  by default — `req.user` will be `undefined`, and `Auth.user()` returns `null`.
75
78
  For token-based API auth, reach for [`@rudderjs/passport`](../passport):
@@ -1,273 +1,40 @@
1
1
  ---
2
2
  name: auth-setup
3
3
  description: Setting up authentication with guards, sessions, registration, password reset, gates/policies, and vendor views in RudderJS
4
+ license: MIT
5
+ appliesTo:
6
+ - '@rudderjs/auth'
7
+ trigger: configuring guards/providers in `config/auth.ts`, vendoring auth views, wiring `Gate`/`Policy`, or working with password reset / email verification
8
+ skip: a route handler that just reads `Auth.user()` — no setup needed
9
+ metadata:
10
+ author: rudderjs
4
11
  ---
5
12
 
6
13
  # Auth Setup
7
14
 
8
15
  ## When to use this skill
9
16
 
10
- Load this skill when you need to set up authentication, configure guards, add login/register views, implement authorization gates/policies, or work with password reset and email verification.
17
+ Load when you're configuring guards/providers, vendoring auth views, wiring `Gate`/`Policy` authorization, or working with password reset / email verification. For depth, open the rule file matching your task.
11
18
 
12
- ## Key concepts
19
+ ## Quick Reference
13
20
 
14
- - **AuthManager**: Process-wide DI singleton that creates fresh `SessionGuard` instances per call (never cached -- prevents ghost user leaks across requests).
15
- - **Guard contract**: `user()`, `check()`, `guest()`, `attempt()`, `login()`, `logout()` -- all async.
16
- - **`auth()` helper**: Returns the current request's `AuthManager` via AsyncLocalStorage. Mirrors Laravel's `auth()->user()`.
17
- - **Auth facade**: `Auth.user()`, `Auth.check()` etc. -- static class that proxies to `currentAuth()`.
18
- - **AuthMiddleware**: Sets up the auth ALS context and populates `req.user` for every request.
19
- - **RequireAuth**: Returns 401 if not authenticated.
20
- - **RequireGuest**: Redirects authenticated users away from guest-only pages (login, register).
21
- - **Gate/Policy**: Authorization system for checking abilities and model-level policies.
21
+ | Task | Open |
22
+ |---|---|
23
+ | Provider setup install deps, `config/auth.ts`, register provider, make User authenticatable | `rules/provider-setup.md` |
24
+ | Reading the current user — `auth()`, `Auth` facade, `RequireAuth` / `RequireGuest` middleware, login/logout endpoints | `rules/guards-and-handlers.md` |
25
+ | Login / register UI — `vendor:publish` auth views, `registerAuthRoutes`, custom paths and view ids | `rules/auth-views.md` |
26
+ | Authorization `Gate.define`, model `Policy` classes, `before` callbacks | `rules/gates-and-policies.md` |
27
+ | Email verification + password reset `MustVerifyEmail`, `verificationUrl`, `PasswordBroker` | `rules/email-and-password-reset.md` |
22
28
 
23
- ## Step-by-step
29
+ ## Key concepts (load once)
24
30
 
25
- ### 1. Install dependencies
26
-
27
- Auth requires `@rudderjs/session` and `@rudderjs/hash` as peer dependencies:
28
-
29
- ```bash
30
- pnpm add @rudderjs/auth @rudderjs/session @rudderjs/hash
31
- ```
32
-
33
- ### 2. Configure auth (config/auth.ts)
34
-
35
- ```ts
36
- import { User } from '../app/Models/User.js'
37
- import type { AuthConfig } from '@rudderjs/auth'
38
-
39
- export default {
40
- defaults: {
41
- guard: 'web',
42
- },
43
- guards: {
44
- web: {
45
- driver: 'session',
46
- provider: 'users',
47
- },
48
- },
49
- providers: {
50
- users: {
51
- driver: 'eloquent',
52
- model: User,
53
- },
54
- },
55
- } satisfies AuthConfig
56
- ```
57
-
58
- ### 3. Register the provider (bootstrap/providers.ts)
59
-
60
- ```ts
61
- import { defaultProviders } from '@rudderjs/core'
62
- // AuthProvider is auto-discovered via defaultProviders() if @rudderjs/auth is installed.
63
- // It requires HashProvider and SessionProvider to boot before it.
64
- export default [
65
- ...(await defaultProviders()),
66
- // ... your app providers
67
- ]
68
- ```
69
-
70
- ### 4. Make the User model authenticatable
71
-
72
- ```ts
73
- import { Model, Hidden } from '@rudderjs/orm'
74
- import type { Authenticatable } from '@rudderjs/auth'
75
-
76
- export class User extends Model implements Authenticatable {
77
- static fillable = ['name', 'email', 'password']
78
-
79
- @Hidden password = ''
80
-
81
- getAuthIdentifier(): string { return String(this.id) }
82
- getAuthPassword(): string { return this.password }
83
- getRememberToken(): string | null { return null }
84
- setRememberToken(_token: string): void {}
85
- }
86
- ```
87
-
88
- ### 5. Use auth in route handlers
89
-
90
- ```ts
91
- import { auth, Auth, RequireAuth } from '@rudderjs/auth'
92
-
93
- // Using the auth() helper (Laravel-style)
94
- router.get('/api/me', async (req, res) => {
95
- const user = await auth().user()
96
- if (!user) return res.status(401).json({ message: 'Unauthorized' })
97
- res.json({ user })
98
- })
99
-
100
- // Using the Auth facade
101
- router.get('/api/profile', async (req, res) => {
102
- if (await Auth.guest()) return res.status(401).json({ message: 'Unauthorized' })
103
- const user = await Auth.user()
104
- res.json({ user })
105
- })
106
-
107
- // Using RequireAuth middleware
108
- router.get('/api/dashboard', RequireAuth(), async (req, res) => {
109
- // req.user is guaranteed to exist here
110
- res.json({ user: req.user })
111
- })
112
- ```
113
-
114
- ### 6. Login / logout endpoints
115
-
116
- ```ts
117
- router.post('/auth/login', async (req, res) => {
118
- const { email, password } = req.body
119
- const success = await auth().attempt({ email, password })
120
- if (!success) {
121
- return res.status(422).json({ message: 'Invalid credentials.' })
122
- }
123
- const user = await auth().user()
124
- res.json({ user })
125
- })
126
-
127
- router.post('/auth/register', async (req, res) => {
128
- const user = await User.create({
129
- name: req.body.name,
130
- email: req.body.email,
131
- password: req.body.password, // hashed by Attribute mutator
132
- })
133
- await auth().login(user)
134
- res.json({ user })
135
- })
136
-
137
- router.post('/auth/logout', RequireAuth(), async (req, res) => {
138
- await auth().logout()
139
- res.json({ message: 'Logged out.' })
140
- })
141
- ```
142
-
143
- ### 7. Set up auth views (login/register pages)
144
-
145
- Vendor the view files into your app:
146
-
147
- ```bash
148
- pnpm rudder vendor:publish --tag=auth-views
149
- ```
150
-
151
- This copies `@rudderjs/auth/views/react/` into `app/Views/Auth/`. Then register routes:
152
-
153
- ```ts
154
- // routes/web.ts
155
- import { Route } from '@rudderjs/router'
156
- import { registerAuthRoutes } from '@rudderjs/auth/routes'
157
-
158
- registerAuthRoutes(Route)
159
- // Registers: GET /login, GET /register, GET /forgot-password, GET /reset-password
160
- ```
161
-
162
- Customize paths and view ids:
163
-
164
- ```ts
165
- registerAuthRoutes(Route, {
166
- paths: {
167
- login: '/sign-in',
168
- register: '/sign-up',
169
- },
170
- views: {
171
- login: 'auth.sign-in', // maps to app/Views/Auth/SignIn.tsx
172
- register: 'auth.sign-up',
173
- },
174
- homeUrl: '/dashboard', // redirect destination for authenticated users
175
- })
176
- ```
177
-
178
- ### 8. Authorization with Gates
179
-
180
- ```ts
181
- import { Gate, Policy, AuthorizationError } from '@rudderjs/auth'
182
-
183
- // Define abilities
184
- Gate.define('manage-settings', (user) => user.role === 'admin')
185
- Gate.define('edit-post', (user, post) => post.authorId === user.getAuthIdentifier())
186
-
187
- // Check in handlers
188
- if (await Gate.allows('manage-settings')) { /* ... */ }
189
- if (await Gate.denies('edit-post', post)) { /* ... */ }
190
-
191
- // Throw 403 if denied
192
- await Gate.authorize('edit-post', post)
193
-
194
- // Before callback -- runs before all checks
195
- Gate.before((user, ability) => {
196
- if (user.role === 'super-admin') return true // allow everything
197
- return null // fall through to normal checks
198
- })
199
- ```
200
-
201
- ### 9. Model policies
202
-
203
- ```ts
204
- import { Policy } from '@rudderjs/auth'
205
- import type { Authenticatable } from '@rudderjs/auth'
206
-
207
- class PostPolicy extends Policy {
208
- before(user: Authenticatable) {
209
- if ((user as any).role === 'admin') return true
210
- return null // fall through
211
- }
212
-
213
- view(user: Authenticatable, post: Post) {
214
- return post.isPublished || post.authorId === user.getAuthIdentifier()
215
- }
216
-
217
- update(user: Authenticatable, post: Post) {
218
- return post.authorId === user.getAuthIdentifier()
219
- }
220
-
221
- delete(user: Authenticatable, post: Post) {
222
- return post.authorId === user.getAuthIdentifier()
223
- }
224
- }
225
-
226
- // Register the policy
227
- Gate.policy(Post, PostPolicy)
228
-
229
- // Use it
230
- await Gate.authorize('update', post) // auto-finds PostPolicy.update()
231
- ```
232
-
233
- ### 10. Email verification
234
-
235
- ```ts
236
- import { EnsureEmailIsVerified, verificationUrl, handleEmailVerification } from '@rudderjs/auth'
237
- import type { MustVerifyEmail } from '@rudderjs/auth'
238
-
239
- // Make user implement MustVerifyEmail
240
- class User extends Model implements Authenticatable, MustVerifyEmail {
241
- hasVerifiedEmail() { return this.emailVerifiedAt !== null }
242
- async markEmailAsVerified() { await User.update(this.id, { emailVerifiedAt: new Date() }) }
243
- getEmailForVerification() { return this.email }
244
- }
245
-
246
- // Protect routes
247
- router.get('/dashboard', RequireAuth(), EnsureEmailIsVerified(), handler)
248
-
249
- // Generate verification URL (for sending in emails)
250
- const url = verificationUrl(user)
251
- ```
252
-
253
- ### 11. Password reset
254
-
255
- ```ts
256
- import { PasswordBroker, MemoryTokenRepository } from '@rudderjs/auth'
257
-
258
- const broker = new PasswordBroker(new MemoryTokenRepository())
259
- // In production, implement TokenRepository backed by your database
260
- ```
31
+ - **AuthManager** — process-wide DI singleton that creates fresh `SessionGuard` instances per call. **Never cached** — cached guards leak `_user` across requests.
32
+ - **`auth()` helper** — Laravel-style accessor returning the request-scoped `AuthManager` via AsyncLocalStorage.
33
+ - **`Auth` facade** `Auth.user()`, `Auth.check()`, etc. static class that proxies to `currentAuth()`.
34
+ - **Middleware groups** — `AuthMiddleware` auto-installs on the `web` group only. API routes are stateless by default; use `RequireBearer()` + `scope(...)` (passport) for token auth per-route.
35
+ - **`SessionGuard.user()` soft-fails** — returns `null` (not throw) when there's no ALS context, matching Laravel's `Auth::user()` semantics.
36
+ - **Peer deps**: `@rudderjs/session` and `@rudderjs/hash` are **required peers**. `HashProvider` must boot before `AuthProvider` (`defaultProviders()` orders this automatically).
261
37
 
262
38
  ## Examples
263
39
 
264
- See `playground/config/auth.ts` for configuration, `playground/app/Models/User.ts` for the model, `playground/routes/web.ts` for route registration, and `playground/app/Views/Auth/` for vendored view files.
265
-
266
- ## Common pitfalls
267
-
268
- - **Ghost signed-in user**: `AuthManager` must NOT cache `SessionGuard` instances. The manager is a DI singleton; cached guards leak `_user` across requests.
269
- - **Provider boot order**: `HashProvider` and `SessionProvider` must boot before `AuthProvider`. With `defaultProviders()`, this is handled automatically.
270
- - **Session middleware required**: Auth views require session middleware. Ensure `@rudderjs/session` is installed and its provider is registered.
271
- - **View route override**: Auth view files need `export const route = '/login'` etc. so SPA navigation works correctly (URL must match Vike's route table).
272
- - **POST handlers not included**: `registerAuthRoutes()` only registers GET routes for the UI pages. POST endpoints for login/register/logout are your responsibility in `routes/api.ts`.
273
- - **Guard driver**: Currently only `'session'` is supported as a guard driver. API token guards are planned.
40
+ See `playground/config/auth.ts`, `playground/app/Models/User.ts`, `playground/routes/web.ts`, and `playground/app/Views/Auth/`.
@@ -0,0 +1,90 @@
1
+ # Auth Views (Login / Register UI)
2
+
3
+ ## Vendor the view files
4
+
5
+ `@rudderjs/auth` ships React and Vue auth views. Vendor them into your app:
6
+
7
+ ```bash
8
+ pnpm rudder vendor:publish --tag=auth-views
9
+ ```
10
+
11
+ This copies `@rudderjs/auth/views/react/` (or `/vue/`) into `app/Views/Auth/`. After vendoring, the files belong to your app — edit them freely.
12
+
13
+ ## Register the controller routes
14
+
15
+ ```ts
16
+ // routes/web.ts
17
+ import { Route } from '@rudderjs/router'
18
+ import { registerAuthRoutes } from '@rudderjs/auth/routes'
19
+
20
+ registerAuthRoutes(Route)
21
+ ```
22
+
23
+ `registerAuthRoutes(Route)` registers:
24
+
25
+ | URL | View id |
26
+ |---|---|
27
+ | `GET /login` | `auth.login` |
28
+ | `GET /register` | `auth.register` |
29
+ | `GET /forgot-password` | `auth.forgot-password` |
30
+ | `GET /reset-password` | `auth.reset-password` |
31
+
32
+ The view-id mapping uses `view('id', props)` and lands in `app/Views/Auth/<File>.tsx`.
33
+
34
+ ## Customize paths and view ids
35
+
36
+ ```ts
37
+ registerAuthRoutes(Route, {
38
+ paths: {
39
+ login: '/sign-in',
40
+ register: '/sign-up',
41
+ },
42
+ views: {
43
+ login: 'auth.sign-in', // maps to app/Views/Auth/SignIn.tsx
44
+ register: 'auth.sign-up',
45
+ },
46
+ homeUrl: '/dashboard', // redirect destination for authenticated users
47
+ })
48
+ ```
49
+
50
+ ## Add the `route` export to vendored views
51
+
52
+ Vendored auth views need an explicit URL because the id-derived default (`/auth/login`) doesn't match the controller route (`/login`).
53
+
54
+ ```tsx
55
+ // app/Views/Auth/Login.tsx
56
+ export const route = '/login'
57
+
58
+ export default function Login(props: { /* ... */ }) { /* ... */ }
59
+ ```
60
+
61
+ Without this, **SPA navigation falls back to full page reloads** because Vike's client route table doesn't match the browser URL.
62
+
63
+ ## Pitfalls
64
+
65
+ ❌ **Don't** assume POST handlers come from `registerAuthRoutes`:
66
+
67
+ ```ts
68
+ registerAuthRoutes(Route)
69
+ // ❌ POST /login / POST /register / POST /logout — those are YOUR job
70
+ ```
71
+
72
+ ✅ **Do** add them in `routes/api.ts` or `routes/web.ts` using `auth().attempt()` / `auth().login()` / `auth().logout()`.
73
+
74
+ ❌ **Don't** skip the `route` export when customizing URLs:
75
+
76
+ ```tsx
77
+ // app/Views/Auth/Login.tsx — no route export
78
+ export default function Login() { /* ... */ }
79
+ ```
80
+
81
+ ✅ **Do** add it so SPA nav works:
82
+
83
+ ```tsx
84
+ export const route = '/sign-in'
85
+ export default function Login() { /* ... */ }
86
+ ```
87
+
88
+ ❌ **Don't** mix `vike-react` and `vike-vue` — the scanner errors at boot.
89
+
90
+ ✅ **Do** pick one renderer per project; vendor the matching auth views (`--tag=auth-views` auto-detects).
@@ -0,0 +1,91 @@
1
+ # Email Verification + Password Reset
2
+
3
+ ## Email verification
4
+
5
+ Implement `MustVerifyEmail` on your User model:
6
+
7
+ ```ts
8
+ import type { Authenticatable, MustVerifyEmail } from '@rudderjs/auth'
9
+
10
+ class User extends Model implements Authenticatable, MustVerifyEmail {
11
+ hasVerifiedEmail() { return this.emailVerifiedAt !== null }
12
+ getEmailForVerification() { return this.email }
13
+ async markEmailAsVerified() {
14
+ await User.update(this.id, { emailVerifiedAt: new Date() })
15
+ }
16
+ }
17
+ ```
18
+
19
+ Protect routes that require a verified email:
20
+
21
+ ```ts
22
+ import { RequireAuth, EnsureEmailIsVerified } from '@rudderjs/auth'
23
+
24
+ router.get('/dashboard', RequireAuth(), EnsureEmailIsVerified(), handler)
25
+ ```
26
+
27
+ Generate a signed URL to send in an email:
28
+
29
+ ```ts
30
+ import { verificationUrl } from '@rudderjs/auth'
31
+
32
+ const url = verificationUrl(user)
33
+ // Sign-protected — calling it with a tampered signature returns 403
34
+ ```
35
+
36
+ Handle the click:
37
+
38
+ ```ts
39
+ import { handleEmailVerification } from '@rudderjs/auth'
40
+
41
+ router.get('/verify-email/:id/:hash', async (req, res) => {
42
+ await handleEmailVerification(req.params.id, req.params.hash, (id) => User.find(id))
43
+ res.redirect('/dashboard')
44
+ })
45
+ ```
46
+
47
+ ## Password reset
48
+
49
+ The `PasswordBroker` orchestrates token generation, email dispatch, and verification. A token repository persists the tokens.
50
+
51
+ ```ts
52
+ import { PasswordBroker, MemoryTokenRepository } from '@rudderjs/auth'
53
+
54
+ const broker = new PasswordBroker(new MemoryTokenRepository())
55
+ ```
56
+
57
+ In production, implement `TokenRepository` backed by your database:
58
+
59
+ ```ts
60
+ import type { TokenRepository } from '@rudderjs/auth'
61
+
62
+ class PrismaTokenRepository implements TokenRepository {
63
+ async create(email: string, token: string) { /* INSERT */ }
64
+ async findByEmailAndToken(email: string, token: string) { /* SELECT */ }
65
+ async delete(email: string) { /* DELETE */ }
66
+ async deleteExpired(thresholdMs: number) { /* DELETE WHERE created_at < ... */ }
67
+ }
68
+ ```
69
+
70
+ ## Pitfalls
71
+
72
+ ❌ **Don't** ship `MemoryTokenRepository` to production — tokens evaporate on restart, and multi-process / multi-worker setups never share state.
73
+
74
+ ✅ **Do** persist tokens via your ORM (Prisma/Drizzle) and run a scheduled cleanup of expired rows.
75
+
76
+ ❌ **Don't** assume `verificationUrl(user)` works without `APP_KEY`:
77
+
78
+ ```ts
79
+ // Throws if APP_KEY isn't set
80
+ const url = verificationUrl(user)
81
+ ```
82
+
83
+ ✅ **Do** set `APP_KEY` in `.env` (or call `Url.setKey('test-key')` in tests).
84
+
85
+ ❌ **Don't** roll a hand-crafted signature on the verification URL:
86
+
87
+ ```ts
88
+ const url = `/verify-email/${user.id}/${crypto.createHash(...)}`
89
+ ```
90
+
91
+ ✅ **Do** use `verificationUrl(user)` — it uses HMAC-SHA256 with timing-safe comparison via `@rudderjs/router`'s `Url`.
@@ -0,0 +1,110 @@
1
+ # Gates and Policies
2
+
3
+ `Gate` is the imperative authorization API; `Policy` classes group abilities per model.
4
+
5
+ ## Define abilities
6
+
7
+ ```ts
8
+ import { Gate } from '@rudderjs/auth'
9
+
10
+ Gate.define('manage-settings', (user) => user.role === 'admin')
11
+ Gate.define('edit-post', (user, post) => post.authorId === user.getAuthIdentifier())
12
+ ```
13
+
14
+ ## Check in route handlers
15
+
16
+ ```ts
17
+ // Returns boolean
18
+ if (await Gate.allows('manage-settings')) { /* ... */ }
19
+ if (await Gate.denies('edit-post', post)) { /* ... */ }
20
+
21
+ // Throws 403 on denial
22
+ await Gate.authorize('edit-post', post)
23
+ ```
24
+
25
+ `authorize` throws `AuthorizationError` which the framework maps to a `403` response.
26
+
27
+ ## Before callback
28
+
29
+ Runs before every gate check — useful for super-admins:
30
+
31
+ ```ts
32
+ Gate.before((user, ability) => {
33
+ if (user.role === 'super-admin') return true // allow everything
34
+ return null // fall through to normal checks
35
+ })
36
+ ```
37
+
38
+ Return `true` to allow, `false` to deny, `null` to fall through.
39
+
40
+ ## Model policies
41
+
42
+ ```ts
43
+ import { Policy } from '@rudderjs/auth'
44
+ import type { Authenticatable } from '@rudderjs/auth'
45
+
46
+ class PostPolicy extends Policy {
47
+ before(user: Authenticatable) {
48
+ if ((user as { role?: string }).role === 'admin') return true
49
+ return null
50
+ }
51
+
52
+ view(user: Authenticatable, post: Post) {
53
+ return post.isPublished || post.authorId === user.getAuthIdentifier()
54
+ }
55
+
56
+ update(user: Authenticatable, post: Post) {
57
+ return post.authorId === user.getAuthIdentifier()
58
+ }
59
+
60
+ delete(user: Authenticatable, post: Post) {
61
+ return post.authorId === user.getAuthIdentifier()
62
+ }
63
+ }
64
+
65
+ Gate.policy(Post, PostPolicy)
66
+ ```
67
+
68
+ Method names on the policy class match ability names. `Gate.authorize('update', post)` looks up `PostPolicy.update(user, post)`.
69
+
70
+ ## Pitfalls
71
+
72
+ ❌ **Don't** call `Gate.authorize` outside an auth context expecting it to throw 401:
73
+
74
+ ```ts
75
+ // In a job — no auth context, user is null, gate throws AuthorizationError as 403
76
+ await Gate.authorize('edit-post', post)
77
+ ```
78
+
79
+ ✅ **Do** check authentication first or scope the gate explicitly:
80
+
81
+ ```ts
82
+ await Gate.forUser(user).authorize('edit-post', post)
83
+ ```
84
+
85
+ ❌ **Don't** rely on policy autoloading from disk:
86
+
87
+ ```ts
88
+ // Putting app/Policies/PostPolicy.ts on disk does NOT auto-register it
89
+ ```
90
+
91
+ ✅ **Do** register policies in a service provider's `boot()`:
92
+
93
+ ```ts
94
+ // app/Providers/AppServiceProvider.ts
95
+ boot() {
96
+ Gate.policy(Post, PostPolicy)
97
+ }
98
+ ```
99
+
100
+ ❌ **Don't** return non-boolean from a `define` callback expecting it to count:
101
+
102
+ ```ts
103
+ Gate.define('edit-post', (user, post) => post.authorId) // truthy number, but not boolean
104
+ ```
105
+
106
+ ✅ **Do** return a boolean explicitly:
107
+
108
+ ```ts
109
+ Gate.define('edit-post', (user, post) => post.authorId === user.getAuthIdentifier())
110
+ ```