mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Merge pull request #1292 from supabase/example/next-js-with-next-js-ssr
Example/next js auth example without @supabase/ui auth component
This commit is contained in:
15 files changed
+17735
No files matched your search
@@ -0,0 +1,3 @@
|
||||
# Update these with your Supabase details from your project settings > API
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
@@ -0,0 +1,34 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -0,0 +1,5 @@
|
||||
# Example: Supabase authentication client- and server-side (API routes), and SSR with auth cookie.
|
||||
|
||||
This example shows how to use Supabase auth both on the client and server in both [API routes](https://nextjs.org/docs/api-routes/introduction) and when using [server side rendering (SSR)](https://nextjs.org/docs/basic-features/pages#server-side-rendering).
|
||||
|
||||
This example is the same as [nextjs-with-supabase-auth](https://github.com/supabase/supabase/tree/master/examples/nextjs-with-supabase-auth) however it does not use the @supabase/ui Auth component.
|
||||
@@ -0,0 +1,209 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
function Auth(props) {
|
||||
const { supabaseClient, authView, setAuthView } = props
|
||||
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [message, setMessage] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSignIn = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
const { error: signInError } = await supabaseClient.auth.signIn({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
if (signInError) setError(signInError.message)
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleSignUp = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
const { error: signUpError } = await supabaseClient.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
if (signUpError) setError(signUpError.message)
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handlePasswordReset = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setMessage('')
|
||||
setLoading(true)
|
||||
const { error } = await supabaseClient.auth.api.resetPasswordForEmail(email)
|
||||
if (error) setError(error.message)
|
||||
else setMessage('Check your email for the password reset link')
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleMagicLinkSignIn = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setMessage('')
|
||||
setLoading(true)
|
||||
const { error } = await supabaseClient.auth.signIn({ email })
|
||||
if (error) setError(error.message)
|
||||
else setMessage('Check your email for the magic link')
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <h3>Loading..</h3>}
|
||||
{error && <div style={{ color: 'red' }}>{error}</div>}
|
||||
{message && <div style={{ color: 'green' }}>{message}</div>}
|
||||
{authView === 'sign_in' ? (
|
||||
<>
|
||||
<h4>Sign in</h4>
|
||||
<form onSubmit={(e) => handleSignIn(e)}>
|
||||
<label for="sign-in__email">Email</label>
|
||||
<input
|
||||
id="sign-in__email"
|
||||
label="Email address"
|
||||
autoComplete="email"
|
||||
placeholder="Type in your email address"
|
||||
defaultValue={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<label for="sign-in__password">Password</label>
|
||||
<input
|
||||
id="sign-in__password"
|
||||
label="Password"
|
||||
type="password"
|
||||
defaultValue={password}
|
||||
autoComplete="current-password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button type="submit">SignIn</button>
|
||||
</form>
|
||||
<hr />
|
||||
<a onClick={() => setAuthView('sign_up')}>Don't have an account? Sign up</a>
|
||||
<a onClick={() => setAuthView('forgotten_password')}>Forgot my password</a>
|
||||
<hr />
|
||||
<a onClick={() => setAuthView('magic_link')}>Send magic link email</a>
|
||||
</>
|
||||
) : authView === 'sign_up' ? (
|
||||
<>
|
||||
<h4>Sign up</h4>
|
||||
<form onSubmit={(e) => handleSignUp(e)}>
|
||||
<label for="sign-up__email">Email</label>
|
||||
<input
|
||||
id="sign-up__email"
|
||||
label="Email address"
|
||||
autoComplete="email"
|
||||
placeholder="Type in your email address"
|
||||
defaultValue={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<label for="sign-up__password">Password</label>
|
||||
<input
|
||||
id="sign-up__password"
|
||||
label="Password"
|
||||
type="password"
|
||||
defaultValue={password}
|
||||
autoComplete="current-password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button type="submit">SignUp</button>
|
||||
</form>
|
||||
<hr />
|
||||
<a onClick={() => setAuthView('sign_in')}>Already have an account, Sign in</a>
|
||||
<a onClick={() => setAuthView('forgotten_password')}>Forgot my password</a>
|
||||
<hr />
|
||||
<a onClick={() => setAuthView('magic_link')}>Send magic link email</a>
|
||||
</>
|
||||
) : authView === 'forgotten_password' ? (
|
||||
<>
|
||||
<h4>Forgotten password</h4>
|
||||
<form onSubmit={handlePasswordReset}>
|
||||
<label for="forgotten_password__email">Email</label>
|
||||
<input
|
||||
id="forgotten_password__email"
|
||||
label="Email address"
|
||||
autoComplete="email"
|
||||
placeholder="Type in your email address"
|
||||
defaultValue={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<button type="submit">Send reset password instructions</button>
|
||||
</form>
|
||||
<hr />
|
||||
<a onClick={() => setAuthView('sign_up')}>Don't have an account? Sign up</a>
|
||||
<a onClick={() => setAuthView('sign_in')}>Already have an account, Sign in</a>
|
||||
<hr />
|
||||
<a onClick={() => setAuthView('magic_link')}>Send magic link email</a>
|
||||
</>
|
||||
) : authView === 'magic_link' ? (
|
||||
<>
|
||||
<h4>Magic link</h4>
|
||||
<form onSubmit={handleMagicLinkSignIn}>
|
||||
<input
|
||||
label="Email address"
|
||||
autoComplete="email"
|
||||
placeholder="Type in your email address"
|
||||
defaultValue={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<button type="submit">Send magic link</button>
|
||||
</form>
|
||||
<hr />
|
||||
<a onClick={() => setAuthView('sign_up')}>Don't have an account? Sign up</a>
|
||||
<a onClick={() => setAuthView('sign_in')}>Already have an account, Sign in</a>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function UpdatePassword({ supabaseClient }) {
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [message, setMessage] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handlePasswordReset = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setMessage('')
|
||||
setLoading(true)
|
||||
const { error } = await supabaseClient.auth.update({ password })
|
||||
if (error) setError(error.message)
|
||||
else setMessage('Your password has been updated')
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <h3>Loading..</h3>}
|
||||
{error && <div style={{ color: 'red' }}>{error}</div>}
|
||||
{message && <div style={{ color: 'green' }}>{message}</div>}
|
||||
<h4>Set a new password</h4>
|
||||
<form onSubmit={handlePasswordReset}>
|
||||
<input
|
||||
label="New password"
|
||||
placeholder="Enter your new password"
|
||||
type="password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button block size="large" htmlType="submit">
|
||||
Update password
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Auth.UpdatePassword = UpdatePassword
|
||||
export default Auth
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { useEffect, useState, createContext, useContext } from 'react'
|
||||
import { SupabaseClient, Session, User } from '@supabase/supabase-js'
|
||||
|
||||
const UserContext = createContext({ user: null, session: null })
|
||||
|
||||
export const UserContextProvider = (props) => {
|
||||
const { supabaseClient } = props
|
||||
const [session, setSession] = useState(null)
|
||||
const [user, setUser] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const session = supabaseClient.auth.session()
|
||||
setSession(session)
|
||||
setUser(session?.user ?? null)
|
||||
const { data: authListener } = supabaseClient.auth.onAuthStateChange(async (event, session) => {
|
||||
setSession(session)
|
||||
setUser(session?.user ?? null)
|
||||
})
|
||||
|
||||
return () => {
|
||||
authListener.unsubscribe()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const value = {
|
||||
session,
|
||||
user,
|
||||
}
|
||||
return <UserContext.Provider value={value} {...props} />
|
||||
}
|
||||
|
||||
export const useUser = () => {
|
||||
const context = useContext(UserContext)
|
||||
if (context === undefined) {
|
||||
throw new Error(`useUser must be used within a UserContextProvider.`)
|
||||
}
|
||||
return context
|
||||
}
|
||||
Generated
+12939
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "with-supabase-auth",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^1.2.1",
|
||||
"next": "latest",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"swr": "0.2.3"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { UserContextProvider } from '../lib/UserContext'
|
||||
import { supabase } from '../utils/initSupabase'
|
||||
import './../style.css'
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<main>
|
||||
<UserContextProvider supabaseClient={supabase}>
|
||||
<Component {...pageProps} />
|
||||
</UserContextProvider>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* NOTE: this file is only needed if you're doing SSR (getServerSideProps)!
|
||||
*/
|
||||
import { supabase } from '../../utils/initSupabase'
|
||||
|
||||
export default function handler(req, res) {
|
||||
supabase.auth.api.setAuthCookie(req, res)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { supabase } from '../../utils/initSupabase'
|
||||
|
||||
// Example of how to verify and get user data server-side.
|
||||
const getUser = async (req, res) => {
|
||||
const token = req.headers.token
|
||||
|
||||
const { data: user, error } = await supabase.auth.api.getUser(token)
|
||||
|
||||
if (error) return res.status(401).json({ error: error.message })
|
||||
return res.status(200).json(user)
|
||||
}
|
||||
|
||||
export default getUser
|
||||
@@ -0,0 +1,95 @@
|
||||
import Link from 'next/link'
|
||||
import useSWR from 'swr'
|
||||
import { supabase } from '../utils/initSupabase'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Auth from './../components/Auth'
|
||||
import { useUser } from '../lib/UserContext'
|
||||
|
||||
const fetcher = (url, token) =>
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
headers: new Headers({ 'Content-Type': 'application/json', token }),
|
||||
credentials: 'same-origin',
|
||||
}).then((res) => res.json())
|
||||
|
||||
const Index = () => {
|
||||
const { user, session } = useUser()
|
||||
const { data, error } = useSWR(session ? ['/api/getUser', session.access_token] : null, fetcher)
|
||||
const [authView, setAuthView] = useState('sign_in')
|
||||
|
||||
useEffect(() => {
|
||||
const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (event === 'PASSWORD_RECOVERY') setAuthView('update_password')
|
||||
if (event === 'USER_UPDATED') setTimeout(() => setAuthView('sign_in'), 1000)
|
||||
// Send session to /api/auth route to set the auth cookie.
|
||||
// NOTE: this is only needed if you're doing SSR (getServerSideProps)!
|
||||
fetch('/api/auth', {
|
||||
method: 'POST',
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ event, session }),
|
||||
}).then((res) => res.json())
|
||||
})
|
||||
|
||||
return () => {
|
||||
authListener.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const View = () => {
|
||||
if (!user)
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<img src="https://app.supabase.io/img/supabase-dark.svg" width="96" />
|
||||
<h2>
|
||||
Supabase Auth <br />
|
||||
with NextJS SSR
|
||||
</h2>
|
||||
</div>
|
||||
<Auth supabaseClient={supabase} authView={authView} setAuthView={setAuthView} />
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{authView === 'update_password' && <Auth.UpdatePassword supabaseClient={supabase} />}
|
||||
{user && (
|
||||
<>
|
||||
<h4>You're signed in</h4>
|
||||
<h5>Email: {user.email}</h5>
|
||||
|
||||
<button type="outline" onClick={() => supabase.auth.signOut()}>
|
||||
Log out
|
||||
</button>
|
||||
<hr />
|
||||
{error && <div style={{ color: 'red' }}>Failed to fetch user!</div>}
|
||||
{data && !error ? (
|
||||
<>
|
||||
<div style={{ color: 'green' }}>
|
||||
User data retrieved server-side (in API route):
|
||||
</div>
|
||||
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
</>
|
||||
) : (
|
||||
<div>Loading...</div>
|
||||
)}
|
||||
|
||||
<Link href="/profile">
|
||||
<a>SSR example with getServerSideProps</a>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '520px', margin: '96px auto' }}>
|
||||
<View />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Index
|
||||
@@ -0,0 +1,37 @@
|
||||
import Link from 'next/link'
|
||||
import { supabase } from '../utils/initSupabase'
|
||||
|
||||
export default function Profile({ user }) {
|
||||
return (
|
||||
<div style={{ maxWidth: '520px', margin: '96px auto' }}>
|
||||
<h4>You're signed in</h4>
|
||||
<h5>Email: {user.email}</h5>
|
||||
<hr />
|
||||
|
||||
<div style={{ color: 'green' }}>
|
||||
User data retrieved server-side (from Cookie in getServerSideProps):
|
||||
</div>
|
||||
<div>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Link href="/">
|
||||
<a>Static example with useSWR</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps({ req }) {
|
||||
const { user } = await supabase.auth.api.getUserByCookie(req)
|
||||
|
||||
if (!user) {
|
||||
// If no user, redirect to index.
|
||||
return { props: {}, redirect: { destination: '/', permanent: false } }
|
||||
}
|
||||
|
||||
// If there is a user, return it.
|
||||
return { props: { user } }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
body {
|
||||
font-family: Helvetica, Arial, Sans-Serif;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 320px;
|
||||
font-size: 16px;
|
||||
padding: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-bottom: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: black;
|
||||
color: white;
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
export const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
)
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user