chore: update examples supabase-js v2
This commit is contained in:
5
examples/archive/README.md
Normal file
5
examples/archive/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Supabase Examples Archive
|
||||
|
||||
## supabase-js v1
|
||||
|
||||
You can find the supabase-js v1 examples at [github.com/supabase/examples-archive](https://github.com/supabase/examples-archive).
|
||||
3
examples/auth/javascript-auth/.gitignore
vendored
3
examples/auth/javascript-auth/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
dist/*
|
||||
|
||||
.vercel
|
||||
@@ -1,14 +0,0 @@
|
||||
# Vanilla-js Auth Example
|
||||
|
||||
Live Example: [https://auth-vanilla-js.vercel.app/](https://auth-vanilla-js.vercel.app/)
|
||||
|
||||
How to sign up and login using supabase and supabase-js using HTML and JavaScript only
|
||||
|
||||
<img width="558" alt="image" src="https://user-images.githubusercontent.com/458736/88377414-b6fb4180-cdd1-11ea-8061-103ec4577b7b.png">
|
||||
|
||||
### running
|
||||
|
||||
`npm run dev` (requires npx to be installed)
|
||||
|
||||
if you want to make changes without restarting the server run this in a different terminal window:
|
||||
`npm run watch`
|
||||
@@ -1,16 +0,0 @@
|
||||
body {
|
||||
/* margin: 0; */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: rgb(250, 217, 246);
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 1em;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@1"></script>
|
||||
<script src="./index.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="./index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='container'>
|
||||
<div class='section'>
|
||||
<h1>Supabase Auth Example</h1>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<a href="https://github.com/supabase/supabase/tree/master/examples/auth/javascript-auth">View the code on GitHub</a>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<form id='sign-up'>
|
||||
<h3>Sign Up</h3>
|
||||
<label>Email</label><input type='email' name='email' />
|
||||
<label>Password</label><input type='password' name='password' />
|
||||
<input type='submit'>
|
||||
</form>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<form id='log-in'>
|
||||
<h3>Log In</h3>
|
||||
<label>Email</label><input type='email' name='email' />
|
||||
<label>Password</label><input type='password' name='password' />
|
||||
<input type='submit'>
|
||||
</form>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<form id='validate'>
|
||||
<h3>Access Token</h3>
|
||||
<input readonly=readonly type='text' id='access-token' /> <small>Default expiry is 60 minutes</small>
|
||||
<h3>Refresh Token</h3>
|
||||
<input readonly=readonly type='text' id='refresh-token' /> <small>Supabase-js will use this to automatically fetch a new accessToken for you every 60 mins whilst the client is running</small>
|
||||
</form>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<h3>Fetch User Details</h3>
|
||||
<button id='user-button'>Fetch</button>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<h3>Logout</h3>
|
||||
<button id='logout-button'>Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,79 +0,0 @@
|
||||
var SUPABASE_URL = 'https://ernhobnpmmupjnmxpfbt.supabase.co'
|
||||
var SUPABASE_KEY =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxMzA5ODU0NCwiZXhwIjoxOTI4Njc0NTQ0fQ.Z9bRrfaL2oGhSuyBckFcdcnRIJDelWJ1II98OnEtLO0'
|
||||
|
||||
var supabase = supabase.createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
window.userToken = null
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function (event) {
|
||||
var signUpForm = document.querySelector('#sign-up')
|
||||
signUpForm.onsubmit = signUpSubmitted.bind(signUpForm)
|
||||
|
||||
var logInForm = document.querySelector('#log-in')
|
||||
logInForm.onsubmit = logInSubmitted.bind(logInForm)
|
||||
|
||||
var userDetailsButton = document.querySelector('#user-button')
|
||||
userDetailsButton.onclick = fetchUserDetails.bind(userDetailsButton)
|
||||
|
||||
var logoutButton = document.querySelector('#logout-button')
|
||||
logoutButton.onclick = logoutSubmitted.bind(logoutButton)
|
||||
})
|
||||
|
||||
const signUpSubmitted = (event) => {
|
||||
event.preventDefault()
|
||||
const email = event.target[0].value
|
||||
const password = event.target[1].value
|
||||
|
||||
supabase.auth
|
||||
.signUp({ email, password })
|
||||
.then((response) => {
|
||||
response.error ? alert(response.error.message) : setToken(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
}
|
||||
|
||||
const logInSubmitted = (event) => {
|
||||
event.preventDefault()
|
||||
const email = event.target[0].value
|
||||
const password = event.target[1].value
|
||||
|
||||
supabase.auth
|
||||
.signIn({ email, password })
|
||||
.then((response) => {
|
||||
response.error ? alert(response.error.message) : setToken(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
alert(err.response.text)
|
||||
})
|
||||
}
|
||||
|
||||
const fetchUserDetails = () => {
|
||||
alert(JSON.stringify(supabase.auth.user()))
|
||||
}
|
||||
|
||||
const logoutSubmitted = (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
supabase.auth
|
||||
.signOut()
|
||||
.then((_response) => {
|
||||
document.querySelector('#access-token').value = ''
|
||||
document.querySelector('#refresh-token').value = ''
|
||||
alert('Logout successful')
|
||||
})
|
||||
.catch((err) => {
|
||||
alert(err.response.text)
|
||||
})
|
||||
}
|
||||
|
||||
function setToken(response) {
|
||||
if (response.user.confirmation_sent_at && !response?.session?.access_token) {
|
||||
alert('Confirmation Email Sent')
|
||||
} else {
|
||||
document.querySelector('#access-token').value = response.session.access_token
|
||||
document.querySelector('#refresh-token').value = response.session.refresh_token
|
||||
alert('Logged in as ' + response.user.email)
|
||||
}
|
||||
}
|
||||
5
examples/auth/javascript-auth/package-lock.json
generated
5
examples/auth/javascript-auth/package-lock.json
generated
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "supabase-auth-vanilla-js",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 1
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "supabase-auth-vanilla-js",
|
||||
"version": "0.0.1",
|
||||
"description": "basic auth example for supabase with js",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "npx light-server -s ."
|
||||
},
|
||||
"author": "ant@supabase.io",
|
||||
"license": "MIT"
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
@@ -1,3 +0,0 @@
|
||||
# Update these with your Supabase details from your project settings > API
|
||||
NEXT_PUBLIC_SUPABASE_URL=
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||
35
examples/auth/nextjs-auth-tailwind/.gitignore
vendored
35
examples/auth/nextjs-auth-tailwind/.gitignore
vendored
@@ -1,35 +0,0 @@
|
||||
# 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
|
||||
/
|
||||
.env
|
||||
|
||||
# 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
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<img src="./.assets/Screen.png">
|
||||
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
@@ -1,12 +0,0 @@
|
||||
import { default as HeadContainer } from 'next/head'
|
||||
|
||||
const Head = () => {
|
||||
return (
|
||||
<HeadContainer>
|
||||
<title>Supabase Example</title>
|
||||
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
|
||||
</HeadContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Head
|
||||
@@ -1,87 +0,0 @@
|
||||
import React from 'react'
|
||||
import AuthUser from '../../hooks/authUser'
|
||||
import { Disclosure } from '@headlessui/react'
|
||||
import { MenuIcon, XIcon } from '@heroicons/react/outline'
|
||||
import MenuLogado from './menuLogado'
|
||||
import MenuNotLogado from './menuNotLogado'
|
||||
import Navigation from './navigation'
|
||||
import classNames from '../../utils/classsesNames'
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<Disclosure as="nav" className="bg-gray-800">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||
<div className="relative flex items-center justify-between h-16">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||
{/* Mobile menu button*/}
|
||||
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
) : (
|
||||
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<div className="flex-shrink-0 flex items-center text-white">
|
||||
<img
|
||||
className="h-8 w-auto"
|
||||
src="https://supabase.com/images/logo-dark.png"
|
||||
alt="supabase"
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden sm:block sm:ml-6">
|
||||
<div className="flex space-x-4">
|
||||
{Navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current
|
||||
? 'bg-gray-900 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
|
||||
'px-3 py-2 rounded-md text-sm font-medium'
|
||||
)}
|
||||
aria-current={item.current ? 'page' : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||
{/** notifications */}
|
||||
|
||||
{AuthUser() ? <MenuLogado /> : <MenuNotLogado />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Disclosure.Panel className="sm:hidden">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||
{Navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current
|
||||
? 'bg-gray-900 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
|
||||
'block px-3 py-2 rounded-md text-base font-medium'
|
||||
)}
|
||||
aria-current={item.current ? 'page' : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Fragment } from 'react'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import { UserCircleIcon } from '@heroicons/react/outline'
|
||||
import classNames from '../../utils/classsesNames'
|
||||
|
||||
import { SignOut } from '../../hooks/authUser'
|
||||
|
||||
const MenuLogado = () => (
|
||||
<Menu as="div" className="ml-3 relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Menu.Button className="bg-gray-800 flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<UserCircleIcon className="h-8 w-8 text-white" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
static
|
||||
className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
href="/profile"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700'
|
||||
)}
|
||||
>
|
||||
Your Profile
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
href="/#"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700'
|
||||
)}
|
||||
>
|
||||
Settings
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<button onClick={() => SignOut()} className="block px-4 py-2 text-sm text-gray-700">
|
||||
Sign out
|
||||
</button>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
)
|
||||
|
||||
export default MenuLogado
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
const MenuNotLogado = () => (
|
||||
<div className="flex space-x-4">
|
||||
<Link href="/auth">
|
||||
<a className="text-white hover:bg-gray-700 px-3 py-2 rounded-md text-sm font-medium">LOGIN</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default MenuNotLogado
|
||||
@@ -1,7 +0,0 @@
|
||||
const Navigation = [
|
||||
{ name: 'Home', href: '/', current: true },
|
||||
{ name: 'Jobs', href: '#', current: false },
|
||||
{ name: 'Developers', href: '#', current: false },
|
||||
]
|
||||
|
||||
export default Navigation
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useEffect, useState, createContext, useContext } from 'react'
|
||||
import { supabase } from '../utils/initSupabase'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export const SignOut = async () => {
|
||||
await supabase.auth.signOut()
|
||||
}
|
||||
|
||||
export const RequireAuth = () => {
|
||||
const { user } = useUser()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
router.push('/auth')
|
||||
}
|
||||
}, [user, router])
|
||||
}
|
||||
|
||||
export const AuthRedirect = () => {
|
||||
const { user } = useUser()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
router.push('/profile')
|
||||
}
|
||||
}, [user, router])
|
||||
}
|
||||
|
||||
export const UserContext = createContext()
|
||||
|
||||
export const UserContextProvider = (props) => {
|
||||
const [session, setSession] = useState(false)
|
||||
const [user, setUser] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const session = supabase.auth.session()
|
||||
setSession(session)
|
||||
setUser(session?.user ?? false)
|
||||
const { data: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
setSession(session)
|
||||
setUser(session?.user ?? false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
authListener.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const AuthUser = () => {
|
||||
const { user } = useUser()
|
||||
return user
|
||||
}
|
||||
|
||||
export default AuthUser
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "nextjs-auth-tailwind",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"format": "prettier --write './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.2.0",
|
||||
"@heroicons/react": "^1.0.1",
|
||||
"@supabase/supabase-js": "^1.11.15",
|
||||
"@supabase/ui": "^0.26.1",
|
||||
"@tailwindcss/typography": "^0.4.0",
|
||||
"next": "12.0.9",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.2.5",
|
||||
"postcss": "^8.3.0",
|
||||
"prettier": "^2.3.0",
|
||||
"tailwindcss": "^2.1.2"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react'
|
||||
import { UserContextProvider } from '../hooks/authUser'
|
||||
|
||||
import 'tailwindcss/tailwind.css'
|
||||
import '../styles/globals.css'
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<main className={'dark'}>
|
||||
<UserContextProvider>
|
||||
<Component {...pageProps} />
|
||||
</UserContextProvider>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react'
|
||||
import { AuthRedirect } from '../hooks/authUser'
|
||||
import { supabase } from '../utils/initSupabase'
|
||||
import { Auth, Card, Typography, Space } from '@supabase/ui'
|
||||
|
||||
import Head from '../components/Head'
|
||||
import Header from '../components/Header'
|
||||
|
||||
const AuthPage = () => {
|
||||
AuthRedirect()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head />
|
||||
<Header />
|
||||
<div className="authcontainer">
|
||||
<Card>
|
||||
<Space direction="vertical" size={8}>
|
||||
<div>
|
||||
<Typography.Title level={3}>Welcome</Typography.Title>
|
||||
</div>
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
providers={['google', 'github']}
|
||||
view={'sign_in'}
|
||||
socialLayout="horizontal"
|
||||
socialButtonSize="xlarge"
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthPage
|
||||
@@ -1,44 +0,0 @@
|
||||
import Header from '../components/Header'
|
||||
import Head from '../components/Head'
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<>
|
||||
<Head />
|
||||
|
||||
<Header />
|
||||
<main className="mt-10 mx-auto max-w-7xl px-4 sm:mt-12 sm:px-6 md:mt-16 lg:mt-20 lg:px-8 xl:mt-28">
|
||||
<div className="sm:text-center lg:text-left">
|
||||
<h1 className="text-4xl tracking-tight font-extrabold text-white sm:text-5xl md:text-6xl">
|
||||
<span className="block xl:inline">The Open Source</span>{' '}
|
||||
<span className="block text-green-400 xl:inline">Firebase Alternative</span>
|
||||
</h1>
|
||||
<p className="mt-3 text-base text-white sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0">
|
||||
Create a backend in less than 2 minutes. Start your project with a Postgres Database,
|
||||
Authentication, instant APIs, Realtime subscriptions and Storage.
|
||||
</p>
|
||||
<div className="mt-5 sm:mt-8 sm:flex sm:justify-center lg:justify-start">
|
||||
<div className="rounded-md shadow">
|
||||
<a
|
||||
href="#"
|
||||
className="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-white bg-green-400 hover:bg-green-600 md:py-4 md:text-lg md:px-10"
|
||||
>
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-0 sm:ml-3">
|
||||
<a
|
||||
href="#"
|
||||
className="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-green-400 bg-indigo-100 hover:bg-indigo-200 md:py-4 md:text-lg md:px-10"
|
||||
>
|
||||
Live demo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Index
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react'
|
||||
import { useUser, RequireAuth } from '../hooks/authUser'
|
||||
|
||||
import { Card, Typography, Space } from '@supabase/ui'
|
||||
import Header from '../components/Header'
|
||||
|
||||
export default function Profile() {
|
||||
RequireAuth()
|
||||
|
||||
const { user } = useUser()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
{user && (
|
||||
<div style={{ maxWidth: '620px', margin: '96px auto' }}>
|
||||
<Card>
|
||||
<Space direction="vertical" size={6}>
|
||||
<Typography.Text>you're signed in</Typography.Text>
|
||||
<Typography.Text strong>Email: {user.email}</Typography.Text>
|
||||
<Typography.Text type="success">User data:</Typography.Text>
|
||||
|
||||
<Typography.Text>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,29 +0,0 @@
|
||||
body {
|
||||
background: #181818;
|
||||
}
|
||||
|
||||
/*
|
||||
* Auth
|
||||
*/
|
||||
|
||||
.authcontainer {
|
||||
max-width: 420px;
|
||||
margin: 96px auto;
|
||||
}
|
||||
|
||||
.authcontainer a {
|
||||
color: #8b5cf6 !important;
|
||||
}
|
||||
|
||||
.authcontainer button[type='submit'] {
|
||||
background: #8b5cf6 !important;
|
||||
}
|
||||
|
||||
.authcontainer input:focus {
|
||||
border-color: #8b5cf6 !important;
|
||||
}
|
||||
|
||||
.authcontainer .sbui-typography-text-danger {
|
||||
top: 3px !important;
|
||||
position: absolute !important;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export default classNames
|
||||
@@ -1,6 +0,0 @@
|
||||
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
@@ -1,3 +0,0 @@
|
||||
# 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
examples/auth/nextjs-auth/.gitignore
vendored
34
examples/auth/nextjs-auth/.gitignore
vendored
@@ -1,34 +0,0 @@
|
||||
# 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
|
||||
@@ -1,5 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,209 +0,0 @@
|
||||
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 htmlFor="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 htmlFor="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 htmlFor="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 htmlFor="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 htmlFor="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
|
||||
@@ -1,38 +0,0 @@
|
||||
import React, { useEffect, useState, createContext, useContext } from 'react'
|
||||
|
||||
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
|
||||
}
|
||||
5571
examples/auth/nextjs-auth/package-lock.json
generated
5571
examples/auth/nextjs-auth/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"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": "12.0.9",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"swr": "0.2.3"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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
|
||||
@@ -1,95 +0,0 @@
|
||||
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.com/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
|
||||
@@ -1,37 +0,0 @@
|
||||
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 } }
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
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
@@ -1,2 +0,0 @@
|
||||
SUPABASE_SERVICE_KEY="{SERVICE_KEY}"
|
||||
SUPABASE_URL="https://{YOUR_INSTANCE_NAME}.supabase.co"
|
||||
6
examples/auth/remix-auth/.gitignore
vendored
6
examples/auth/remix-auth/.gitignore
vendored
@@ -1,6 +0,0 @@
|
||||
node_modules
|
||||
|
||||
/.cache
|
||||
/build
|
||||
/public/build
|
||||
.env
|
||||
@@ -1,53 +0,0 @@
|
||||
# Remix Auth - Supabase Strategy with redirectTo
|
||||
|
||||
Authentication using `signInWithEmail` handling redirectTo.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Copy `.env.example` to create a new file `.env`:
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Go to https://app.supabase.com/project/{PROJECT}/api?page=auth to find your secrets
|
||||
3. Add your `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE` in `.env`
|
||||
|
||||
```env
|
||||
SUPABASE_SERVICE_KEY="{SERVICE_KEY}"
|
||||
SUPABASE_URL="https://{YOUR_INSTANCE_NAME}.supabase.co"
|
||||
```
|
||||
|
||||
## Using the Remix Auth & SupabaseStrategy 🚀
|
||||
|
||||
SupabaseStrategy provides `checkSession` working like Remix Auth `isAuthenticated` but handles token refresh
|
||||
|
||||
You must use `checkSession` instead of `isAuthenticated`
|
||||
|
||||
## Example
|
||||
|
||||
This is using Remix Auth, `remix-auth-supabase` and `supabase-js` packages.
|
||||
|
||||
> Thanks to Remix, we can securely use server only authentication with `supabase.auth.api.signInWithEmail`
|
||||
>
|
||||
> This function should only be called on a server (`loader` or `action` functions).
|
||||
>
|
||||
> **⚠️ Never expose your `service_role` key in the browser**
|
||||
|
||||
The `/login` route renders a form with a email and password input. After a submit it runs some validations and store `user` object, `access_token` and `refresh_token` in the session.
|
||||
|
||||
The `/private` routes redirects the user to `/login` if it's not logged-in, or shows the user email and a logout form if it's logged-in.
|
||||
|
||||
The `/private/profile` routes redirects the user to `/login?redirectTo=/private/profile` if it's not logged-in, or shows the user email and a logout form if it's logged-in.
|
||||
|
||||
If the user go to `/private/profile` and is not logged-in, it'll be redirected here after login success.
|
||||
|
||||
**Handle refreshing of tokens** (if expired) or redirects to `/login` if it fails
|
||||
|
||||
More use cases can be found on [Remix Auth Supabase - Use cases](https://github.com/mitchelvanbever/remix-auth-supabase#using-the-authenticator--strategy-)
|
||||
|
||||
## Related Links
|
||||
|
||||
- [Remix Auth](https://github.com/sergiodxa/remix-auth)
|
||||
- [Remix Auth Supabase](https://github.com/mitchelvanbever/remix-auth-supabase)
|
||||
- [supabase-js](https://github.com/supabase/supabase-js)
|
||||
@@ -1,53 +0,0 @@
|
||||
import { createCookieSessionStorage } from 'remix'
|
||||
import { Authenticator, AuthorizationError } from 'remix-auth'
|
||||
import { SupabaseStrategy } from 'remix-auth-supabase'
|
||||
import { supabaseClient } from '~/supabase'
|
||||
import type { Session } from '~/supabase'
|
||||
|
||||
export const sessionStorage = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: 'sb',
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
secrets: ['s3cr3t'], // This should be an env variable
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
})
|
||||
|
||||
export const supabaseStrategy = new SupabaseStrategy(
|
||||
{
|
||||
supabaseClient,
|
||||
sessionStorage,
|
||||
sessionKey: 'sb:session',
|
||||
sessionErrorKey: 'sb:error',
|
||||
},
|
||||
async ({ req, supabaseClient }) => {
|
||||
const form = await req.formData()
|
||||
const email = form?.get('email')
|
||||
const password = form?.get('password')
|
||||
|
||||
if (!email) throw new AuthorizationError('Email is required')
|
||||
if (typeof email !== 'string') throw new AuthorizationError('Email must be a string')
|
||||
|
||||
if (!password) throw new AuthorizationError('Password is required')
|
||||
if (typeof password !== 'string') throw new AuthorizationError('Password must be a string')
|
||||
|
||||
return supabaseClient.auth.api
|
||||
.signInWithEmail(email, password)
|
||||
.then(({ data, error }): Session => {
|
||||
if (error || !data) {
|
||||
throw new AuthorizationError(error?.message ?? 'No user session found')
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export const authenticator = new Authenticator<Session>(sessionStorage, {
|
||||
sessionKey: supabaseStrategy.sessionKey,
|
||||
sessionErrorKey: supabaseStrategy.sessionErrorKey,
|
||||
})
|
||||
|
||||
authenticator.use(supabaseStrategy)
|
||||
@@ -1,4 +0,0 @@
|
||||
import { hydrate } from 'react-dom'
|
||||
import { RemixBrowser } from 'remix'
|
||||
|
||||
hydrate(<RemixBrowser />, document)
|
||||
@@ -1,19 +0,0 @@
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { RemixServer } from 'remix'
|
||||
import type { EntryContext } from 'remix'
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
const markup = renderToString(<RemixServer context={remixContext} url={request.url} />)
|
||||
|
||||
responseHeaders.set('Content-Type', 'text/html')
|
||||
|
||||
return new Response('<!DOCTYPE html>' + markup, {
|
||||
status: responseStatusCode,
|
||||
headers: responseHeaders,
|
||||
})
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from 'remix'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
{process.env.NODE_ENV === 'development' && <LiveReload />}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Link } from 'remix'
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<>
|
||||
<h1>Index page</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/private">Go to private page</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/private/profile">Go to private/profile page</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/login">Go to login page</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { ActionFunction, LoaderFunction } from 'remix'
|
||||
import { Form, json, useLoaderData, useSearchParams } from 'remix'
|
||||
import { authenticator, sessionStorage, supabaseStrategy } from '~/auth.server'
|
||||
|
||||
type LoaderData = {
|
||||
error: { message: string } | null
|
||||
}
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const data = await request.clone().formData()
|
||||
const redirectTo = (data.get('redirectTo') ?? '/private') as string
|
||||
|
||||
await authenticator.authenticate('sb', request, {
|
||||
successRedirect: redirectTo,
|
||||
failureRedirect: '/login',
|
||||
})
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const redirectTo = new URL(request.url).searchParams.get('redirectTo') ?? '/private'
|
||||
|
||||
await supabaseStrategy.checkSession(request, {
|
||||
successRedirect: redirectTo,
|
||||
})
|
||||
|
||||
const session = await sessionStorage.getSession(request.headers.get('Cookie'))
|
||||
|
||||
const error = session.get(authenticator.sessionErrorKey) as LoaderData['error']
|
||||
|
||||
return json<LoaderData>({ error })
|
||||
}
|
||||
|
||||
export default function Screen() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const { error } = useLoaderData<LoaderData>()
|
||||
|
||||
return (
|
||||
<Form method="post">
|
||||
{error && <div>{error.message}</div>}
|
||||
<input
|
||||
name="redirectTo"
|
||||
value={searchParams.get('redirectTo') ?? undefined}
|
||||
hidden
|
||||
readOnly
|
||||
/>
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input type="email" name="email" id="email" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password">Password</label>
|
||||
<input type="password" name="password" id="password" />
|
||||
</div>
|
||||
|
||||
<button>Log In</button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { ActionFunction, LoaderFunction } from 'remix'
|
||||
import { Form, json, useLoaderData } from 'remix'
|
||||
import { authenticator, supabaseStrategy } from '~/auth.server'
|
||||
|
||||
type LoaderData = { email?: string; id?: string }
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
await authenticator.logout(request, { redirectTo: '/login' })
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const session = await supabaseStrategy.checkSession(request, {
|
||||
failureRedirect: '/login?redirectTo=/private/profile',
|
||||
})
|
||||
|
||||
return json<LoaderData>({ email: session.user?.email, id: session.user?.id })
|
||||
}
|
||||
|
||||
export default function Screen() {
|
||||
const { email, id } = useLoaderData<LoaderData>()
|
||||
return (
|
||||
<>
|
||||
<h1>Hello {email}</h1>
|
||||
<h2>Welcome in Private Profile</h2>
|
||||
<h3>Your user id is {id}</h3>
|
||||
|
||||
<Form method="post">
|
||||
<button>Log Out</button>
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { ActionFunction, LoaderFunction } from 'remix'
|
||||
import { Form, json, useLoaderData } from 'remix'
|
||||
import { authenticator, supabaseStrategy } from '~/auth.server'
|
||||
|
||||
type LoaderData = { email?: string }
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
await authenticator.logout(request, { redirectTo: '/login' })
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const session = await supabaseStrategy.checkSession(request, {
|
||||
failureRedirect: '/login',
|
||||
})
|
||||
|
||||
return json<LoaderData>({ email: session.user?.email })
|
||||
}
|
||||
|
||||
export default function Screen() {
|
||||
const { email } = useLoaderData<LoaderData>()
|
||||
return (
|
||||
<>
|
||||
<h1>Hello {email}</h1>
|
||||
<h2>Welcome in Private index</h2>
|
||||
|
||||
<Form method="post">
|
||||
<button>Log Out</button>
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import type { Session } from '@supabase/supabase-js'
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
SUPABASE_URL: string
|
||||
SUPABASE_SERVICE_KEY: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!process.env.SUPABASE_URL) {
|
||||
throw new Error('SUPABASE_URL is required')
|
||||
}
|
||||
|
||||
if (!process.env.SUPABASE_SERVICE_KEY) {
|
||||
throw new Error('SUPABASE_SERVICE_KEY is required')
|
||||
}
|
||||
|
||||
// Supabase options example (build your own :))
|
||||
// https://supabase.com/docs/reference/javascript/initializing#with-additional-parameters
|
||||
|
||||
// const supabaseOptions = {
|
||||
// fetch, // see ⚠️ cloudflare
|
||||
// schema: "public",
|
||||
// persistSession: true,
|
||||
// autoRefreshToken: true,
|
||||
// detectSessionInUrl: true,
|
||||
// headers: { "x-application-name": "{my-site-name}" }
|
||||
// };
|
||||
|
||||
// ⚠️ cloudflare needs you define fetch option : https://github.com/supabase/supabase-js#custom-fetch-implementation
|
||||
// Use Remix fetch polyfill for node (See https://remix.run/docs/en/v1/other-api/node)
|
||||
|
||||
export const supabaseClient = createClient(
|
||||
process.env.SUPABASE_URL,
|
||||
process.env.SUPABASE_SERVICE_KEY
|
||||
// supabaseOptions
|
||||
)
|
||||
|
||||
export { Session }
|
||||
9907
examples/auth/remix-auth/package-lock.json
generated
9907
examples/auth/remix-auth/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "remix-app",
|
||||
"description": "",
|
||||
"license": "",
|
||||
"scripts": {
|
||||
"build": "remix build",
|
||||
"dev": "node -r dotenv/config node_modules/.bin/remix dev",
|
||||
"postinstall": "remix setup node",
|
||||
"start": "remix-serve build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remix-run/react": "^1.1.1",
|
||||
"@remix-run/serve": "^1.5.0",
|
||||
"@supabase/supabase-js": "^1.29.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"remix": "^1.1.1",
|
||||
"remix-auth": "^3.2.1",
|
||||
"remix-auth-supabase": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^1.1.1",
|
||||
"@types/react": "^17.0.38",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"dotenv": "^10.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* @type {import('@remix-run/dev/config').AppConfig}
|
||||
*/
|
||||
module.exports = {
|
||||
appDirectory: 'app',
|
||||
browserBuildDirectory: 'public/build',
|
||||
publicPath: '/build/',
|
||||
serverBuildDirectory: 'build',
|
||||
devServerPort: 8002,
|
||||
}
|
||||
2
examples/auth/remix-auth/remix.env.d.ts
vendored
2
examples/auth/remix-auth/remix.env.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
/// <reference types="@remix-run/dev" />
|
||||
/// <reference types="@remix-run/node/globals" />
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2021"],
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"target": "ES2019",
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
|
||||
// Remix takes care of building everything in `remix build`.
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
1
examples/edge-functions/.temp/project-ref
Normal file
1
examples/edge-functions/.temp/project-ref
Normal file
@@ -0,0 +1 @@
|
||||
uvqnpvxfahfkegscgmjl
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@supabase/auth-ui-react": "^0.2.1",
|
||||
"@supabase/supabase-js": "^2.0.0-rc.12",
|
||||
"@supabase/supabase-js": "^2.0.0",
|
||||
"@testing-library/jest-dom": "^5.16.3",
|
||||
"@testing-library/react": "^12.1.4",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.0.0-rc.12'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.0.0'
|
||||
import { corsHeaders } from '../_shared/cors.ts'
|
||||
|
||||
console.log(`Function "get-tshirt-competition" up and running!`)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.0.0-rc.12'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.0.0'
|
||||
import { corsHeaders } from '../_shared/cors.ts'
|
||||
|
||||
console.log(`Function "select-from-table-with-auth-rls" up and running!`)
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
|
||||
import { serve } from 'https://deno.land/std@0.132.0/http/server.ts'
|
||||
|
||||
// esm.sh is used to compile stripe-node to be compatible with ES modules.
|
||||
import Stripe from 'https://esm.sh/stripe@9.6.0?target=deno&no-check'
|
||||
import Stripe from 'https://esm.sh/stripe@10.13.0?target=deno&deno-std=0.132.0'
|
||||
|
||||
const stripe = Stripe(Deno.env.get('STRIPE_API_KEY'), {
|
||||
// This is needed to use the Fetch API rather than relying on the Node http
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# 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
examples/nextjs-with-supabase-auth/.gitignore
vendored
34
examples/nextjs-with-supabase-auth/.gitignore
vendored
@@ -1,34 +0,0 @@
|
||||
# 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
|
||||
@@ -1,9 +0,0 @@
|
||||
# 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 uses helper functions that can be viewed at [supabase auth helpers repository](https://github.com/supabase-community/supabase-auth-helpers).
|
||||
|
||||
## Deploy with Vercel
|
||||
|
||||
The Vercel deployment will guide you through creating a Supabase account and project. After installation of the Supabase integration, all relevant environment variables will be set up so that the project is usable immediately after deployment 🚀
|
||||
|
||||
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fnextjs-with-supabase-auth&project-name=nextjs-with-supabase-auth&repository-name=nextjs-with-supabase-auth&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv)
|
||||
1243
examples/nextjs-with-supabase-auth/package-lock.json
generated
1243
examples/nextjs-with-supabase-auth/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "with-supabase-auth",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-auth-helpers": "^1.0.6",
|
||||
"next": "^12.0.9",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"swr": "^0.5.5"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { UserProvider } from '@supabase/supabase-auth-helpers/react'
|
||||
import { supabaseClient } from '@supabase/supabase-auth-helpers/nextjs'
|
||||
import './../style.css'
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<main className={'dark'}>
|
||||
<UserProvider supabaseClient={supabaseClient}>
|
||||
<Component {...pageProps} />
|
||||
</UserProvider>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { handleAuth } from '@supabase/supabase-auth-helpers/nextjs'
|
||||
|
||||
export default handleAuth()
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useUser, Auth } from '@supabase/supabase-auth-helpers/react'
|
||||
import { supabaseClient } from '@supabase/supabase-auth-helpers/nextjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const LoginPage = () => {
|
||||
const { user, error } = useUser()
|
||||
const [data, setData] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
const { data } = await supabaseClient.from('test').select('*')
|
||||
setData(data)
|
||||
}
|
||||
// Only run query once user is logged in.
|
||||
if (user) loadData()
|
||||
}, [user])
|
||||
|
||||
if (!user)
|
||||
return (
|
||||
<>
|
||||
{error && <p>{error.message}</p>}
|
||||
<Auth
|
||||
// view="update_password"
|
||||
supabaseClient={supabaseClient}
|
||||
providers={['google', 'github']}
|
||||
socialLayout="horizontal"
|
||||
socialButtonSize="xlarge"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => supabaseClient.auth.signOut()}>Sign out</button>
|
||||
<p>user:</p>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
<p>client-side data fetching with RLS</p>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
||||
@@ -1,13 +0,0 @@
|
||||
// pages/profile.js
|
||||
import { withAuthRequired } from '@supabase/supabase-auth-helpers/nextjs'
|
||||
|
||||
export default function Profile({ user }) {
|
||||
return (
|
||||
<>
|
||||
<div>Hello {user.email}</div>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = withAuthRequired()
|
||||
@@ -1,4 +0,0 @@
|
||||
body {
|
||||
background: #3d3d3d;
|
||||
font-family: Helvetica, Arial, Sans-Serif;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,26 +25,46 @@ export const useStore = (props) => {
|
||||
fetchChannels(setChannels)
|
||||
// Listen for new and deleted messages
|
||||
const messageListener = supabase
|
||||
.from('messages')
|
||||
.on('INSERT', (payload) => handleNewMessage(payload.new))
|
||||
.on('DELETE', (payload) => handleDeletedMessage(payload.old))
|
||||
.channel('public:messages')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'INSERT', schema: 'public', table: 'messages' },
|
||||
(payload) => handleNewMessage(payload.new)
|
||||
)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'DELETE', schema: 'public', table: 'messages' },
|
||||
(payload) => handleDeletedMessage(payload.old)
|
||||
)
|
||||
.subscribe()
|
||||
// Listen for changes to our users
|
||||
const userListener = supabase
|
||||
.from('users')
|
||||
.on('*', (payload) => handleNewOrUpdatedUser(payload.new))
|
||||
.channel('public:users')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: '*', schema: 'public', table: 'users' },
|
||||
(payload) => handleNewOrUpdatedUser(payload.new)
|
||||
)
|
||||
.subscribe()
|
||||
// Listen for new and deleted channels
|
||||
const channelListener = supabase
|
||||
.from('channels')
|
||||
.on('INSERT', (payload) => handleNewChannel(payload.new))
|
||||
.on('DELETE', (payload) => handleDeletedChannel(payload.old))
|
||||
.channel('public:channels')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'INSERT', schema: 'public', table: 'channels' },
|
||||
(payload) => handleNewChannel(payload.new)
|
||||
)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'DELETE', schema: 'public', table: 'channels' },
|
||||
(payload) => handleDeletedChannel(payload.old)
|
||||
)
|
||||
.subscribe()
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
messageListener.unsubscribe()
|
||||
userListener.unsubscribe()
|
||||
channelListener.unsubscribe()
|
||||
supabase.removeChannel('public:messages')
|
||||
supabase.removeChannel('public:users')
|
||||
supabase.removeChannel('public:channels')
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -110,9 +130,9 @@ export const useStore = (props) => {
|
||||
*/
|
||||
export const fetchChannels = async (setState) => {
|
||||
try {
|
||||
let { body } = await supabase.from('channels').select('*')
|
||||
if (setState) setState(body)
|
||||
return body
|
||||
let { data } = await supabase.from('channels').select('*')
|
||||
if (setState) setState(data)
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
@@ -125,8 +145,8 @@ export const fetchChannels = async (setState) => {
|
||||
*/
|
||||
export const fetchUser = async (userId, setState) => {
|
||||
try {
|
||||
let { body } = await supabase.from('users').select(`*`).eq('id', userId)
|
||||
let user = body[0]
|
||||
let { data } = await supabase.from('users').select(`*`).eq('id', userId)
|
||||
let user = data[0]
|
||||
if (setState) setState(user)
|
||||
return user
|
||||
} catch (error) {
|
||||
@@ -140,9 +160,9 @@ export const fetchUser = async (userId, setState) => {
|
||||
*/
|
||||
export const fetchUserRoles = async (setState) => {
|
||||
try {
|
||||
let { body } = await supabase.from('user_roles').select(`*`)
|
||||
if (setState) setState(body)
|
||||
return body
|
||||
let { data } = await supabase.from('user_roles').select(`*`)
|
||||
if (setState) setState(data)
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
@@ -155,13 +175,13 @@ export const fetchUserRoles = async (setState) => {
|
||||
*/
|
||||
export const fetchMessages = async (channelId, setState) => {
|
||||
try {
|
||||
let { body } = await supabase
|
||||
let { data } = await supabase
|
||||
.from('messages')
|
||||
.select(`*, author:user_id(*)`)
|
||||
.eq('channel_id', channelId)
|
||||
.order('inserted_at', true)
|
||||
if (setState) setState(body)
|
||||
return body
|
||||
if (setState) setState(data)
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
@@ -174,8 +194,8 @@ export const fetchMessages = async (channelId, setState) => {
|
||||
*/
|
||||
export const addChannel = async (slug, user_id) => {
|
||||
try {
|
||||
let { body } = await supabase.from('channels').insert([{ slug, created_by: user_id }])
|
||||
return body
|
||||
let { data } = await supabase.from('channels').insert([{ slug, created_by: user_id }]).select()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
@@ -189,8 +209,8 @@ export const addChannel = async (slug, user_id) => {
|
||||
*/
|
||||
export const addMessage = async (message, channel_id, user_id) => {
|
||||
try {
|
||||
let { body } = await supabase.from('messages').insert([{ message, channel_id, user_id }])
|
||||
return body
|
||||
let { data } = await supabase.from('messages').insert([{ message, channel_id, user_id }]).select()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
@@ -202,8 +222,8 @@ export const addMessage = async (message, channel_id, user_id) => {
|
||||
*/
|
||||
export const deleteChannel = async (channel_id) => {
|
||||
try {
|
||||
let { body } = await supabase.from('channels').delete().match({ id: channel_id })
|
||||
return body
|
||||
let { data } = await supabase.from('channels').delete().match({ id: channel_id })
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
@@ -215,8 +235,8 @@ export const deleteChannel = async (channel_id) => {
|
||||
*/
|
||||
export const deleteMessage = async (message_id) => {
|
||||
try {
|
||||
let { body } = await supabase.from('messages').delete().match({ id: message_id })
|
||||
return body
|
||||
let { data } = await supabase.from('messages').delete().match({ id: message_id })
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "supabase-slack-clone-basic",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "supabase-slack-clone-basic",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^1.35.3",
|
||||
"next": "latest",
|
||||
"@supabase/supabase-js": "^2.0.0-rc.9",
|
||||
"next": "*",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"sass": "^1.26.2",
|
||||
"tailwindcss": "^1.1.4"
|
||||
"tailwindcss": "^1.9.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullhuman/postcss-purgecss": {
|
||||
@@ -296,56 +296,57 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.3.tgz",
|
||||
"integrity": "sha512-35vO9niHRtzGe1QSvXKdOfvGPiX2KC44dGpWU6y0/gZCfTIgog/soU9HqABzQC/maVowO3hGLWfez5aN0MKfow==",
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-cYFaSA28NoTREM4ZrHWaxXjElfY0S8umrjoffTbmI4Rgkg8+4raBnQIi52usfr5MuFpy566vhbLNOcP9kNHjOw==",
|
||||
"dependencies": {
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/gotrue-js": {
|
||||
"version": "1.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.15.tgz",
|
||||
"integrity": "sha512-7/mwnd1hR/bpkCmbDvjnwPfWyRcE2B1ZnfxthqgVaZ5oJHS/CQibyuLBL8DA75fxmgY9nIfednDZSydSm6zK0w==",
|
||||
"version": "2.0.0-rc.4",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.0.0-rc.4.tgz",
|
||||
"integrity": "sha512-jHWyF1/Rl/1MXybFnHnHVjXYrOuvLzSUACow2Ii2KFD7zURPjPzy5KvOzq6tk+wL92LDgCl88nU1QVvO7dvRog==",
|
||||
"dependencies": {
|
||||
"cross-fetch": "^3.0.6"
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "0.37.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-0.37.2.tgz",
|
||||
"integrity": "sha512-3Dgx5k3RvtKqc8DvR2BEyh2fVyjZe5P4e0zD1r8dyuVmpaYDaASZ2YeNVgyWXMCWH7xzrj4vepTYlKwfj78QLg==",
|
||||
"version": "1.0.0-rc.5",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.0.0-rc.5.tgz",
|
||||
"integrity": "sha512-rBOyhK+qLd2fdX6WbCW3e9gY9qbwC5Zeso6QWM+00RlRz3k156lxnreVpe4KKGXNT3JMqJwAHXjQ2lbxmtBJqg==",
|
||||
"dependencies": {
|
||||
"cross-fetch": "^3.0.6"
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.2.tgz",
|
||||
"integrity": "sha512-DMUaFIKj7KszGtWTTQbhMmUzZf7UnwYqySsmY+G8HgYxvY3ZaVa+DZD0I6ofgr4OLNr0po/ODM2a4lf5m5GNBg==",
|
||||
"version": "2.0.0-rc.5",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.0.0-rc.5.tgz",
|
||||
"integrity": "sha512-0JiSJjoTmwleufGzBQwEVamI047jnAiemBk8qOkgwyoLOlfGTQk8XSA7aT023BsGVgRNKZqp/jJJAVBht/Rd2w==",
|
||||
"dependencies": {
|
||||
"@types/phoenix": "^1.5.4",
|
||||
"websocket": "^1.0.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.0.tgz",
|
||||
"integrity": "sha512-f5EBw0wM96hKmnrXhgiqq2Reh9O0NgjKE+jkaKY4jQmfutefqaCAWn+cBzlmHs9h135H2ldaGmhWRFHUSkLt2g==",
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-OjPtPuKzhzQW6nO/UdwVJVFrzfFnIWt1u+gTQ4+8t1+IBdn7xDHssDteBPkMCd+fYcbMZrqpggbagoFH3FylXw==",
|
||||
"dependencies": {
|
||||
"cross-fetch": "^3.1.0"
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "1.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.35.3.tgz",
|
||||
"integrity": "sha512-uwO8OVdMFsGZNZ1xQhFz22+PSW0EWYZ5xVq+jQeGz8nhabEu+Q9Uyep/bcNzOpyPJRzbGfxSPRzgAdAxfJgFhw==",
|
||||
"version": "2.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-WTb60mEjzkI7FcuQA7KW9IkLltl2I7ojlpBIORwbekuCz5nKDSlo4g6/Wyo4u6Qe97weh3mhFMLGZRorhRq7lg==",
|
||||
"dependencies": {
|
||||
"@supabase/functions-js": "^1.3.3",
|
||||
"@supabase/gotrue-js": "^1.22.14",
|
||||
"@supabase/postgrest-js": "^0.37.2",
|
||||
"@supabase/realtime-js": "^1.7.2",
|
||||
"@supabase/storage-js": "^1.7.0"
|
||||
"@supabase/functions-js": "^2.0.0-rc.1",
|
||||
"@supabase/gotrue-js": "^2.0.0-rc.4",
|
||||
"@supabase/postgrest-js": "^1.0.0-rc.5",
|
||||
"@supabase/realtime-js": "^2.0.0-rc.5",
|
||||
"@supabase/storage-js": "^2.0.0-rc.1",
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
@@ -723,9 +724,9 @@
|
||||
"integrity": "sha512-/s3+fwhKf1YK4k7btOImOzCQLpUjS6MaPf0ODTNuT4eTM1Bg4itBpLkydhOzJmpmH6Z9eXFyuuK5czsmzRzwtw=="
|
||||
},
|
||||
"node_modules/es5-ext": {
|
||||
"version": "0.10.61",
|
||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.61.tgz",
|
||||
"integrity": "sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA==",
|
||||
"version": "0.10.62",
|
||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
|
||||
"integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"es6-iterator": "^2.0.3",
|
||||
@@ -739,7 +740,7 @@
|
||||
"node_modules/es6-iterator": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
|
||||
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
|
||||
"dependencies": {
|
||||
"d": "1",
|
||||
"es5-ext": "^0.10.35",
|
||||
@@ -772,17 +773,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ext": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz",
|
||||
"integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
|
||||
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
|
||||
"dependencies": {
|
||||
"type": "^2.5.0"
|
||||
"type": "^2.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ext/node_modules/type": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/type/-/type-2.6.0.tgz",
|
||||
"integrity": "sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ=="
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
|
||||
"integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
@@ -972,7 +973,7 @@
|
||||
"node_modules/is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
||||
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
@@ -1022,7 +1023,7 @@
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.2",
|
||||
@@ -1117,9 +1118,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz",
|
||||
"integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz",
|
||||
"integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
@@ -1719,7 +1720,7 @@
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/type": {
|
||||
"version": "1.2.0",
|
||||
@@ -1762,7 +1763,7 @@
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/websocket": {
|
||||
"version": "1.0.34",
|
||||
@@ -1783,7 +1784,7 @@
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
@@ -1805,7 +1806,7 @@
|
||||
"node_modules/yaeti": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
|
||||
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=",
|
||||
"integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==",
|
||||
"engines": {
|
||||
"node": ">=0.10.32"
|
||||
}
|
||||
@@ -1965,56 +1966,57 @@
|
||||
"optional": true
|
||||
},
|
||||
"@supabase/functions-js": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.3.tgz",
|
||||
"integrity": "sha512-35vO9niHRtzGe1QSvXKdOfvGPiX2KC44dGpWU6y0/gZCfTIgog/soU9HqABzQC/maVowO3hGLWfez5aN0MKfow==",
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-cYFaSA28NoTREM4ZrHWaxXjElfY0S8umrjoffTbmI4Rgkg8+4raBnQIi52usfr5MuFpy566vhbLNOcP9kNHjOw==",
|
||||
"requires": {
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"@supabase/gotrue-js": {
|
||||
"version": "1.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.15.tgz",
|
||||
"integrity": "sha512-7/mwnd1hR/bpkCmbDvjnwPfWyRcE2B1ZnfxthqgVaZ5oJHS/CQibyuLBL8DA75fxmgY9nIfednDZSydSm6zK0w==",
|
||||
"version": "2.0.0-rc.4",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.0.0-rc.4.tgz",
|
||||
"integrity": "sha512-jHWyF1/Rl/1MXybFnHnHVjXYrOuvLzSUACow2Ii2KFD7zURPjPzy5KvOzq6tk+wL92LDgCl88nU1QVvO7dvRog==",
|
||||
"requires": {
|
||||
"cross-fetch": "^3.0.6"
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"@supabase/postgrest-js": {
|
||||
"version": "0.37.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-0.37.2.tgz",
|
||||
"integrity": "sha512-3Dgx5k3RvtKqc8DvR2BEyh2fVyjZe5P4e0zD1r8dyuVmpaYDaASZ2YeNVgyWXMCWH7xzrj4vepTYlKwfj78QLg==",
|
||||
"version": "1.0.0-rc.5",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.0.0-rc.5.tgz",
|
||||
"integrity": "sha512-rBOyhK+qLd2fdX6WbCW3e9gY9qbwC5Zeso6QWM+00RlRz3k156lxnreVpe4KKGXNT3JMqJwAHXjQ2lbxmtBJqg==",
|
||||
"requires": {
|
||||
"cross-fetch": "^3.0.6"
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"@supabase/realtime-js": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.2.tgz",
|
||||
"integrity": "sha512-DMUaFIKj7KszGtWTTQbhMmUzZf7UnwYqySsmY+G8HgYxvY3ZaVa+DZD0I6ofgr4OLNr0po/ODM2a4lf5m5GNBg==",
|
||||
"version": "2.0.0-rc.5",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.0.0-rc.5.tgz",
|
||||
"integrity": "sha512-0JiSJjoTmwleufGzBQwEVamI047jnAiemBk8qOkgwyoLOlfGTQk8XSA7aT023BsGVgRNKZqp/jJJAVBht/Rd2w==",
|
||||
"requires": {
|
||||
"@types/phoenix": "^1.5.4",
|
||||
"websocket": "^1.0.34"
|
||||
}
|
||||
},
|
||||
"@supabase/storage-js": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.0.tgz",
|
||||
"integrity": "sha512-f5EBw0wM96hKmnrXhgiqq2Reh9O0NgjKE+jkaKY4jQmfutefqaCAWn+cBzlmHs9h135H2ldaGmhWRFHUSkLt2g==",
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-OjPtPuKzhzQW6nO/UdwVJVFrzfFnIWt1u+gTQ4+8t1+IBdn7xDHssDteBPkMCd+fYcbMZrqpggbagoFH3FylXw==",
|
||||
"requires": {
|
||||
"cross-fetch": "^3.1.0"
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"@supabase/supabase-js": {
|
||||
"version": "1.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.35.3.tgz",
|
||||
"integrity": "sha512-uwO8OVdMFsGZNZ1xQhFz22+PSW0EWYZ5xVq+jQeGz8nhabEu+Q9Uyep/bcNzOpyPJRzbGfxSPRzgAdAxfJgFhw==",
|
||||
"version": "2.0.0-rc.9",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.0.0-rc.9.tgz",
|
||||
"integrity": "sha512-WTb60mEjzkI7FcuQA7KW9IkLltl2I7ojlpBIORwbekuCz5nKDSlo4g6/Wyo4u6Qe97weh3mhFMLGZRorhRq7lg==",
|
||||
"requires": {
|
||||
"@supabase/functions-js": "^1.3.3",
|
||||
"@supabase/gotrue-js": "^1.22.14",
|
||||
"@supabase/postgrest-js": "^0.37.2",
|
||||
"@supabase/realtime-js": "^1.7.2",
|
||||
"@supabase/storage-js": "^1.7.0"
|
||||
"@supabase/functions-js": "^2.0.0-rc.1",
|
||||
"@supabase/gotrue-js": "^2.0.0-rc.4",
|
||||
"@supabase/postgrest-js": "^1.0.0-rc.5",
|
||||
"@supabase/realtime-js": "^2.0.0-rc.5",
|
||||
"@supabase/storage-js": "^2.0.0-rc.1",
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"@types/phoenix": {
|
||||
@@ -2287,9 +2289,9 @@
|
||||
"integrity": "sha512-/s3+fwhKf1YK4k7btOImOzCQLpUjS6MaPf0ODTNuT4eTM1Bg4itBpLkydhOzJmpmH6Z9eXFyuuK5czsmzRzwtw=="
|
||||
},
|
||||
"es5-ext": {
|
||||
"version": "0.10.61",
|
||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.61.tgz",
|
||||
"integrity": "sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA==",
|
||||
"version": "0.10.62",
|
||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
|
||||
"integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==",
|
||||
"requires": {
|
||||
"es6-iterator": "^2.0.3",
|
||||
"es6-symbol": "^3.1.3",
|
||||
@@ -2299,7 +2301,7 @@
|
||||
"es6-iterator": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
|
||||
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
|
||||
"requires": {
|
||||
"d": "1",
|
||||
"es5-ext": "^0.10.35",
|
||||
@@ -2326,17 +2328,17 @@
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
},
|
||||
"ext": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz",
|
||||
"integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
|
||||
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
|
||||
"requires": {
|
||||
"type": "^2.5.0"
|
||||
"type": "^2.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/type/-/type-2.6.0.tgz",
|
||||
"integrity": "sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ=="
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
|
||||
"integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2479,7 +2481,7 @@
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
||||
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
@@ -2523,7 +2525,7 @@
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.2",
|
||||
@@ -2575,9 +2577,9 @@
|
||||
}
|
||||
},
|
||||
"node-gyp-build": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz",
|
||||
"integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ=="
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz",
|
||||
"integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg=="
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "2.0.3",
|
||||
@@ -3032,7 +3034,7 @@
|
||||
"tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"type": {
|
||||
"version": "1.2.0",
|
||||
@@ -3068,7 +3070,7 @@
|
||||
"webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"websocket": {
|
||||
"version": "1.0.34",
|
||||
@@ -3086,7 +3088,7 @@
|
||||
"whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"requires": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
@@ -3105,7 +3107,7 @@
|
||||
"yaeti": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
|
||||
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
|
||||
"integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "supabase-slack-clone-basic",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
@@ -9,11 +9,11 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^1.35.3",
|
||||
"@supabase/supabase-js": "^2.0.0-rc.9",
|
||||
"next": "latest",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"sass": "^1.26.2",
|
||||
"tailwindcss": "^1.1.4"
|
||||
"tailwindcss": "^1.9.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import '~/styles/style.scss'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import Router from 'next/router'
|
||||
import { useRouter } from 'next/router'
|
||||
import UserContext from 'lib/UserContext'
|
||||
import { supabase, fetchUserRoles } from 'lib/Store'
|
||||
|
||||
@@ -9,40 +9,44 @@ export default function SupabaseSlackClone({ Component, pageProps }) {
|
||||
const [user, setUser] = useState(null)
|
||||
const [session, setSession] = useState(null)
|
||||
const [userRoles, setUserRoles] = useState([])
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const session = supabase.auth.session()
|
||||
setSession(session)
|
||||
setUser(session?.user ?? null)
|
||||
setUserLoaded(session ? true : false)
|
||||
if (user) {
|
||||
signIn()
|
||||
Router.push('/channels/[id]', '/channels/1')
|
||||
}
|
||||
supabase.auth.getSession().then(({ data: { session }}) => {
|
||||
setSession(session)
|
||||
setUserLoaded(session ? true : false)
|
||||
if (session?.user) {
|
||||
signIn()
|
||||
router.push('/channels/[id]', '/channels/1')
|
||||
}
|
||||
})
|
||||
|
||||
const { data: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
|
||||
const { subscription: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
setSession(session)
|
||||
const currentUser = session?.user
|
||||
setUser(currentUser ?? null)
|
||||
setUserLoaded(!!currentUser)
|
||||
if (currentUser) {
|
||||
signIn(currentUser.id, currentUser.email)
|
||||
Router.push('/channels/[id]', '/channels/1')
|
||||
router.push('/channels/[id]', '/channels/1')
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
authListener.unsubscribe()
|
||||
}
|
||||
}, [user])
|
||||
}, [])
|
||||
|
||||
const signIn = async () => {
|
||||
await fetchUserRoles((userRoles) => setUserRoles(userRoles.map((userRole) => userRole.role)))
|
||||
}
|
||||
|
||||
const signOut = async () => {
|
||||
const result = await supabase.auth.signOut()
|
||||
Router.push('/')
|
||||
const { error } = await supabase.auth.signOut()
|
||||
if (!error) {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,9 +7,9 @@ const Home = () => {
|
||||
|
||||
const handleLogin = async (type, username, password) => {
|
||||
try {
|
||||
const { error, user } =
|
||||
const { error, data: { user } } =
|
||||
type === 'LOGIN'
|
||||
? await supabase.auth.signIn({ email: username, password })
|
||||
? await supabase.auth.signInWithPassword({ email: username, password })
|
||||
: await supabase.auth.signUp({ email: username, password })
|
||||
// If the user doesn't exist here and an error hasn't been raised yet,
|
||||
// that must mean that a confirmation email has been sent.
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
# Todo example using Supabase & Angular
|
||||
|
||||
- Frontend:
|
||||
- [Angular](https://angular.io)
|
||||
- [Supabase.js](https://supabase.com/docs/library/getting-started) for user management and realtime data syncing.
|
||||
- Backend:
|
||||
- [app.supabase.com](https://app.supabase.com/): hosted Postgres database with restful API for usage with Supabase.js.
|
||||
|
||||
## Deploy your own
|
||||
|
||||
### 1. Create new project
|
||||
|
||||
Sign up to Supabase - [https://app.supabase.com](https://app.supabase.com) and create a new project. Wait for your database to start.
|
||||
|
||||
### 2. Run "Todo List" Quickstart
|
||||
|
||||
Once your database has started, run the "Todo List" quickstart. Inside of your project, enter the `SQL editor` tab and scroll down until you see `TODO LIST: Build a basic todo list with Row Level Security`.
|
||||
|
||||
### 3. Get the URL and Key
|
||||
|
||||
Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step.
|
||||
|
||||
The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token. This enables row level security for your data. Read more about this [below](#postgres-row-level-security).
|
||||
|
||||

|
||||
|
||||
**_NOTE_**: The `service_role` key has full access to your data, bypassing any security policies. These keys have to be kept secret and are meant to be used in server environments and never on a client or browser.
|
||||
|
||||
### 4. Run the Angular client locally
|
||||
|
||||
Copy the url and the key retrieved into the previous step into the `environment.ts` file :
|
||||
|
||||
```ts
|
||||
export const environment = {
|
||||
production: false,
|
||||
supabaseUrl: 'YOUR_SUPABASE_URL',
|
||||
supabaseKey: 'YOUR_SUPABASE_KEY',
|
||||
}
|
||||
```
|
||||
|
||||
run `ng serve`
|
||||
|
||||
### 5. Deploy the Angular client
|
||||
|
||||
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2todo-list%2Fangular-todo-list&env=ANGULAR_APP_SUPABASE_URL,ANGULAR_APP_SUPABASE_KEY&envDescription=Find%20the%20Supabase%20URL%20and%20key%20in%20the%20your%20auto-generated%20docs%20at%20app.supabase.com&project-name=supabase-todo-list&repo-name=supabase-todo-list)
|
||||
|
||||
Here, we recommend forking this repo so you can deploy through Vercel by clicking the button above. When you click the button, replace the repo URL with your fork's URL.
|
||||
|
||||
You will be asked for a `ANGULAR_APP_SUPABASE_URL` and `ANGULAR_APP_SUPABASE_KEY`. Use the API URL and `anon` key from [step 3](#3.-get-the-url-and-key).
|
||||
|
||||
### 6. Change authentication settings if necessary
|
||||
|
||||

|
||||
|
||||
On [app.supabase.com](https://app.supabase.com), you can go to Authentication -> Settings to change your auth settings for your project if necessary. Here, you can change the site URL, which is used for determining where to redirect users after they confirm their email addresses or attempt to use a magic link to log in.
|
||||
|
||||
Here, you can also enable external oauth providers, such as Google and GitHub.
|
||||
|
||||
Instructions to activate Google provider : https://supabase.com/docs/guides/auth/auth-google
|
||||
Instructions to activate GitHub provider : https://supabase.com/docs/guides/auth/auth-github
|
||||
|
||||
## Supabase details
|
||||
|
||||
### Postgres Row level security
|
||||
|
||||
This project uses very high-level Authorization using Postgres' Role Level Security.
|
||||
When you start a Postgres database on Supabase, we populate it with an `auth` schema, and some helper functions.
|
||||
When a user logs in, they are issued a JWT with the role `authenticated` and their UUID.
|
||||
We can use these details to provide fine-grained control over what each user can and cannot do.
|
||||
|
||||
This is a trimmed-down schema, with the policies:
|
||||
|
||||
```sql
|
||||
create table todos (
|
||||
id bigint generated by default as identity primary key,
|
||||
user_id uuid references auth.users not null,
|
||||
task text check (char_length(task) > 3),
|
||||
is_complete boolean default false,
|
||||
inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
|
||||
);
|
||||
|
||||
alter table todos enable row level security;
|
||||
|
||||
create policy "Individuals can create todos." on todos for
|
||||
insert with check (auth.uid() = user_id);
|
||||
|
||||
create policy "Individuals can view their own todos. " on todos for
|
||||
select using (auth.uid() = user_id);
|
||||
|
||||
create policy "Individuals can update their own todos." on todos for
|
||||
update using (auth.uid() = user_id);
|
||||
|
||||
create policy "Individuals can delete their own todos." on todos for
|
||||
delete using (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
- [Supabase](https://supabase.com)
|
||||
- [Gerome Grignon](https://github.com/geromegrignon)
|
||||
|
||||
Supabase is open source, we'd love for you to follow along and get involved at https://github.com/supabase/supabase
|
||||
|
||||
## Local development
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
moduleNameMapper: {
|
||||
'@core/(.*)': '<rootDir>/src/app/core/$1',
|
||||
},
|
||||
preset: 'jest-preset-angular',
|
||||
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
|
||||
}
|
||||
16131
examples/todo-list/angular-todo-list/package-lock.json
generated
16131
examples/todo-list/angular-todo-list/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"name": "angular-todo-list",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Gerome Grignon",
|
||||
"url": "https://github.com/geromegrignon"
|
||||
},
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"config": "ts-node set-env.ts",
|
||||
"build": "npm run config && ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~12.1.1",
|
||||
"@angular/common": "~12.1.1",
|
||||
"@angular/compiler": "~12.1.1",
|
||||
"@angular/core": "~12.1.1",
|
||||
"@angular/forms": "~12.1.1",
|
||||
"@angular/platform-browser": "~12.1.1",
|
||||
"@angular/platform-browser-dynamic": "~12.1.1",
|
||||
"@angular/router": "~12.1.1",
|
||||
"@briebug/jest-schematic": "^3.1.0",
|
||||
"@ngneat/tailwind": "^7.0.3",
|
||||
"@supabase/supabase-js": "^1.18.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"rxjs": "~6.6.0",
|
||||
"tslib": "^2.2.0",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/jest": "12.1.0",
|
||||
"@angular-devkit/build-angular": "~12.1.1",
|
||||
"@angular/cli": "~12.1.1",
|
||||
"@angular/compiler-cli": "~12.1.1",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/jest": "26.0.24",
|
||||
"@types/node": "^12.11.1",
|
||||
"jasmine-core": "~3.7.0",
|
||||
"jest": "27.0.6",
|
||||
"karma-coverage": "~2.0.3",
|
||||
"tailwindcss": "2.2.4",
|
||||
"ts-node": "^10.1.0",
|
||||
"typescript": "~4.3.2"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import ErrnoException = NodeJS.ErrnoException
|
||||
|
||||
const { writeFile } = require('fs')
|
||||
|
||||
// Configure Angular `environment.ts` file path
|
||||
const targetPath = './src/environments/environment.prod.ts'
|
||||
|
||||
// read environment variables from .env file
|
||||
require('dotenv').config()
|
||||
|
||||
// `environment.ts` file structure
|
||||
const envConfigFile = `export const environment = {
|
||||
production: true,
|
||||
supabaseUrl: '${process.env.ANGULAR_APP_SUPABASE_URL}',
|
||||
supabaseKey: '${process.env.ANGULAR_APP_SUPABASE_KEY}'
|
||||
};
|
||||
`
|
||||
|
||||
writeFile(targetPath, envConfigFile, function (err: ErrnoException | null) {
|
||||
if (err) {
|
||||
throw console.error(err)
|
||||
}
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
import 'jest-preset-angular'
|
||||
|
||||
/* global mocks for jsdom */
|
||||
const mock = () => {
|
||||
let storage: { [key: string]: string } = {}
|
||||
return {
|
||||
getItem: (key: string) => (key in storage ? storage[key] : null),
|
||||
setItem: (key: string, value: string) => (storage[key] = value || ''),
|
||||
removeItem: (key: string) => delete storage[key],
|
||||
clear: () => (storage = {}),
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'localStorage', { value: mock() })
|
||||
Object.defineProperty(window, 'sessionStorage', { value: mock() })
|
||||
Object.defineProperty(window, 'getComputedStyle', {
|
||||
value: () => ['-webkit-appearance'],
|
||||
})
|
||||
|
||||
Object.defineProperty(document.body.style, 'transform', {
|
||||
value: () => {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
/* output shorter and more meaningful Zone error stack traces */
|
||||
// Error.stackTraceLimit = 2;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { AuthComponent } from './components/auth/auth.component'
|
||||
import { HomeComponent } from './components/home/home.component'
|
||||
import { AuthGuard } from './guards/auth.guard'
|
||||
import { RecoveryPasswordGuard } from './guards/recovery-password.guard'
|
||||
import { RecoveryPasswordComponent } from './components/recovery-password/recovery-password.component'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomeComponent,
|
||||
canActivate: [AuthGuard, RecoveryPasswordGuard],
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
component: AuthComponent,
|
||||
},
|
||||
{
|
||||
path: 'recovery-password',
|
||||
component: RecoveryPasswordComponent,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
@@ -1 +0,0 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -1,18 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { AppComponent } from './app.component'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [RouterTestingModule],
|
||||
}).compileComponents()
|
||||
})
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent)
|
||||
const app = fixture.componentInstance
|
||||
expect(app).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
})
|
||||
export class AppComponent {}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
|
||||
import { AppComponent } from './app.component'
|
||||
import { ReactiveFormsModule } from '@angular/forms'
|
||||
import { AuthComponent } from './components/auth/auth.component'
|
||||
import { HomeComponent } from './components/home/home.component'
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { TodoItemComponent } from './components/todo-item/todo-item.component'
|
||||
import { RecoveryPasswordComponent } from './components/recovery-password/recovery-password.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
AuthComponent,
|
||||
HomeComponent,
|
||||
TodoItemComponent,
|
||||
RecoveryPasswordComponent,
|
||||
],
|
||||
imports: [BrowserModule, ReactiveFormsModule, AppRoutingModule],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -1,179 +0,0 @@
|
||||
<div class="min-w-full min-h-screen flex items-center justify-center bg-gray-200">
|
||||
<form
|
||||
[formGroup]="authForm"
|
||||
class="w-full h-full sm:h-auto sm:w-2/5 max-w-sm p-5 bg-white shadow flex flex-col text-base"
|
||||
>
|
||||
<span class="font-sans text-4xl text-center pb-2 mb-1 border-b mx-4 align-center"> Login </span>
|
||||
<label class="mt-3 mb-2 font-medium text-lg" for="email">
|
||||
<span class="font-mono mr-1 text-red-400">*</span>
|
||||
Email:
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
class="bg-gray-100 border py-1 px-3"
|
||||
type="email"
|
||||
name="email"
|
||||
formControlName="email"
|
||||
required
|
||||
/>
|
||||
<label class="mt-3 mb-2 font-medium text-lg" for="password">
|
||||
<span class="font-mono mr-1 text-red-400">*</span>
|
||||
Password:
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
class="bg-gray-100 border py-1 px-3"
|
||||
type="password"
|
||||
name="password"
|
||||
formControlName="password"
|
||||
required
|
||||
/>
|
||||
<span
|
||||
class="text-blue-600 mt-2 cursor-pointer self-end text-sm font-medium"
|
||||
(click)="forgotPassword()"
|
||||
>
|
||||
Forgot Password?
|
||||
</span>
|
||||
<div
|
||||
*ngIf="helperText?.text && helperText?.error"
|
||||
class="border px-1 py-2 my-2 text-center text-sm bg-red-100 border-red-300 text-red-400"
|
||||
>
|
||||
{{ helperText?.text }}
|
||||
</div>
|
||||
<div
|
||||
*ngIf="helperText?.text && !helperText?.error"
|
||||
class="border px-1 py-2 my-2 text-center text-sm bg-green-100 border-green-300 text-green-500"
|
||||
>
|
||||
{{ helperText?.text }}
|
||||
</div>
|
||||
<div class="mt-2 flex">
|
||||
<span class="block mx-1.5 w-full rounded-md shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
(click)="handleLogin('REGISTER')"
|
||||
class="
|
||||
border
|
||||
w-full
|
||||
border-blue-600
|
||||
text-blue-600
|
||||
flex
|
||||
justify-center
|
||||
py-2
|
||||
px-4
|
||||
text-sm
|
||||
font-medium
|
||||
rounded-md
|
||||
hover:bg-blue-200
|
||||
focus:outline-none focus:border-blue-700 focus:shadow-outline-blue
|
||||
active:bg-blue-700
|
||||
transition
|
||||
duration-150
|
||||
ease-in-out
|
||||
"
|
||||
>
|
||||
Sign Up
|
||||
</button>
|
||||
</span>
|
||||
<span class="block w-full mx-1.5 rounded-md shadow-sm">
|
||||
<button
|
||||
(click)="handleLogin('LOGIN')"
|
||||
type="button"
|
||||
class="
|
||||
flex
|
||||
w-full
|
||||
justify-center
|
||||
py-2
|
||||
px-4
|
||||
border border-transparent
|
||||
text-sm
|
||||
font-medium
|
||||
rounded-md
|
||||
text-white
|
||||
bg-blue-600
|
||||
hover:bg-blue-500
|
||||
focus:outline-none focus:border-blue-700 focus:shadow-outline-blue
|
||||
active:bg-blue-700
|
||||
transition
|
||||
duration-150
|
||||
ease-in-out
|
||||
"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full mx-1.5 border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm leading-5">
|
||||
<span class="px-2 bg-white text-gray-500"> Or continue with </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mt-3">
|
||||
<span class="block rounded-md shadow-sm">
|
||||
<button
|
||||
(click)="handleOAuthLogin('github')"
|
||||
type="button"
|
||||
class="
|
||||
w-3/4
|
||||
mx-auto
|
||||
flex
|
||||
justify-center
|
||||
py-2
|
||||
px-4
|
||||
border border-transparent
|
||||
text-sm
|
||||
font-medium
|
||||
rounded-md
|
||||
text-white
|
||||
bg-blue-600
|
||||
hover:bg-blue-500
|
||||
focus:outline-none focus:border-blue-700 focus:shadow-outline-blue
|
||||
active:bg-blue-700
|
||||
transition
|
||||
duration-150
|
||||
ease-in-out
|
||||
"
|
||||
>
|
||||
GitHub
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<span class="block rounded-md shadow-sm">
|
||||
<button
|
||||
(click)="handleOAuthLogin('google')"
|
||||
type="button"
|
||||
class="
|
||||
w-3/4
|
||||
mx-auto
|
||||
flex
|
||||
justify-center
|
||||
py-2
|
||||
px-4
|
||||
border border-transparent
|
||||
text-sm
|
||||
font-medium
|
||||
rounded-md
|
||||
text-white
|
||||
bg-blue-600
|
||||
hover:bg-blue-500
|
||||
focus:outline-none focus:border-blue-700 focus:shadow-outline-blue
|
||||
active:bg-blue-700
|
||||
transition
|
||||
duration-150
|
||||
ease-in-out
|
||||
"
|
||||
>
|
||||
Google
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { AuthComponent } from './auth.component'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { ReactiveFormsModule } from '@angular/forms'
|
||||
|
||||
describe('AuthComponent', () => {
|
||||
let component: AuthComponent
|
||||
let fixture: ComponentFixture<AuthComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AuthComponent],
|
||||
imports: [RouterTestingModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AuthComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms'
|
||||
import { SupabaseService } from '../../services/supabase.service'
|
||||
import { Provider } from '@supabase/supabase-js'
|
||||
import { Router } from '@angular/router'
|
||||
|
||||
interface HelperText {
|
||||
text: string
|
||||
error: boolean
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth',
|
||||
templateUrl: './auth.component.html',
|
||||
})
|
||||
export class AuthComponent {
|
||||
authForm: FormGroup = new FormGroup({
|
||||
email: new FormControl('', Validators.required),
|
||||
password: new FormControl('', Validators.required),
|
||||
})
|
||||
|
||||
helperText: HelperText | undefined
|
||||
|
||||
constructor(private readonly supabase: SupabaseService, private readonly router: Router) {}
|
||||
|
||||
async forgotPassword(): Promise<void> {
|
||||
const email = prompt('Please enter your email:')
|
||||
|
||||
let { error } = await this.supabase.resetPassword(email as string)
|
||||
if (error) {
|
||||
console.error('Error: ', error.message)
|
||||
} else {
|
||||
this.helperText = {
|
||||
error: false,
|
||||
text: 'Password recovery email has been sent.',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleLogin(type: string): Promise<void> {
|
||||
const { email, password } = this.authForm.value
|
||||
|
||||
const { user, error, session } =
|
||||
type === 'LOGIN'
|
||||
? await this.supabase.signIn(email, password)
|
||||
: await this.supabase.signUp(email, password)
|
||||
|
||||
if (user && type === 'LOGIN') {
|
||||
await this.router.navigate(['/'])
|
||||
} else if (error) {
|
||||
this.helperText = { error: true, text: error.message }
|
||||
} else if (user && !session && !error) {
|
||||
this.helperText = {
|
||||
error: false,
|
||||
text: 'An email has been sent to you for verification!',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleOAuthLogin(provider: Provider): Promise<void> {
|
||||
// You need to enable the third party auth you want in Authentication > Settings
|
||||
// Read more on: https://supabase.com/docs/guides/auth#third-party-logins
|
||||
let { error } = await this.supabase.signInWithProvider(provider)
|
||||
if (error) console.error('Error: ', error.message)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user