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:
Copple authored and GitHub committed 2021-05-02 10:56:24 +08:00
commit 4de5e6e19a
15 files changed
+17735

No files matched your search

+3
View File
@@ -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
+34
View File
@@ -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
+5
View File
@@ -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.
+209
View File
@@ -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
+39
View File
@@ -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
}
File diff suppressed because it is too large. Load diff
+17
View File
@@ -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"
}
+13
View File
@@ -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>
)
}
+8
View File
@@ -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)
}
+13
View File
@@ -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
+95
View File
@@ -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
+37
View File
@@ -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 } }
}
+29
View File
@@ -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