@remix-run/cli 0.1.0 → 0.3.0
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 +0 -3
- package/dist/lib/bootstrap-project.d.ts.map +1 -1
- package/dist/lib/bootstrap-project.js +32 -9
- package/dist/lib/cli.d.ts +29 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +33 -14
- package/dist/lib/commands/completion.d.ts.map +1 -1
- package/dist/lib/commands/completion.js +5 -1
- package/dist/lib/commands/doctor.js +18 -18
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/help.js +9 -33
- package/dist/lib/commands/routes.js +3 -3
- package/dist/lib/commands/test.d.ts +1 -1
- package/dist/lib/commands/test.d.ts.map +1 -1
- package/dist/lib/commands/test.js +8 -4
- package/dist/lib/completion.d.ts.map +1 -1
- package/dist/lib/completion.js +4 -106
- package/dist/lib/controller-files.d.ts +0 -1
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +3 -5
- package/dist/lib/controller-ownership.d.ts +9 -9
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +56 -91
- package/dist/lib/doctor/controller-findings.d.ts +1 -1
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +15 -87
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
- package/dist/lib/doctor/controller-fix-plans.js +13 -24
- package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
- package/dist/lib/doctor/controller-placeholders.js +18 -149
- package/dist/lib/doctor/controllers.js +1 -1
- package/dist/lib/doctor/project.js +60 -52
- package/dist/lib/doctor/types.d.ts +2 -2
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/errors.d.ts +0 -6
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +0 -11
- package/dist/lib/load-route-map-worker.js +17 -9
- package/dist/lib/route-map.d.ts +1 -1
- package/dist/lib/route-map.d.ts.map +1 -1
- package/dist/lib/route-map.js +29 -17
- package/package.json +4 -5
- package/src/lib/bootstrap-project.ts +39 -13
- package/src/lib/cli.ts +46 -15
- package/src/lib/commands/completion.ts +6 -1
- package/src/lib/commands/doctor.ts +18 -21
- package/src/lib/commands/help.ts +9 -43
- package/src/lib/commands/routes.ts +3 -3
- package/src/lib/commands/test.ts +10 -4
- package/src/lib/completion.ts +4 -151
- package/src/lib/controller-files.ts +4 -8
- package/src/lib/controller-ownership.ts +78 -141
- package/src/lib/doctor/controller-findings.ts +20 -97
- package/src/lib/doctor/controller-fix-plans.ts +13 -29
- package/src/lib/doctor/controller-placeholders.ts +17 -189
- package/src/lib/doctor/controllers.ts +1 -1
- package/src/lib/doctor/project.ts +60 -52
- package/src/lib/doctor/types.ts +1 -5
- package/src/lib/errors.ts +0 -12
- package/src/lib/load-route-map-worker.ts +19 -10
- package/src/lib/route-map.ts +61 -16
- package/template/.agents/skills/remix/SKILL.md +588 -0
- package/template/.agents/skills/remix/references/animate-elements.md +195 -0
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +130 -0
- package/template/.agents/skills/remix/references/auth-and-sessions.md +443 -0
- package/template/.agents/skills/remix/references/component-model.md +282 -0
- package/template/.agents/skills/remix/references/create-mixins.md +158 -0
- package/template/.agents/skills/remix/references/data-and-validation.md +379 -0
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
- package/template/.agents/skills/remix/references/middleware-and-server.md +233 -0
- package/template/.agents/skills/remix/references/mixins-styling-events.md +213 -0
- package/template/.agents/skills/remix/references/routing-and-controllers.md +391 -0
- package/template/.agents/skills/remix/references/testing-patterns.md +172 -0
- package/{bootstrap → template}/AGENTS.md +13 -8
- package/template/README.md +29 -0
- package/template/app/actions/controller.tsx +18 -0
- package/template/app/assets/entry.ts +8 -0
- package/template/app/assets/prompt-button.tsx +163 -0
- package/template/app/assets.ts +20 -0
- package/template/app/middleware/render.tsx +43 -0
- package/template/app/router.ts +20 -0
- package/template/app/routes.ts +6 -0
- package/template/app/ui/document.tsx +33 -0
- package/template/app/ui/scaffold-home-page.tsx +527 -0
- package/template/gitignore +4 -0
- package/{bootstrap → template}/package.json +5 -6
- package/template/public/favicon.svg +11 -0
- package/{bootstrap → template}/server.ts +4 -5
- package/{bootstrap → template}/tsconfig.json +3 -3
- package/bootstrap/README.md +0 -27
- package/bootstrap/app/controllers/auth.tsx +0 -21
- package/bootstrap/app/controllers/home.tsx +0 -26
- package/bootstrap/app/router.ts +0 -10
- package/bootstrap/app/routes.ts +0 -6
- package/bootstrap/app/ui/document.tsx +0 -21
- package/bootstrap/app/ui/layout.tsx +0 -22
- package/bootstrap/app/utils/render.tsx +0 -7
- package/dist/lib/commands/skills.d.ts +0 -6
- package/dist/lib/commands/skills.d.ts.map +0 -1
- package/dist/lib/commands/skills.js +0 -222
- package/dist/lib/skills-cache.d.ts +0 -19
- package/dist/lib/skills-cache.d.ts.map +0 -1
- package/dist/lib/skills-cache.js +0 -89
- package/dist/lib/skills.d.ts +0 -30
- package/dist/lib/skills.d.ts.map +0 -1
- package/dist/lib/skills.js +0 -441
- package/src/lib/commands/skills.ts +0 -306
- package/src/lib/skills-cache.ts +0 -140
- package/src/lib/skills.ts +0 -706
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
# Authentication and Sessions
|
|
2
|
+
|
|
3
|
+
## What This Covers
|
|
4
|
+
|
|
5
|
+
How to remember things about a browser between requests and how to identify a user. Read this when
|
|
6
|
+
the task involves:
|
|
7
|
+
|
|
8
|
+
- Storing per-browser state across requests (login, cart, "I have submitted this form")
|
|
9
|
+
- Adding a credentials login flow or an OAuth provider
|
|
10
|
+
- Protecting routes with `requireAuth()` or stacking authorization checks
|
|
11
|
+
- Reading or writing `Session`, `Auth`, or other identity-related context values
|
|
12
|
+
- Logging in, logging out, or rotating session IDs
|
|
13
|
+
|
|
14
|
+
For raw cookies that are not session-backed (theme, locale, dismissed-banner), see
|
|
15
|
+
`createCookie` in this file plus the broader `Package Map` in `SKILL.md`.
|
|
16
|
+
|
|
17
|
+
## Sessions vs Plain Cookies
|
|
18
|
+
|
|
19
|
+
Reach for `remix/session` when state is sensitive, must be tamper-resistant, or represents the
|
|
20
|
+
identity of a request: who is logged in, which form a browser already submitted, what items are in
|
|
21
|
+
a cart. Sessions sign or encrypt their backing cookie with a server-held secret and give you a
|
|
22
|
+
typed `Session` object you can `get`, `set`, `flash`, `unset`, and `regenerateId`.
|
|
23
|
+
|
|
24
|
+
Reach for `remix/cookie` directly when the browser is allowed to carry the value and the server
|
|
25
|
+
does not need session semantics. This often means preferences (theme, locale, dismissed banner),
|
|
26
|
+
but a signed cookie can also be fine for small low-risk values where you truly only need one
|
|
27
|
+
cookie-shaped fact and do not need `Session` helpers.
|
|
28
|
+
|
|
29
|
+
If a malicious user editing the value would be a bug, or if the value needs server-managed
|
|
30
|
+
lifecycle, reach for a session.
|
|
31
|
+
|
|
32
|
+
### Quick chooser
|
|
33
|
+
|
|
34
|
+
| Need | Best fit | Why |
|
|
35
|
+
| ------------------------------------------------------------------- | --------------- | -------------------------------------------------- |
|
|
36
|
+
| Theme, locale, dismissed banner | `remix/cookie` | Browser-controlled preference |
|
|
37
|
+
| Small signed hint with minimal lifecycle | `remix/cookie` | One value, no `Session` helpers needed |
|
|
38
|
+
| "This browser already submitted", cart, flash messages, login state | `remix/session` | Tamper-sensitive, server-managed per-browser state |
|
|
39
|
+
| "One real person only", ownership, durable identity | account/auth | Cookies or sessions alone do not prove personhood |
|
|
40
|
+
|
|
41
|
+
## Session Setup
|
|
42
|
+
|
|
43
|
+
### Create a session cookie
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { createCookie } from 'remix/cookie'
|
|
47
|
+
|
|
48
|
+
let sessionSecret = process.env.SESSION_SECRET
|
|
49
|
+
if (!sessionSecret && process.env.NODE_ENV !== 'test') {
|
|
50
|
+
throw new Error('SESSION_SECRET is required')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export let sessionCookie = createCookie('session', {
|
|
54
|
+
secrets: [sessionSecret ?? 'test-only-secret'],
|
|
55
|
+
httpOnly: true,
|
|
56
|
+
sameSite: 'Lax',
|
|
57
|
+
secure: process.env.NODE_ENV === 'production',
|
|
58
|
+
maxAge: 2592000, // 30 days
|
|
59
|
+
path: '/',
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The cookie should always be `httpOnly`, default to `sameSite: 'Lax'`, and be `secure` in
|
|
64
|
+
production. Demo defaults like `'s3cr3t'` are fine in tests but should never reach production —
|
|
65
|
+
fail fast when the secret is missing.
|
|
66
|
+
|
|
67
|
+
### Create session storage
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Filesystem storage
|
|
71
|
+
import { createFsSessionStorage } from 'remix/session-storage/fs'
|
|
72
|
+
export let sessionStorage = createFsSessionStorage('./tmp/sessions')
|
|
73
|
+
|
|
74
|
+
// Memory storage (for tests)
|
|
75
|
+
import { createMemorySessionStorage } from 'remix/session-storage/memory'
|
|
76
|
+
export let sessionStorage = createMemorySessionStorage()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Add session middleware
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { session } from 'remix/middleware/session'
|
|
83
|
+
|
|
84
|
+
let router = createRouter({
|
|
85
|
+
middleware: [
|
|
86
|
+
session(sessionCookie, sessionStorage),
|
|
87
|
+
// ... other middleware
|
|
88
|
+
],
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Using sessions in handlers
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { Session } from 'remix/session'
|
|
96
|
+
|
|
97
|
+
async function handler({ get }) {
|
|
98
|
+
let session = get(Session)
|
|
99
|
+
|
|
100
|
+
// Read
|
|
101
|
+
let userId = session.get('userId')
|
|
102
|
+
|
|
103
|
+
// Write
|
|
104
|
+
session.set('userId', 42)
|
|
105
|
+
|
|
106
|
+
// Flash (read once, then cleared)
|
|
107
|
+
session.flash('message', 'Settings saved!')
|
|
108
|
+
let message = session.get('message') // returns and clears
|
|
109
|
+
|
|
110
|
+
// Remove a key
|
|
111
|
+
session.unset('userId')
|
|
112
|
+
|
|
113
|
+
// Regenerate session ID (after login/logout)
|
|
114
|
+
session.regenerateId(true)
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Sessions for non-auth state
|
|
119
|
+
|
|
120
|
+
Sessions are not just for login. They are the right place to store any tamper-sensitive
|
|
121
|
+
per-browser fact: which form a browser already submitted, how many free actions are left in a
|
|
122
|
+
trial, which feature flags a tester opted into, what items are in a cart.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
async function submit({ get }) {
|
|
126
|
+
let session = get(Session)
|
|
127
|
+
if (session.get('hasSubmitted')) {
|
|
128
|
+
return render(<AlreadySubmittedPage />, { status: 409 })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let parsed = s.parseSafe(submitSchema, get(FormData))
|
|
132
|
+
if (!parsed.success) {
|
|
133
|
+
return render(<SubmitPage errors={parsed.issues} />, { status: 400 })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await saveSubmission(parsed.value)
|
|
137
|
+
session.set('hasSubmitted', true)
|
|
138
|
+
session.flash('message', 'Thanks for submitting!')
|
|
139
|
+
|
|
140
|
+
return redirect(routes.thanks.href())
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Notice that there is no manual `Set-Cookie` plumbing in the action — the session middleware handles
|
|
145
|
+
that, and the handler returns an ordinary `Response`. Per-browser state enforced this way is still
|
|
146
|
+
bypassable by clearing cookies; if the guarantee needs to survive that, you also need an account
|
|
147
|
+
(see auth providers below).
|
|
148
|
+
|
|
149
|
+
## Auth Middleware
|
|
150
|
+
|
|
151
|
+
### Basic setup
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { auth, createSessionAuthScheme } from 'remix/middleware/auth'
|
|
155
|
+
import { Session } from 'remix/session'
|
|
156
|
+
import { Database } from 'remix/data-table'
|
|
157
|
+
|
|
158
|
+
export function loadAuth() {
|
|
159
|
+
return auth({
|
|
160
|
+
schemes: [
|
|
161
|
+
createSessionAuthScheme({
|
|
162
|
+
read(session) {
|
|
163
|
+
let data = session.get('auth')
|
|
164
|
+
return data ?? null
|
|
165
|
+
},
|
|
166
|
+
async verify(value, context) {
|
|
167
|
+
let db = context.get(Database)
|
|
168
|
+
return (await db.find(users, value.userId)) ?? null
|
|
169
|
+
},
|
|
170
|
+
invalidate(session) {
|
|
171
|
+
session.unset('auth')
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
],
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Reading auth state
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { Auth } from 'remix/middleware/auth'
|
|
183
|
+
|
|
184
|
+
function handler({ get }) {
|
|
185
|
+
let auth = get(Auth)
|
|
186
|
+
|
|
187
|
+
if (auth.ok) {
|
|
188
|
+
// User is authenticated
|
|
189
|
+
let user = auth.identity
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Credentials Auth
|
|
195
|
+
|
|
196
|
+
### Define a credentials provider
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { createCredentialsAuthProvider, verifyCredentials, completeAuth } from 'remix/auth'
|
|
200
|
+
import * as s from 'remix/data-schema'
|
|
201
|
+
import * as f from 'remix/data-schema/form-data'
|
|
202
|
+
|
|
203
|
+
let loginSchema = f.object({
|
|
204
|
+
email: f.field(s.defaulted(s.string(), '')),
|
|
205
|
+
password: f.field(s.defaulted(s.string(), '')),
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
export let passwordProvider = createCredentialsAuthProvider({
|
|
209
|
+
parse(context) {
|
|
210
|
+
let formData = context.get(FormData)
|
|
211
|
+
return s.parse(loginSchema, formData)
|
|
212
|
+
},
|
|
213
|
+
async verify({ email, password }, context) {
|
|
214
|
+
let db = context.get(Database)
|
|
215
|
+
let user = await db.findOne(users, { where: { email } })
|
|
216
|
+
if (!user || !(await verifyPassword(password, user.password_hash))) {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
return user
|
|
220
|
+
},
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Login action
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { verifyCredentials, completeAuth } from 'remix/auth'
|
|
228
|
+
import { redirect } from 'remix/response/redirect'
|
|
229
|
+
|
|
230
|
+
async action(context) {
|
|
231
|
+
let user = await verifyCredentials(passwordProvider, context)
|
|
232
|
+
|
|
233
|
+
if (user == null) {
|
|
234
|
+
let session = context.get(Session)
|
|
235
|
+
session.flash('error', 'Invalid email or password.')
|
|
236
|
+
return redirect(routes.auth.login.href())
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let session = completeAuth(context)
|
|
240
|
+
session.set('auth', { userId: user.id })
|
|
241
|
+
|
|
242
|
+
return redirect(routes.home.href())
|
|
243
|
+
},
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Logout action
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { Session } from 'remix/session'
|
|
250
|
+
import { redirect } from 'remix/response/redirect'
|
|
251
|
+
|
|
252
|
+
function logout(context) {
|
|
253
|
+
let session = context.get(Session)
|
|
254
|
+
session.unset('auth')
|
|
255
|
+
session.regenerateId(true)
|
|
256
|
+
return redirect(routes.home.href())
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## OAuth / External Auth
|
|
261
|
+
|
|
262
|
+
### Create providers
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import {
|
|
266
|
+
createAtmosphereAuthProvider,
|
|
267
|
+
createGoogleAuthProvider,
|
|
268
|
+
createGitHubAuthProvider,
|
|
269
|
+
startExternalAuth,
|
|
270
|
+
finishExternalAuth,
|
|
271
|
+
completeAuth,
|
|
272
|
+
refreshExternalAuth,
|
|
273
|
+
} from 'remix/auth'
|
|
274
|
+
|
|
275
|
+
let googleProvider = createGoogleAuthProvider({
|
|
276
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
277
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
278
|
+
redirectUri: new URL(routes.auth.google.callback.href(), origin),
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
let githubProvider = createGitHubAuthProvider({
|
|
282
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
283
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
284
|
+
redirectUri: new URL(routes.auth.github.callback.href(), origin),
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
let atmosphereSessionSecret = process.env.ATMOSPHERE_SESSION_SECRET
|
|
288
|
+
if (!atmosphereSessionSecret && process.env.NODE_ENV !== 'test') {
|
|
289
|
+
throw new Error('ATMOSPHERE_SESSION_SECRET is required')
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let atmosphereProvider = createAtmosphereAuthProvider({
|
|
293
|
+
clientId: 'https://app.example.com/oauth/client-metadata.json',
|
|
294
|
+
redirectUri: new URL(routes.auth.atmosphere.callback.href(), origin),
|
|
295
|
+
sessionSecret: atmosphereSessionSecret ?? 'test-only-secret',
|
|
296
|
+
})
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
For Atmosphere-compatible atproto OAuth, create the provider once, call
|
|
300
|
+
`atmosphereProvider.prepare(handleOrDid)` before `startExternalAuth(...)`, then pass the same
|
|
301
|
+
module-scope provider to `finishExternalAuth(...)` and `refreshExternalAuth(...)`.
|
|
302
|
+
|
|
303
|
+
### OAuth controller
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
import { createController } from 'remix/router'
|
|
307
|
+
|
|
308
|
+
export default createController(routes.auth.google, {
|
|
309
|
+
actions: {
|
|
310
|
+
// GET /auth/google — redirect to Google
|
|
311
|
+
async index(context) {
|
|
312
|
+
return await startExternalAuth(googleProvider, context, {
|
|
313
|
+
returnTo: context.url.searchParams.get('returnTo'),
|
|
314
|
+
})
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// GET /auth/google/callback — handle redirect back
|
|
318
|
+
async callback(context) {
|
|
319
|
+
let { result, returnTo } = await finishExternalAuth(googleProvider, context)
|
|
320
|
+
|
|
321
|
+
let db = context.get(Database)
|
|
322
|
+
let { user, authAccount } = await resolveExternalAuth(db, result)
|
|
323
|
+
|
|
324
|
+
let session = completeAuth(context)
|
|
325
|
+
session.set('auth', {
|
|
326
|
+
userId: user.id,
|
|
327
|
+
loginMethod: result.provider,
|
|
328
|
+
authAccountId: authAccount.id,
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
return redirect(returnTo ?? routes.account.index.href())
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
})
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Refresh stored provider tokens
|
|
338
|
+
|
|
339
|
+
Use `refreshExternalAuth(provider, tokens)` when an app has stored OAuth/OIDC tokens and needs a
|
|
340
|
+
fresh access token from a refresh token. Built-in OIDC providers, X, and Atmosphere support
|
|
341
|
+
refresh-token exchange. If the provider does not rotate the refresh token, the refreshed bundle
|
|
342
|
+
preserves the current one.
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
async function refreshGoogleTokens({ get }) {
|
|
346
|
+
let db = get(Database)
|
|
347
|
+
let account = await db.findOne(authAccounts, { where: { provider: 'google' } })
|
|
348
|
+
if (!account) return null
|
|
349
|
+
|
|
350
|
+
let refreshed = await refreshExternalAuth(googleProvider, account.tokens)
|
|
351
|
+
await db.update(authAccounts, account.id, { tokens: refreshed.tokens })
|
|
352
|
+
|
|
353
|
+
return refreshed.tokens
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Protecting Routes
|
|
358
|
+
|
|
359
|
+
### Controller-level protection
|
|
360
|
+
|
|
361
|
+
Apply `requireAuth()` to every action in one controller:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import { createController } from 'remix/router'
|
|
365
|
+
import { requireAuth } from 'remix/middleware/auth'
|
|
366
|
+
|
|
367
|
+
export default createController(routes.account, {
|
|
368
|
+
middleware: [requireAuth()],
|
|
369
|
+
actions: {
|
|
370
|
+
index() {
|
|
371
|
+
/* guaranteed authenticated */
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
})
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Nested route maps need their own explicit protection:
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// app/router.ts
|
|
381
|
+
router.map(routes.account, accountController)
|
|
382
|
+
router.map(routes.account.settings, accountSettingsController)
|
|
383
|
+
|
|
384
|
+
// app/actions/account/settings/controller.tsx
|
|
385
|
+
export default createController(routes.account.settings, {
|
|
386
|
+
middleware: [requireAuth()],
|
|
387
|
+
actions: {
|
|
388
|
+
index() {
|
|
389
|
+
/* guaranteed authenticated */
|
|
390
|
+
},
|
|
391
|
+
update() {
|
|
392
|
+
/* guaranteed authenticated */
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
})
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Stacking middleware
|
|
399
|
+
|
|
400
|
+
Combine auth checks with role checks:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
export default createController(routes.admin, {
|
|
404
|
+
middleware: [requireAuth(), requireAdmin()],
|
|
405
|
+
actions: {
|
|
406
|
+
index() {
|
|
407
|
+
/* requires auth + admin */
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
})
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Action-level protection
|
|
414
|
+
|
|
415
|
+
Apply middleware to a single route:
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
import { Auth, requireAuth } from 'remix/middleware/auth'
|
|
419
|
+
|
|
420
|
+
router.get(routes.account.index, {
|
|
421
|
+
middleware: [requireAuth()],
|
|
422
|
+
handler(context) {
|
|
423
|
+
let auth = context.get(Auth)
|
|
424
|
+
return render(<AccountPage identity={auth.identity} />)
|
|
425
|
+
},
|
|
426
|
+
})
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Redirect on auth failure
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
import { requireAuth } from 'remix/middleware/auth'
|
|
433
|
+
import { redirect } from 'remix/response/redirect'
|
|
434
|
+
|
|
435
|
+
export function requireAuthRedirect() {
|
|
436
|
+
return requireAuth({
|
|
437
|
+
onFailure(context) {
|
|
438
|
+
let returnTo = encodeURIComponent(context.url.pathname)
|
|
439
|
+
return redirect(routes.auth.login.href() + `?returnTo=${returnTo}`, 303)
|
|
440
|
+
},
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
```
|