@netlify/identity 0.3.0-alpha.0 → 0.3.0-alpha.2
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 +521 -30
- package/dist/index.cjs +357 -111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +229 -24
- package/dist/index.d.ts +229 -24
- package/dist/index.js +362 -111
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,39 @@ A lightweight, no-config headless authentication library for projects using Netl
|
|
|
4
4
|
|
|
5
5
|
> **Status:** Beta. The API may change before 1.0.
|
|
6
6
|
|
|
7
|
-
For a pre-built login widget, see [netlify-identity-widget](https://github.com/netlify/netlify-identity-widget).
|
|
8
|
-
|
|
9
7
|
**Prerequisites:**
|
|
10
8
|
|
|
11
9
|
- [Netlify Identity](https://docs.netlify.com/security/secure-access-to-sites/identity/) must be enabled on your Netlify project
|
|
12
10
|
- For local development, use [`netlify dev`](https://docs.netlify.com/cli/local-development/) so the Identity endpoint is available
|
|
13
11
|
|
|
12
|
+
### How this library relates to other Netlify auth packages
|
|
13
|
+
|
|
14
|
+
| Package | What it is | When to use it |
|
|
15
|
+
| ------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
16
|
+
| **`@netlify/identity`** (this library) | Headless TypeScript API for browser and server | You want full control over your auth UI and need server-side auth (SSR, Netlify Functions) |
|
|
17
|
+
| [`netlify-identity-widget`](https://github.com/netlify/netlify-identity-widget) | Pre-built login/signup modal (HTML + CSS) | You want a drop-in UI component with no custom design |
|
|
18
|
+
| [`gotrue-js`](https://github.com/netlify/gotrue-js) | Low-level GoTrue HTTP client (browser only) | You're building your own auth wrapper and need direct API access |
|
|
19
|
+
|
|
20
|
+
This library wraps `gotrue-js` in the browser and calls the GoTrue HTTP API directly on the server. It provides a unified API that works in both contexts, handles cookie management, and normalizes the user object. You do not need to install `gotrue-js` or the widget separately.
|
|
21
|
+
|
|
22
|
+
## Table of contents
|
|
23
|
+
|
|
24
|
+
- [Installation](#installation)
|
|
25
|
+
- [Quick start](#quick-start)
|
|
26
|
+
- [API](#api)
|
|
27
|
+
- [Functions](#functions) -- `getUser`, `login`, `signup`, `logout`, `oauthLogin`, `handleAuthCallback`, `onAuthChange`, `hydrateSession`, and more
|
|
28
|
+
- [Admin Operations](#admin-operations) -- `admin.listUsers`, `admin.getUser`, `admin.createUser`, `admin.updateUser`, `admin.deleteUser`
|
|
29
|
+
- [Types](#types) -- `User`, `AuthEvent`, `CallbackResult`, `Settings`, etc.
|
|
30
|
+
- [Errors](#errors) -- `AuthError`, `MissingIdentityError`
|
|
31
|
+
- [Framework integration](#framework-integration) -- Next.js, Remix, TanStack Start, Astro, SvelteKit
|
|
32
|
+
- [Guides](#guides)
|
|
33
|
+
- [React `useAuth` hook](#react-useauth-hook)
|
|
34
|
+
- [Listening for auth changes](#listening-for-auth-changes)
|
|
35
|
+
- [OAuth login](#oauth-login)
|
|
36
|
+
- [Password recovery](#password-recovery)
|
|
37
|
+
- [Invite acceptance](#invite-acceptance)
|
|
38
|
+
- [Session lifetime](#session-lifetime)
|
|
39
|
+
|
|
14
40
|
## Installation
|
|
15
41
|
|
|
16
42
|
```bash
|
|
@@ -19,18 +45,20 @@ npm install @netlify/identity
|
|
|
19
45
|
|
|
20
46
|
## Quick start
|
|
21
47
|
|
|
22
|
-
###
|
|
48
|
+
### Log in (browser)
|
|
23
49
|
|
|
24
50
|
```ts
|
|
25
|
-
import { getUser } from '@netlify/identity'
|
|
51
|
+
import { login, getUser } from '@netlify/identity'
|
|
26
52
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
53
|
+
// Log in
|
|
54
|
+
const user = await login('jane@example.com', 'password123')
|
|
55
|
+
console.log(`Hello, ${user.name}`)
|
|
56
|
+
|
|
57
|
+
// Later, check auth state synchronously
|
|
58
|
+
const currentUser = getUser()
|
|
31
59
|
```
|
|
32
60
|
|
|
33
|
-
### Netlify Function
|
|
61
|
+
### Protect a Netlify Function
|
|
34
62
|
|
|
35
63
|
```ts
|
|
36
64
|
import { getUser } from '@netlify/identity'
|
|
@@ -43,7 +71,7 @@ export default async (req: Request, context: Context) => {
|
|
|
43
71
|
}
|
|
44
72
|
```
|
|
45
73
|
|
|
46
|
-
### Edge Function
|
|
74
|
+
### Protect an Edge Function
|
|
47
75
|
|
|
48
76
|
```ts
|
|
49
77
|
import { getUser } from '@netlify/identity'
|
|
@@ -66,7 +94,9 @@ export default async (req: Request, context: Context) => {
|
|
|
66
94
|
getUser(): User | null
|
|
67
95
|
```
|
|
68
96
|
|
|
69
|
-
Returns the current authenticated user, or `null` if not logged in. Synchronous
|
|
97
|
+
Returns the current authenticated user, or `null` if not logged in. Synchronous. Never throws.
|
|
98
|
+
|
|
99
|
+
> **Next.js note:** Calling `getUser()` in a Server Component opts the page into [dynamic rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-rendering) because it reads cookies. This is expected and correct for authenticated pages. Next.js handles the internal dynamic rendering signal automatically.
|
|
70
100
|
|
|
71
101
|
#### `isAuthenticated`
|
|
72
102
|
|
|
@@ -109,12 +139,14 @@ In the browser, uses gotrue-js and emits a `'login'` event. On the server (Netli
|
|
|
109
139
|
#### `signup`
|
|
110
140
|
|
|
111
141
|
```ts
|
|
112
|
-
signup(email: string, password: string, data?:
|
|
142
|
+
signup(email: string, password: string, data?: SignupData): Promise<User>
|
|
113
143
|
```
|
|
114
144
|
|
|
115
145
|
Creates a new account. Works in both browser and server contexts.
|
|
116
146
|
|
|
117
|
-
|
|
147
|
+
If autoconfirm is enabled in your Identity settings, the user is logged in immediately: cookies are set and a `'login'` event is emitted. If autoconfirm is **disabled** (the default), the user receives a confirmation email and must click the link before they can log in. In that case, no cookies are set and no auth event is emitted.
|
|
148
|
+
|
|
149
|
+
The optional `data` parameter sets user metadata (e.g., `{ full_name: 'Jane Doe' }`), stored in the user's `user_metadata` field.
|
|
118
150
|
|
|
119
151
|
**Throws:** `AuthError` on failure (e.g., email already registered, signup disabled). In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
|
|
120
152
|
|
|
@@ -126,9 +158,9 @@ logout(): Promise<void>
|
|
|
126
158
|
|
|
127
159
|
Logs out the current user and clears the session. Works in both browser and server contexts.
|
|
128
160
|
|
|
129
|
-
In the browser, uses gotrue-js and emits a `'logout'` event. On the server, calls GoTrue's `/logout` endpoint with the JWT from the `nf_jwt` cookie, then deletes the cookie.
|
|
161
|
+
In the browser, uses gotrue-js and emits a `'logout'` event. On the server, calls GoTrue's `/logout` endpoint with the JWT from the `nf_jwt` cookie, then deletes the cookie. Auth cookies are always cleared, even if the GoTrue call fails.
|
|
130
162
|
|
|
131
|
-
**Throws:**
|
|
163
|
+
**Throws:** In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
|
|
132
164
|
|
|
133
165
|
#### `oauthLogin`
|
|
134
166
|
|
|
@@ -140,7 +172,7 @@ Redirects to an OAuth provider. The page navigates away, so this function never
|
|
|
140
172
|
|
|
141
173
|
The `provider` argument should be one of the `AuthProvider` values: `'google'`, `'github'`, `'gitlab'`, `'bitbucket'`, `'facebook'`, or `'saml'`.
|
|
142
174
|
|
|
143
|
-
**Throws:** `MissingIdentityError` if Identity is not configured. `
|
|
175
|
+
**Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if called on the server.
|
|
144
176
|
|
|
145
177
|
#### `handleAuthCallback`
|
|
146
178
|
|
|
@@ -158,7 +190,29 @@ Processes the URL hash after an OAuth redirect, email confirmation, password rec
|
|
|
158
190
|
onAuthChange(callback: AuthCallback): () => void
|
|
159
191
|
```
|
|
160
192
|
|
|
161
|
-
Subscribes to auth state changes (login, logout, token refresh, user updates). Returns an unsubscribe function. Also fires on cross-tab session changes. No-op on the server.
|
|
193
|
+
Subscribes to auth state changes (login, logout, token refresh, user updates, and recovery). Returns an unsubscribe function. Also fires on cross-tab session changes. No-op on the server. The `'recovery'` event fires when `handleAuthCallback()` processes a password recovery token; listen for it to redirect users to a password reset form.
|
|
194
|
+
|
|
195
|
+
#### `hydrateSession`
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
hydrateSession(): Promise<User | null>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Bootstraps the browser-side gotrue-js session from server-set auth cookies (`nf_jwt`, `nf_refresh`). Returns the hydrated `User`, or `null` if no auth cookies are present. No-op on the server.
|
|
202
|
+
|
|
203
|
+
**When to use:** After a server-side login (e.g., via a Netlify Function or Server Action), the `nf_jwt` cookie is set but gotrue-js has no browser session yet. `getUser()` works immediately (it decodes the cookie), but account operations like `updateUser()` or `verifyEmailChange()` require a live gotrue-js session. Call `hydrateSession()` once on page load to bridge this gap.
|
|
204
|
+
|
|
205
|
+
If a gotrue-js session already exists (e.g., from a browser-side login), this is a no-op and returns the existing user.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { hydrateSession, updateUser } from '@netlify/identity'
|
|
209
|
+
|
|
210
|
+
// On page load, hydrate the session from server-set cookies
|
|
211
|
+
await hydrateSession()
|
|
212
|
+
|
|
213
|
+
// Now browser account operations work
|
|
214
|
+
await updateUser({ data: { full_name: 'Jane Doe' } })
|
|
215
|
+
```
|
|
162
216
|
|
|
163
217
|
#### `requestPasswordRecovery`
|
|
164
218
|
|
|
@@ -213,10 +267,10 @@ Redeems a recovery token and sets a new password. Logs the user in on success.
|
|
|
213
267
|
#### `updateUser`
|
|
214
268
|
|
|
215
269
|
```ts
|
|
216
|
-
updateUser(updates:
|
|
270
|
+
updateUser(updates: UserUpdates): Promise<User>
|
|
217
271
|
```
|
|
218
272
|
|
|
219
|
-
Updates the current user's metadata or credentials. Requires an active session.
|
|
273
|
+
Updates the current user's metadata or credentials. Requires an active session. Pass `email` or `password` to change credentials, or `data` to update user metadata (e.g., `{ data: { full_name: 'New Name' } }`).
|
|
220
274
|
|
|
221
275
|
**Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if no user is logged in, or the update fails.
|
|
222
276
|
|
|
@@ -231,6 +285,30 @@ The `admin` namespace provides user management functions for administrators. The
|
|
|
231
285
|
import { admin } from '@netlify/identity'
|
|
232
286
|
```
|
|
233
287
|
|
|
288
|
+
**Example: managing users in a Netlify Function**
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { admin } from '@netlify/identity'
|
|
292
|
+
import type { Context } from '@netlify/functions'
|
|
293
|
+
|
|
294
|
+
export default async (req: Request, context: Context) => {
|
|
295
|
+
// List all users
|
|
296
|
+
const users = await admin.listUsers()
|
|
297
|
+
|
|
298
|
+
// Create a new user (auto-confirmed, no email sent)
|
|
299
|
+
const newUser = await admin.createUser({
|
|
300
|
+
email: 'jane@example.com',
|
|
301
|
+
password: 'securepassword',
|
|
302
|
+
data: { user_metadata: { full_name: 'Jane Doe' } },
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
// Update a user's role
|
|
306
|
+
await admin.updateUser(newUser.id, { role: 'editor' })
|
|
307
|
+
|
|
308
|
+
return Response.json({ created: newUser.id, total: users.length })
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
234
312
|
#### `admin.listUsers`
|
|
235
313
|
|
|
236
314
|
```ts
|
|
@@ -264,10 +342,10 @@ Creates a new user. The user is auto-confirmed. Optional `data` is spread into t
|
|
|
264
342
|
#### `admin.updateUser`
|
|
265
343
|
|
|
266
344
|
```ts
|
|
267
|
-
admin.updateUser(userId: string, attributes:
|
|
345
|
+
admin.updateUser(userId: string, attributes: AdminUserUpdates): Promise<User>
|
|
268
346
|
```
|
|
269
347
|
|
|
270
|
-
Updates an existing user by ID. Pass any attributes to change (e.g., `{ email: 'new@example.com' }`).
|
|
348
|
+
Updates an existing user by ID. Pass any attributes to change (e.g., `{ email: 'new@example.com' }`). See {@link AdminUserUpdates} for typed fields.
|
|
271
349
|
|
|
272
350
|
**Throws:** `AuthError` if the user is not found or the update fails.
|
|
273
351
|
|
|
@@ -325,6 +403,43 @@ interface IdentityConfig {
|
|
|
325
403
|
type AuthProvider = 'google' | 'github' | 'gitlab' | 'bitbucket' | 'facebook' | 'saml' | 'email'
|
|
326
404
|
```
|
|
327
405
|
|
|
406
|
+
#### `UserUpdates`
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
interface UserUpdates {
|
|
410
|
+
email?: string
|
|
411
|
+
password?: string
|
|
412
|
+
data?: Record<string, unknown>
|
|
413
|
+
[key: string]: unknown
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Fields accepted by `updateUser()`. All fields are optional.
|
|
418
|
+
|
|
419
|
+
#### `AdminUserUpdates`
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
interface AdminUserUpdates {
|
|
423
|
+
email?: string
|
|
424
|
+
password?: string
|
|
425
|
+
role?: string
|
|
426
|
+
confirm?: boolean
|
|
427
|
+
app_metadata?: Record<string, unknown>
|
|
428
|
+
user_metadata?: Record<string, unknown>
|
|
429
|
+
[key: string]: unknown
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Fields accepted by `admin.updateUser()`. Unlike `UserUpdates`, admin updates can set `role`, force-confirm a user, and write to `app_metadata`.
|
|
434
|
+
|
|
435
|
+
#### `SignupData`
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
type SignupData = Record<string, unknown>
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
User metadata passed as the third argument to `signup()`. Stored in the user's `user_metadata` field.
|
|
442
|
+
|
|
328
443
|
#### `AppMetadata`
|
|
329
444
|
|
|
330
445
|
```ts
|
|
@@ -335,10 +450,24 @@ interface AppMetadata {
|
|
|
335
450
|
}
|
|
336
451
|
```
|
|
337
452
|
|
|
453
|
+
#### `AUTH_EVENTS`
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
const AUTH_EVENTS: {
|
|
457
|
+
LOGIN: 'login'
|
|
458
|
+
LOGOUT: 'logout'
|
|
459
|
+
TOKEN_REFRESH: 'token_refresh'
|
|
460
|
+
USER_UPDATED: 'user_updated'
|
|
461
|
+
RECOVERY: 'recovery'
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Constants for auth event names. Use these instead of string literals for type safety and autocomplete.
|
|
466
|
+
|
|
338
467
|
#### `AuthEvent`
|
|
339
468
|
|
|
340
469
|
```ts
|
|
341
|
-
type AuthEvent = 'login' | 'logout' | 'token_refresh' | 'user_updated'
|
|
470
|
+
type AuthEvent = 'login' | 'logout' | 'token_refresh' | 'user_updated' | 'recovery'
|
|
342
471
|
```
|
|
343
472
|
|
|
344
473
|
#### `AuthCallback`
|
|
@@ -380,29 +509,367 @@ class MissingIdentityError extends Error {}
|
|
|
380
509
|
|
|
381
510
|
Thrown when Identity is not configured in the current environment.
|
|
382
511
|
|
|
512
|
+
## Framework integration
|
|
513
|
+
|
|
514
|
+
### Recommended pattern for SSR frameworks
|
|
515
|
+
|
|
516
|
+
For SSR frameworks (Next.js, Remix, Astro, TanStack Start), the recommended pattern is:
|
|
517
|
+
|
|
518
|
+
- **Browser-side** for auth mutations: `login()`, `signup()`, `logout()`, `oauthLogin()`
|
|
519
|
+
- **Server-side** for reading auth state: `getUser()`, `getSettings()`, `getIdentityConfig()`
|
|
520
|
+
|
|
521
|
+
Browser-side auth mutations call the GoTrue API directly from the browser, set the `nf_jwt` cookie and gotrue-js localStorage, and emit `onAuthChange` events. This keeps the client UI in sync immediately. Server-side reads work because the cookie is sent with every request.
|
|
522
|
+
|
|
523
|
+
The library also supports server-side mutations (`login()`, `signup()`, `logout()` inside Netlify Functions), but these require the Netlify Functions runtime to set cookies. After a server-side mutation, you need a full page navigation so the browser sends the new cookie.
|
|
524
|
+
|
|
525
|
+
### Next.js (App Router)
|
|
526
|
+
|
|
527
|
+
**Server Actions return results; the client handles navigation:**
|
|
528
|
+
|
|
529
|
+
```tsx
|
|
530
|
+
// app/actions.ts
|
|
531
|
+
'use server'
|
|
532
|
+
import { login, logout } from '@netlify/identity'
|
|
533
|
+
|
|
534
|
+
export async function loginAction(formData: FormData) {
|
|
535
|
+
const email = formData.get('email') as string
|
|
536
|
+
const password = formData.get('password') as string
|
|
537
|
+
await login(email, password)
|
|
538
|
+
return { success: true }
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export async function logoutAction() {
|
|
542
|
+
await logout()
|
|
543
|
+
return { success: true }
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
// app/login/page.tsx
|
|
549
|
+
'use client'
|
|
550
|
+
import { loginAction } from '../actions'
|
|
551
|
+
|
|
552
|
+
export default function LoginPage() {
|
|
553
|
+
async function handleSubmit(formData: FormData) {
|
|
554
|
+
const result = await loginAction(formData)
|
|
555
|
+
if (result.success) {
|
|
556
|
+
window.location.href = '/dashboard' // full page load
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return <form action={handleSubmit}>...</form>
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
```tsx
|
|
565
|
+
// app/dashboard/page.tsx
|
|
566
|
+
import { getUser } from '@netlify/identity'
|
|
567
|
+
import { redirect } from 'next/navigation'
|
|
568
|
+
|
|
569
|
+
export default function Dashboard() {
|
|
570
|
+
const user = getUser()
|
|
571
|
+
if (!user) redirect('/login')
|
|
572
|
+
|
|
573
|
+
return <h1>Hello, {user.email}</h1>
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Use `window.location.href` instead of Next.js `redirect()` after server-side auth mutations. Next.js `redirect()` triggers a soft navigation via the Router, which may not include the newly-set auth cookie. A full page load ensures the cookie is sent and the server sees the updated auth state. Reading auth state with `getUser()` in Server Components works normally, and `redirect()` is fine for auth gates (where no cookie was just set).
|
|
578
|
+
|
|
579
|
+
### Remix
|
|
580
|
+
|
|
581
|
+
**Login with Action (server-side pattern):**
|
|
582
|
+
|
|
583
|
+
```tsx
|
|
584
|
+
// app/routes/login.tsx
|
|
585
|
+
import { login } from '@netlify/identity'
|
|
586
|
+
import { redirect, json } from '@remix-run/node'
|
|
587
|
+
import type { ActionFunctionArgs } from '@remix-run/node'
|
|
588
|
+
|
|
589
|
+
export async function action({ request }: ActionFunctionArgs) {
|
|
590
|
+
const formData = await request.formData()
|
|
591
|
+
const email = formData.get('email') as string
|
|
592
|
+
const password = formData.get('password') as string
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
await login(email, password)
|
|
596
|
+
return redirect('/dashboard')
|
|
597
|
+
} catch (error) {
|
|
598
|
+
return json({ error: (error as Error).message }, { status: 400 })
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
```tsx
|
|
604
|
+
// app/routes/dashboard.tsx
|
|
605
|
+
import { getUser } from '@netlify/identity'
|
|
606
|
+
import { redirect } from '@remix-run/node'
|
|
607
|
+
|
|
608
|
+
export async function loader() {
|
|
609
|
+
const user = getUser()
|
|
610
|
+
if (!user) return redirect('/login')
|
|
611
|
+
return { user }
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Remix `redirect()` works after server-side `login()` because Remix actions return real HTTP responses. The browser receives a 302 with the `Set-Cookie` header already applied, so the next request includes the auth cookie. This is different from Next.js, where `redirect()` in a Server Action triggers a client-side (soft) navigation that may not include newly-set cookies.
|
|
616
|
+
|
|
617
|
+
### TanStack Start
|
|
618
|
+
|
|
619
|
+
**Login from the browser (recommended):**
|
|
620
|
+
|
|
621
|
+
```tsx
|
|
622
|
+
// app/server/auth.ts - server functions for reads only
|
|
623
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
624
|
+
import { getUser } from '@netlify/identity'
|
|
625
|
+
|
|
626
|
+
export const getServerUser = createServerFn({ method: 'GET' }).handler(async () => {
|
|
627
|
+
const user = getUser()
|
|
628
|
+
return user ?? null
|
|
629
|
+
})
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
```tsx
|
|
633
|
+
// app/routes/login.tsx - browser-side auth for mutations
|
|
634
|
+
import { login, signup, onAuthChange } from '@netlify/identity'
|
|
635
|
+
import { getServerUser } from '~/server/auth'
|
|
636
|
+
|
|
637
|
+
export const Route = createFileRoute('/login')({
|
|
638
|
+
beforeLoad: async () => {
|
|
639
|
+
const user = await getServerUser()
|
|
640
|
+
if (user) throw redirect({ to: '/dashboard' })
|
|
641
|
+
},
|
|
642
|
+
component: Login,
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
function Login() {
|
|
646
|
+
const handleLogin = async (email: string, password: string) => {
|
|
647
|
+
await login(email, password) // browser-side: sets cookie + localStorage
|
|
648
|
+
window.location.href = '/dashboard'
|
|
649
|
+
}
|
|
650
|
+
// ...
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
```tsx
|
|
655
|
+
// app/routes/dashboard.tsx
|
|
656
|
+
import { logout } from '@netlify/identity'
|
|
657
|
+
import { getServerUser } from '~/server/auth'
|
|
658
|
+
|
|
659
|
+
export const Route = createFileRoute('/dashboard')({
|
|
660
|
+
beforeLoad: async () => {
|
|
661
|
+
const user = await getServerUser()
|
|
662
|
+
if (!user) throw redirect({ to: '/login' })
|
|
663
|
+
},
|
|
664
|
+
loader: async () => {
|
|
665
|
+
const user = await getServerUser()
|
|
666
|
+
return { user: user! }
|
|
667
|
+
},
|
|
668
|
+
component: Dashboard,
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
function Dashboard() {
|
|
672
|
+
const { user } = Route.useLoaderData()
|
|
673
|
+
|
|
674
|
+
const handleLogout = async () => {
|
|
675
|
+
await logout() // browser-side: clears cookie + localStorage
|
|
676
|
+
window.location.href = '/'
|
|
677
|
+
}
|
|
678
|
+
// ...
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Use `window.location.href` instead of TanStack Router's `navigate()` after auth changes. This ensures the browser sends the updated cookie on the next request.
|
|
683
|
+
|
|
684
|
+
### Astro (SSR)
|
|
685
|
+
|
|
686
|
+
**Login via API endpoint (server-side pattern):**
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
// src/pages/api/login.ts
|
|
690
|
+
import type { APIRoute } from 'astro'
|
|
691
|
+
import { login } from '@netlify/identity'
|
|
692
|
+
|
|
693
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
694
|
+
const { email, password } = await request.json()
|
|
695
|
+
|
|
696
|
+
try {
|
|
697
|
+
await login(email, password)
|
|
698
|
+
return new Response(null, {
|
|
699
|
+
status: 302,
|
|
700
|
+
headers: { Location: '/dashboard' },
|
|
701
|
+
})
|
|
702
|
+
} catch (error) {
|
|
703
|
+
return Response.json({ error: (error as Error).message }, { status: 400 })
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
```astro
|
|
709
|
+
---
|
|
710
|
+
// src/pages/dashboard.astro
|
|
711
|
+
import { getUser } from '@netlify/identity'
|
|
712
|
+
|
|
713
|
+
const user = getUser()
|
|
714
|
+
if (!user) return Astro.redirect('/login')
|
|
715
|
+
---
|
|
716
|
+
<h1>Hello, {user.email}</h1>
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### SvelteKit
|
|
720
|
+
|
|
721
|
+
**Login from the browser (recommended):**
|
|
722
|
+
|
|
723
|
+
```svelte
|
|
724
|
+
<!-- src/routes/login/+page.svelte -->
|
|
725
|
+
<script lang="ts">
|
|
726
|
+
import { login } from '@netlify/identity'
|
|
727
|
+
|
|
728
|
+
let email = ''
|
|
729
|
+
let password = ''
|
|
730
|
+
let error = ''
|
|
731
|
+
|
|
732
|
+
async function handleLogin() {
|
|
733
|
+
try {
|
|
734
|
+
await login(email, password)
|
|
735
|
+
window.location.href = '/dashboard'
|
|
736
|
+
} catch (e) {
|
|
737
|
+
error = (e as Error).message
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
</script>
|
|
741
|
+
|
|
742
|
+
<form on:submit|preventDefault={handleLogin}>
|
|
743
|
+
<input bind:value={email} type="email" />
|
|
744
|
+
<input bind:value={password} type="password" />
|
|
745
|
+
<button type="submit">Log in</button>
|
|
746
|
+
{#if error}<p>{error}</p>{/if}
|
|
747
|
+
</form>
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
// src/routes/dashboard/+page.server.ts
|
|
752
|
+
import { getUser } from '@netlify/identity'
|
|
753
|
+
import { redirect } from '@sveltejs/kit'
|
|
754
|
+
|
|
755
|
+
export function load() {
|
|
756
|
+
const user = getUser()
|
|
757
|
+
if (!user) redirect(302, '/login')
|
|
758
|
+
return { user }
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### Handling OAuth callbacks in SPAs
|
|
763
|
+
|
|
764
|
+
All SPA frameworks need a callback handler that runs on page load to process OAuth redirects, email confirmations, and password recovery tokens. Use a **wrapper component** that blocks page content while processing tokens. This prevents a flash of unauthenticated content that occurs when the page renders before the callback completes.
|
|
765
|
+
|
|
766
|
+
```tsx
|
|
767
|
+
// React component (works with Next.js, Remix, TanStack Start)
|
|
768
|
+
import { useEffect, useState } from 'react'
|
|
769
|
+
import { handleAuthCallback } from '@netlify/identity'
|
|
770
|
+
|
|
771
|
+
const AUTH_HASH_PATTERN = /^#(confirmation_token|recovery_token|invite_token|email_change_token|access_token)=/
|
|
772
|
+
|
|
773
|
+
export function CallbackHandler({ children }: { children: React.ReactNode }) {
|
|
774
|
+
const [processing, setProcessing] = useState(
|
|
775
|
+
() => typeof window !== 'undefined' && AUTH_HASH_PATTERN.test(window.location.hash),
|
|
776
|
+
)
|
|
777
|
+
const [error, setError] = useState<string | null>(null)
|
|
778
|
+
|
|
779
|
+
useEffect(() => {
|
|
780
|
+
if (!window.location.hash || !AUTH_HASH_PATTERN.test(window.location.hash)) return
|
|
781
|
+
|
|
782
|
+
handleAuthCallback()
|
|
783
|
+
.then((result) => {
|
|
784
|
+
if (!result) {
|
|
785
|
+
setProcessing(false)
|
|
786
|
+
return
|
|
787
|
+
}
|
|
788
|
+
if (result.type === 'invite') {
|
|
789
|
+
window.location.href = `/accept-invite?token=${result.token}`
|
|
790
|
+
} else if (result.type === 'recovery') {
|
|
791
|
+
window.location.href = '/reset-password'
|
|
792
|
+
} else {
|
|
793
|
+
window.location.href = '/dashboard'
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
.catch((err) => {
|
|
797
|
+
setError(err instanceof Error ? err.message : 'Callback failed')
|
|
798
|
+
setProcessing(false)
|
|
799
|
+
})
|
|
800
|
+
}, [])
|
|
801
|
+
|
|
802
|
+
if (error) return <div>Auth error: {error}</div>
|
|
803
|
+
if (processing) return <div>Confirming your account...</div>
|
|
804
|
+
return <>{children}</>
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
Wrap your page content with this component in your **root layout** so it runs on every page:
|
|
809
|
+
|
|
810
|
+
```tsx
|
|
811
|
+
// Root layout
|
|
812
|
+
<CallbackHandler>
|
|
813
|
+
<Outlet /> {/* or {children} in Next.js */}
|
|
814
|
+
</CallbackHandler>
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
If you only mount it on a `/callback` route, OAuth redirects and email confirmation links that land on other pages will not be processed.
|
|
818
|
+
|
|
383
819
|
## Guides
|
|
384
820
|
|
|
821
|
+
### React `useAuth` hook
|
|
822
|
+
|
|
823
|
+
The library is framework-agnostic, but here's a simple React hook for keeping components in sync with auth state:
|
|
824
|
+
|
|
825
|
+
```tsx
|
|
826
|
+
import { useState, useEffect } from 'react'
|
|
827
|
+
import { getUser, onAuthChange } from '@netlify/identity'
|
|
828
|
+
import type { User } from '@netlify/identity'
|
|
829
|
+
|
|
830
|
+
export function useAuth() {
|
|
831
|
+
const [user, setUser] = useState<User | null>(getUser())
|
|
832
|
+
|
|
833
|
+
useEffect(() => {
|
|
834
|
+
return onAuthChange((_event, user) => setUser(user))
|
|
835
|
+
}, [])
|
|
836
|
+
|
|
837
|
+
return user
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
```tsx
|
|
842
|
+
function NavBar() {
|
|
843
|
+
const user = useAuth()
|
|
844
|
+
return user ? <p>Hello, {user.name}</p> : <a href="/login">Log in</a>
|
|
845
|
+
}
|
|
846
|
+
```
|
|
847
|
+
|
|
385
848
|
### Listening for auth changes
|
|
386
849
|
|
|
387
|
-
Use `onAuthChange` to keep your UI in sync with auth state. It fires on login, logout, token refresh,
|
|
850
|
+
Use `onAuthChange` to keep your UI in sync with auth state. It fires on login, logout, token refresh, user updates, and recovery. It also detects session changes in other browser tabs (via `localStorage`).
|
|
388
851
|
|
|
389
852
|
```ts
|
|
390
|
-
import { onAuthChange } from '@netlify/identity'
|
|
853
|
+
import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
|
|
391
854
|
|
|
392
855
|
const unsubscribe = onAuthChange((event, user) => {
|
|
393
856
|
switch (event) {
|
|
394
|
-
case
|
|
857
|
+
case AUTH_EVENTS.LOGIN:
|
|
395
858
|
console.log('Logged in:', user?.email)
|
|
396
859
|
break
|
|
397
|
-
case
|
|
860
|
+
case AUTH_EVENTS.LOGOUT:
|
|
398
861
|
console.log('Logged out')
|
|
399
862
|
break
|
|
400
|
-
case
|
|
863
|
+
case AUTH_EVENTS.TOKEN_REFRESH:
|
|
401
864
|
console.log('Token refreshed for:', user?.email)
|
|
402
865
|
break
|
|
403
|
-
case
|
|
866
|
+
case AUTH_EVENTS.USER_UPDATED:
|
|
404
867
|
console.log('User updated:', user?.email)
|
|
405
868
|
break
|
|
869
|
+
case AUTH_EVENTS.RECOVERY:
|
|
870
|
+
console.log('Recovery login:', user?.email)
|
|
871
|
+
// Redirect to password reset form, then call updateUser({ password })
|
|
872
|
+
break
|
|
406
873
|
}
|
|
407
874
|
})
|
|
408
875
|
|
|
@@ -435,11 +902,11 @@ if (result?.type === 'oauth') {
|
|
|
435
902
|
}
|
|
436
903
|
```
|
|
437
904
|
|
|
438
|
-
`handleAuthCallback()` exchanges the token in the URL hash, logs the user in, clears the hash, and emits
|
|
905
|
+
`handleAuthCallback()` exchanges the token in the URL hash, logs the user in, clears the hash, and emits an auth event via `onAuthChange` (`'login'` for OAuth/confirmation, `'recovery'` for password recovery).
|
|
439
906
|
|
|
440
907
|
### Password recovery
|
|
441
908
|
|
|
442
|
-
Password recovery is a two-step flow. The library handles the token exchange automatically via `handleAuthCallback()`, which logs the user in and returns `{type: 'recovery', user}`. You then show a "set new password" form and call `updateUser()` to save it.
|
|
909
|
+
Password recovery is a two-step flow. The library handles the token exchange automatically via `handleAuthCallback()`, which logs the user in and returns `{type: 'recovery', user}`. A `'recovery'` event (not `'login'`) is emitted via `onAuthChange`, so event-based listeners can also detect this flow. You then show a "set new password" form and call `updateUser()` to save it.
|
|
443
910
|
|
|
444
911
|
**Step by step:**
|
|
445
912
|
|
|
@@ -460,6 +927,19 @@ if (result?.type === 'recovery') {
|
|
|
460
927
|
}
|
|
461
928
|
```
|
|
462
929
|
|
|
930
|
+
If you use the event-based pattern instead of checking `result.type`, listen for the `'recovery'` event:
|
|
931
|
+
|
|
932
|
+
```ts
|
|
933
|
+
import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
|
|
934
|
+
|
|
935
|
+
onAuthChange((event, user) => {
|
|
936
|
+
if (event === AUTH_EVENTS.RECOVERY) {
|
|
937
|
+
// Redirect to password reset form.
|
|
938
|
+
// The user is authenticated, so call updateUser({ password }) to set the new password.
|
|
939
|
+
}
|
|
940
|
+
})
|
|
941
|
+
```
|
|
942
|
+
|
|
463
943
|
### Invite acceptance
|
|
464
944
|
|
|
465
945
|
When an admin invites a user, they receive an email with an invite link. Clicking it redirects to your site with an `invite_token` in the URL hash. Unlike other callback types, the user is not logged in automatically because they need to set a password first.
|
|
@@ -481,6 +961,17 @@ if (result?.type === 'invite' && result.token) {
|
|
|
481
961
|
}
|
|
482
962
|
```
|
|
483
963
|
|
|
964
|
+
### Session lifetime
|
|
965
|
+
|
|
966
|
+
Sessions are managed by Netlify Identity (GoTrue) on the server side. The library stores two cookies:
|
|
967
|
+
|
|
968
|
+
- **`nf_jwt`**: A short-lived JWT access token (default: 1 hour). Automatically refreshed by gotrue-js in the browser using the refresh token.
|
|
969
|
+
- **`nf_refresh`**: A long-lived refresh token used to obtain new access tokens without re-authenticating.
|
|
970
|
+
|
|
971
|
+
In the browser, gotrue-js handles token refresh automatically in the background. On the server, the access token in the `nf_jwt` cookie is validated as-is; if it has expired, `getUser()` returns `null`. The user will need to refresh the page (which triggers a browser-side token refresh) or log in again.
|
|
972
|
+
|
|
973
|
+
Session lifetime is configured in your GoTrue/Identity server settings, not in this library.
|
|
974
|
+
|
|
484
975
|
## License
|
|
485
976
|
|
|
486
977
|
MIT
|