mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
docs: Update react auth quickstart to use getClaims and move all code to examples folder (#42279)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a new React authentication quickstart example demonstrating email-based OTP (magic link) authentication flow with Supabase integration. * **Documentation** * Updated the React auth quickstart guide to use dynamic code samples for improved maintainability. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
1862b71745
commit
398480a3cd
9 files changed
+2135
-160
No files matched your search
@@ -74,10 +74,11 @@ hideToc: true
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```text name=.env.local
|
||||
VITE_SUPABASE_URL=your-project-url
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=sb_publishable_... or anon key
|
||||
```
|
||||
<$CodeSample
|
||||
path="/auth/quickstarts/react/.env.example"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=.env.local"
|
||||
/>
|
||||
<$Partial path="api_settings_steps.mdx" variables={{ "framework": "react", "tab": "frameworks" }} />
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
@@ -90,166 +91,16 @@ hideToc: true
|
||||
|
||||
In `App.jsx`, create a Supabase client using your Project URL and key.
|
||||
|
||||
You can configure the Auth component to display whenever there is no session inside `supabase.auth.getSession()`
|
||||
The code uses the [`getClaims`](/docs/reference/javascript/auth-getclaims) method in `App.jsx` to validate the local JWT before showing the signed-in user.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```jsx name=src/App.jsx
|
||||
import "./index.css";
|
||||
import { useState, useEffect } from "react";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
const supabase = createClient(import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY);
|
||||
|
||||
export default function App() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [session, setSession] = useState(null);
|
||||
|
||||
// Check URL params on initial render
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const hasTokenHash = params.get("token_hash");
|
||||
|
||||
const [verifying, setVerifying] = useState(!!hasTokenHash);
|
||||
const [authError, setAuthError] = useState(null);
|
||||
const [authSuccess, setAuthSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if we have token_hash in URL (magic link callback)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token_hash = params.get("token_hash");
|
||||
const type = params.get("type");
|
||||
|
||||
if (token_hash) {
|
||||
// Verify the OTP token
|
||||
supabase.auth.verifyOtp({
|
||||
token_hash,
|
||||
type: type || "email",
|
||||
}).then(({ error }) => {
|
||||
if (error) {
|
||||
setAuthError(error.message);
|
||||
} else {
|
||||
setAuthSuccess(true);
|
||||
// Clear URL params
|
||||
window.history.replaceState({}, document.title, "/");
|
||||
}
|
||||
setVerifying(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Check for existing session
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
});
|
||||
|
||||
// Listen for auth changes
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const handleLogin = async (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
const { error } = await supabase.auth.signInWithOtp({
|
||||
email,
|
||||
options: {
|
||||
emailRedirectTo: window.location.origin,
|
||||
}
|
||||
});
|
||||
if (error) {
|
||||
alert(error.error_description || error.message);
|
||||
} else {
|
||||
alert("Check your email for the login link!");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await supabase.auth.signOut();
|
||||
setSession(null);
|
||||
};
|
||||
|
||||
// Show verification state
|
||||
if (verifying) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Authentication</h1>
|
||||
<p>Confirming your magic link...</p>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show auth error
|
||||
if (authError) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Authentication</h1>
|
||||
<p>✗ Authentication failed</p>
|
||||
<p>{authError}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setAuthError(null);
|
||||
window.history.replaceState({}, document.title, "/");
|
||||
}}
|
||||
>
|
||||
Return to login
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show auth success (briefly before session loads)
|
||||
if (authSuccess && !session) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Authentication</h1>
|
||||
<p>✓ Authentication successful!</p>
|
||||
<p>Loading your account...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If user is logged in, show welcome screen
|
||||
if (session) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome!</h1>
|
||||
<p>You are logged in as: {session.user.email}</p>
|
||||
<button onClick={handleLogout}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show login form
|
||||
return (
|
||||
<div>
|
||||
<h1>Supabase + React</h1>
|
||||
<p>Sign in via magic link with your email below</p>
|
||||
<form onSubmit={handleLogin}>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
value={email}
|
||||
required={true}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<button disabled={loading}>
|
||||
{loading ? <span>Loading</span> : <span>Send magic link</span>}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/auth/quickstarts/react/src/App.jsx"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/App.jsx"
|
||||
/>
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
VITE_SUPABASE_URL=your-project-url
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=your-publishable-key-or-anon-key
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Supabase Auth + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
+1858
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "supabase-auth-react",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.93.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"vite": "^5.4.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import "./index.css";
|
||||
import { useState, useEffect } from "react";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
const supabase = createClient(import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY);
|
||||
|
||||
export default function App() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [claims, setClaims] = useState(null);
|
||||
|
||||
// Check URL params on initial render
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const hasTokenHash = params.get("token_hash");
|
||||
|
||||
const [verifying, setVerifying] = useState(!!hasTokenHash);
|
||||
const [authError, setAuthError] = useState(null);
|
||||
const [authSuccess, setAuthSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if we have token_hash in URL (magic link callback)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token_hash = params.get("token_hash");
|
||||
const type = params.get("type");
|
||||
|
||||
if (token_hash) {
|
||||
// Verify the OTP token
|
||||
supabase.auth.verifyOtp({
|
||||
token_hash,
|
||||
type: type || "email",
|
||||
}).then(({ error }) => {
|
||||
if (error) {
|
||||
setAuthError(error.message);
|
||||
} else {
|
||||
setAuthSuccess(true);
|
||||
// Clear URL params
|
||||
window.history.replaceState({}, document.title, "/");
|
||||
}
|
||||
setVerifying(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Check for existing session using getClaims
|
||||
supabase.auth.getClaims().then(({ data: { claims } }) => {
|
||||
setClaims(claims);
|
||||
});
|
||||
|
||||
// Listen for auth changes
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
supabase.auth.getClaims().then(({ data: { claims } }) => {
|
||||
setClaims(claims);
|
||||
});
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const handleLogin = async (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
const { error } = await supabase.auth.signInWithOtp({
|
||||
email,
|
||||
options: {
|
||||
emailRedirectTo: window.location.origin,
|
||||
}
|
||||
});
|
||||
if (error) {
|
||||
alert(error.error_description || error.message);
|
||||
} else {
|
||||
alert("Check your email for the login link!");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await supabase.auth.signOut();
|
||||
setClaims(null);
|
||||
};
|
||||
|
||||
// Show verification state
|
||||
if (verifying) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Authentication</h1>
|
||||
<p>Confirming your magic link...</p>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show auth error
|
||||
if (authError) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Authentication</h1>
|
||||
<p>✗ Authentication failed</p>
|
||||
<p>{authError}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setAuthError(null);
|
||||
window.history.replaceState({}, document.title, "/");
|
||||
}}
|
||||
>
|
||||
Return to login
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show auth success (briefly before claims load)
|
||||
if (authSuccess && !claims) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Authentication</h1>
|
||||
<p>✓ Authentication successful!</p>
|
||||
<p>Loading your account...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If user is logged in, show welcome screen
|
||||
if (claims) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome!</h1>
|
||||
<p>You are logged in as: {claims.email}</p>
|
||||
<button onClick={handleLogout}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show login form
|
||||
return (
|
||||
<div>
|
||||
<h1>Supabase + React</h1>
|
||||
<p>Sign in via magic link with your email below</p>
|
||||
<form onSubmit={handleLogin}>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
value={email}
|
||||
required={true}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<button disabled={loading}>
|
||||
{loading ? <span>Loading</span> : <span>Send magic link</span>}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #3ecf8e;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
background-color: #3ecf8e;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #2db97a;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in new issue
Block a user