mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
docs: Update nextjs user management guide to use getClaims (#44124)
This commit is contained in:
1 parent
fa4a668eeb
commit
8e33378315
7 files changed
+1296
-1401
No files matched your search
@@ -44,7 +44,7 @@ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY
|
||||
|
||||
### App styling (optional)
|
||||
|
||||
An optional step is to update the CSS file `app/globals.css` to make the app look nice.
|
||||
An optional step is to update the CSS file `app/globals.css` to make the app look better.
|
||||
You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-user-management/app/globals.css).
|
||||
|
||||
### Supabase Server-Side Auth
|
||||
@@ -66,7 +66,7 @@ There are two different types of clients in Supabase:
|
||||
1. **Client Component client** - To access Supabase from Client Components, which run in the browser.
|
||||
2. **Server Component client** - To access Supabase from Server Components, Server Actions, and Route Handlers, which run only on the server.
|
||||
|
||||
It is recommended to create the following essential utilities files for creating clients, and organize them within `lib/supabase` at the root of the project.
|
||||
We recommend creating the following essential utilities files for creating clients, and organize them within `lib/supabase` at the root of the project.
|
||||
|
||||
Create a `client.ts` and a `server.ts` with the following functionalities for client-side Supabase and server-side Supabase, respectively.
|
||||
|
||||
@@ -88,9 +88,11 @@ meta="name=lib/supabase/server.ts"
|
||||
|
||||
### Next.js proxy
|
||||
|
||||
Since Server Components can't write cookies, you need [Proxy](https://nextjs.org/docs/app/getting-started/proxy) to refresh expired Auth tokens and store them. This is accomplished by:
|
||||
Since Server Components can't write cookies, you need [Proxy](https://nextjs.org/docs/app/getting-started/proxy) to refresh expired Auth tokens and store them.
|
||||
|
||||
- Refreshing the Auth token with the call to `supabase.auth.getUser`.
|
||||
You can accomplish this by:
|
||||
|
||||
- Refreshing the Auth token with the call to `supabase.auth.getClaims`.
|
||||
- Passing the refreshed Auth token to Server Components through `request.cookies.set`, so they don't attempt to refresh the same token themselves.
|
||||
- Passing the refreshed Auth token to the browser, so it replaces the old token. This is done with `response.cookies.set`.
|
||||
|
||||
@@ -100,15 +102,15 @@ You could also add a matcher, so that the Proxy only runs on routes that access
|
||||
|
||||
Be careful when protecting pages. The server gets the user session from the cookies, which anyone can spoof.
|
||||
|
||||
Always use `supabase.auth.getUser()` to protect pages and user data.
|
||||
Most of the time, use `supabase.auth.getClaims()` to protect pages and user data.
|
||||
|
||||
_Never_ trust `supabase.auth.getSession()` inside server code such as proxy. It isn't guaranteed to revalidate the Auth token.
|
||||
|
||||
It's safe to trust `getUser()` because it sends a request to the Supabase Auth server every time to revalidate the Auth token.
|
||||
It's safe to trust `getClaims()` because it validates the token in storage, either directly or by calling `getUser()` solely to check the result. It doesn't use the response from `getUser()` itself, only whether the validation succeeded.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Create a `proxy.ts` file at the project root and another one within the `lib/supabase` folder. The `lib/supabase` file contains the logic for updating the session. This is used by the `proxy.ts` file, which is a Next.js convention.
|
||||
Create a `proxy.ts` file at the project root and another one within the `lib/supabase` folder. The `lib/supabase` file contains the logic for updating the session. The `proxy.ts` file uses this, which is a Next.js convention.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
@@ -184,9 +186,9 @@ Before proceeding, change the email template to support support a server-side au
|
||||
- Select the **Confirm signup** template.
|
||||
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
|
||||
|
||||
<Admonition type="tip">
|
||||
<Admonition type="tip" label="Did you know?">
|
||||
|
||||
**Did you know?** You can also customize other emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
You can also customize other emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { type User } from '@supabase/supabase-js'
|
||||
import Avatar from './avatar'
|
||||
|
||||
export default function AccountForm({ user }: { user: User | null }) {
|
||||
type Claims = { sub: string; email?: string; [key: string]: unknown }
|
||||
|
||||
export default function AccountForm({ claims }: { claims: Claims | null }) {
|
||||
const supabase = createClient()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [fullname, setFullname] = useState<string | null>(null)
|
||||
@@ -14,12 +15,17 @@ export default function AccountForm({ user }: { user: User | null }) {
|
||||
|
||||
const getProfile = useCallback(async () => {
|
||||
try {
|
||||
if (!claims?.sub) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
const { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`full_name, username, website, avatar_url`)
|
||||
.eq('id', user?.id)
|
||||
.eq('id', claims.sub)
|
||||
.single()
|
||||
|
||||
if (error && status !== 406) {
|
||||
@@ -38,11 +44,11 @@ export default function AccountForm({ user }: { user: User | null }) {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [user, supabase])
|
||||
}, [claims, supabase])
|
||||
|
||||
useEffect(() => {
|
||||
getProfile()
|
||||
}, [user, getProfile])
|
||||
}, [claims, getProfile])
|
||||
|
||||
async function updateProfile({
|
||||
username,
|
||||
@@ -55,10 +61,15 @@ export default function AccountForm({ user }: { user: User | null }) {
|
||||
avatar_url: string | null
|
||||
}) {
|
||||
try {
|
||||
if (!claims?.sub) {
|
||||
alert('You must be logged in to update your profile')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
const { error } = await supabase.from('profiles').upsert({
|
||||
id: user?.id as string,
|
||||
id: claims.sub,
|
||||
full_name: fullname,
|
||||
username,
|
||||
website,
|
||||
@@ -77,7 +88,7 @@ export default function AccountForm({ user }: { user: User | null }) {
|
||||
return (
|
||||
<div className="form-widget">
|
||||
<Avatar
|
||||
uid={user?.id ?? null}
|
||||
uid={claims?.sub ?? null}
|
||||
url={avatar_url}
|
||||
size={150}
|
||||
onUpload={(url) => {
|
||||
@@ -87,7 +98,7 @@ export default function AccountForm({ user }: { user: User | null }) {
|
||||
/>
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" type="text" value={user?.email} disabled />
|
||||
<input id="email" type="text" value={claims?.email ?? ''} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="fullName">Full Name</label>
|
||||
@@ -121,7 +132,7 @@ export default function AccountForm({ user }: { user: User | null }) {
|
||||
<button
|
||||
className="button primary block"
|
||||
onClick={() => updateProfile({ fullname, username, website, avatar_url })}
|
||||
disabled={loading}
|
||||
disabled={loading || !claims?.sub}
|
||||
>
|
||||
{loading ? 'Loading ...' : 'Update'}
|
||||
</button>
|
||||
|
||||
@@ -4,9 +4,7 @@ import { createClient } from '@/lib/supabase/server'
|
||||
export default async function Account() {
|
||||
const supabase = await createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
const { data: claimsData } = await supabase.auth.getClaims()
|
||||
|
||||
return <AccountForm user={user} />
|
||||
return <AccountForm claims={claimsData?.claims ?? null} />
|
||||
}
|
||||
@@ -6,11 +6,9 @@ export async function POST(req: NextRequest) {
|
||||
const supabase = await createClient()
|
||||
|
||||
// Check if a user's logged in
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
const { data: claimsData } = await supabase.auth.getClaims()
|
||||
|
||||
if (user) {
|
||||
if (claimsData?.claims) {
|
||||
await supabase.auth.signOut()
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,13 @@ export async function updateSession(request: NextRequest) {
|
||||
}
|
||||
)
|
||||
|
||||
// refreshing the auth token
|
||||
await supabase.auth.getUser()
|
||||
// Do not run code between createServerClient and
|
||||
// supabase.auth.getClaims(). A simple mistake could make it very hard to debug
|
||||
// issues with users being randomly logged out.
|
||||
|
||||
// IMPORTANT: If you remove getClaims() and you use server-side rendering
|
||||
// with the Supabase client, your users may be randomly logged out.
|
||||
await supabase.auth.getClaims()
|
||||
|
||||
return supabaseResponse
|
||||
}
|
||||
+1241
-1360
File diff suppressed because it is too large.
Load diff
@@ -9,20 +9,20 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/ssr": "^0.6.1",
|
||||
"@supabase/supabase-js": "^2.53.0",
|
||||
"next": "latest",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
"@supabase/ssr": "^0.9.0",
|
||||
"@supabase/supabase-js": "^2.100.0",
|
||||
"next": "^16.2.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-next": "15.4.5",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5"
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"eslint": "^10.1.0",
|
||||
"eslint-config-next": "16.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.2"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user