Merge pull request #8868 from supabase/chore/remove-old-examples
No files matched your search
@@ -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"></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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
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=
|
||||
@@ -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: {},
|
||||
},
|
||||
}
|
||||
|
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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -1,2 +0,0 @@
|
||||
SUPABASE_SERVICE_KEY="{SERVICE_KEY}"
|
||||
SUPABASE_URL="https://{YOUR_INSTANCE_NAME}.supabase.co"
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
|
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,
|
||||
}
|
||||
@@ -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,66 +0,0 @@
|
||||
# Supabase Edge Function Examples
|
||||
|
||||
## What are Supabase Edge Functions?
|
||||
|
||||
[Supabase Edge Functions](https://supabase.com/edge-functions) are written in TypeScript, run via Deno, and deployed with the Supabase CLI. Please [download](https://github.com/supabase/cli#install-the-cli) the latest version of the Supabase CLI, or [upgrade](https://github.com/supabase/cli#install-the-cli) it if you have it already installed.
|
||||
|
||||
## Example Functions
|
||||
|
||||
The function examples are located in [`./supabase/functions`](./supabase/functions):
|
||||
|
||||
- [`browser-with-cors`](./supabase/functions/browser-with-cors/index.ts): Handle CORS headers for function invocations from browser environments.
|
||||
- [`select-from-table-with-auth-rls`](./supabase/functions/select-from-table-with-auth-rls/index.ts): Retrieve data from an authenticated user via RLS.
|
||||
- [`send-email-smtp`](./supabase/functions/send-email-smtp/index.ts): Send an email using SMTP credentials.
|
||||
- [`stripe-webhooks`](./supabase/functions/stripe-webhooks/index.ts): Handle Stripe Webhooks.
|
||||
- [`telegram-bot`](./supabase/functions/telegram-bot/index.ts): Webhook handler for Telegram bots using [grammY](https://grammy.dev/).
|
||||
|
||||
## Develop locally
|
||||
|
||||
- Run `supabase start` (make sure your Docker daemon is running.)
|
||||
- Run `supabase functions serve your-function-name`
|
||||
- Run the CURL command in the example function, or use the [invoke method](https://supabase.com/docs/reference/javascript/invoke) on the Supabase client or use the test client [app](./app/).
|
||||
|
||||
## Test
|
||||
|
||||
This example includes a create-react-app in the [`./app/`](./app/) directory which you can use as a sort of postman to make test requests both locally and to your deployed functions.
|
||||
|
||||
### Test locally
|
||||
|
||||
- `cd app`
|
||||
- `npm install`
|
||||
- `npm start`
|
||||
|
||||
Note: when testing locally, the select dropdown doesn't have any effect, and invoke simply calls whatever function is currently served by the CLI.
|
||||
|
||||
## Deploy
|
||||
|
||||
- Generate access token and log in to CLI
|
||||
- Navigate to https://app.supabase.com/account/tokens
|
||||
- Click "Generate New Token"
|
||||
- Copy newly created token
|
||||
- Run `supabase login`
|
||||
- Input your token when prompted
|
||||
- Link your project
|
||||
- Within your project root run `supabase link --project-ref your-project-ref`
|
||||
- Set up your secrets
|
||||
- Run `supabase secrets set --from-stdin < .env` to set the env vars from your `.env` file.
|
||||
- You can run `supabase secrets list` to check that it worked and also to see what other env vars are set by default.
|
||||
- Deploy the function
|
||||
- Within your project root run `supabase functions deploy payment-sheet`
|
||||
- In youre [`./app/.env`](./app/.env) file remove the `SUPA_FUNCTION_LOCALHOST` variable and restart your Expo app.
|
||||
|
||||
### Test deployed functions
|
||||
|
||||
This example includes a create-react-app in the [`./app/`](./app/) directory which you can use as a sort of postman to make test requests both locally and to your deployed functions.
|
||||
|
||||
- `cd app`
|
||||
- `cp .env.example .env`
|
||||
- Fill in your env vars from https://app.supabase.com/project/_/settings/api
|
||||
- `npm install`
|
||||
- `npm start`
|
||||
|
||||
## 👁⚡️👁
|
||||
|
||||
\o/ That's it, you can now invoke your Supabase Function via the [`supabase-js`](https://supabase.com/docs/reference/javascript/invoke) and [`supabase-dart`](https://supabase.com/docs/reference/dart/invoke) client libraries. (More client libraries coming soon. Check the [supabase-community](https://github.com/supabase-community#client-libraries) org for details).
|
||||
|
||||
For more info on Supabase Functions, check out the [docs](https://supabase.com/docs/guides/functions) and the [examples](https://github.com/supabase/supabase/tree/master/examples/edge-functions).
|
||||
@@ -1,3 +0,0 @@
|
||||
# Get these from your Dashboard: https://app.supabase.com/project/_/settings/api
|
||||
REACT_APP_SUPABASE_URL=
|
||||
REACT_APP_SUPABASE_ANON_KEY=
|
||||
@@ -1,24 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*lock.json
|
||||
@@ -1,70 +0,0 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^1.33.3",
|
||||
"@supabase/ui": "^0.36.4",
|
||||
"@testing-library/jest-dom": "^5.16.3",
|
||||
"@testing-library/react": "^12.1.4",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-json-editor-ajrm": "^2.5.13",
|
||||
"react-scripts": "5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.4",
|
||||
"postcss": "^8.4.12",
|
||||
"tailwindcss": "^3.0.23"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,40 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using create-react-app" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Supabase Edge Functions Test Client</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Auth, Button, Typography, Select } from '@supabase/ui'
|
||||
import JSONInput from 'react-json-editor-ajrm'
|
||||
import locale from 'react-json-editor-ajrm/locale/en'
|
||||
import { supabase } from './utils/supabaseClient'
|
||||
import { functionsList } from './functionsList'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const sampleObject = { name: 'world' }
|
||||
|
||||
function App() {
|
||||
const { user } = Auth.useUser()
|
||||
const [supaFunction, setSupaFunction] = useState(functionsList[0])
|
||||
const [requestJson, setRequestJson] = useState(sampleObject)
|
||||
const [responseJson, setResponseJson] = useState({})
|
||||
|
||||
const invokeFunction = async () => {
|
||||
setResponseJson({ loading: true })
|
||||
const { data, error } = await supabase.functions.invoke(supaFunction, {
|
||||
body: JSON.stringify(requestJson),
|
||||
})
|
||||
if (error) alert(error)
|
||||
setResponseJson(data)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<Title level={2} className="mb-2">
|
||||
Supabase Egde Functions Test Client
|
||||
</Title>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2">
|
||||
<div className="p-2">
|
||||
<Title level={3} className="mb-2">
|
||||
Request
|
||||
</Title>
|
||||
<Title level={4}>Function</Title>
|
||||
<Select onChange={(e) => setSupaFunction(e.target.value)}>
|
||||
{functionsList.map((func) => (
|
||||
<Select.Option value={func} key={func}>
|
||||
{func}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Text className="mb-2">
|
||||
Note: when using locally, this selection doesn't have any effect and the function that's
|
||||
currently being served via the CLI is called instead.
|
||||
</Text>
|
||||
<Title level={4} className="mb-2">
|
||||
Body
|
||||
</Title>
|
||||
<JSONInput
|
||||
onChange={({ jsObject }) => setRequestJson(jsObject)}
|
||||
placeholder={sampleObject}
|
||||
locale={locale}
|
||||
height="100"
|
||||
width="100%"
|
||||
/>
|
||||
<Button className="mt-2" onClick={invokeFunction}>
|
||||
Invoke Function
|
||||
</Button>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<Title level={3} className="mb-2">
|
||||
Response
|
||||
</Title>
|
||||
<pre className="p-2 bg-gray-300 ">{JSON.stringify(responseJson, null, 2)}</pre>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<Title level={3} className="mb-2">
|
||||
Log in to see RLS in action
|
||||
</Title>
|
||||
{user ? (
|
||||
<div>
|
||||
<Title level={4} className="mb-2">{`Logged in as ${user.email}`}</Title>
|
||||
<Button onClick={() => supabase.auth.signOut()}>Sign out</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Auth supabaseClient={supabase} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -1,6 +0,0 @@
|
||||
export const functionsList = [
|
||||
'local: Whatever function is currently served by the CLI',
|
||||
'browser-with-cors',
|
||||
'select-from-table-with-auth-rls',
|
||||
'send-email-smtp',
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
import { Auth } from '@supabase/ui'
|
||||
import { supabase } from './utils/supabaseClient'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Auth.UserContextProvider supabaseClient={supabase}>
|
||||
<App />
|
||||
</Auth.UserContextProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
@@ -1,7 +0,0 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
export const supabase = createClient(
|
||||
process.env.REACT_APP_SUPABASE_URL ?? 'http://localhost:54321',
|
||||
process.env.REACT_APP_SUPABASE_ANON_KEY ??
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs'
|
||||
)
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "project-root",
|
||||
"path": "./"
|
||||
},
|
||||
{
|
||||
"name": "test-client",
|
||||
"path": "app"
|
||||
},
|
||||
{
|
||||
"name": "supabase-functions",
|
||||
"path": "supabase/functions"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"files.exclude": {
|
||||
"node_modules/": true,
|
||||
"app/": true,
|
||||
"supabase/functions/": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
# send-email-smtp
|
||||
SMTP_HOSTNAME="your.hostname.com"
|
||||
SMTP_PORT="2587"
|
||||
SMTP_USERNAME="your_username"
|
||||
SMTP_PASSWORD="your_password"
|
||||
SMTP_FROM="no-reply@example.com"
|
||||
|
||||
# telegram-bot
|
||||
TELEGRAM_BOT_TOKEN="get it from https://t.me/BotFather"
|
||||
FUNCTION_SECRET="random secret"
|
||||
|
||||
# stripe-webhooks
|
||||
STRIPE_API_KEY="<YOUR API KEY HERE>"
|
||||
STRIPE_WEBHOOK_SIGNING_SECRET="<YOUR WEBHOOK SIGNING HERE>"
|
||||
@@ -1,60 +0,0 @@
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the working
|
||||
# directory name when running `supabase init`.
|
||||
project_id = "edge-functions"
|
||||
|
||||
[api]
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. public and storage are always included.
|
||||
schemas = []
|
||||
# Extra schemas to add to the search_path of every request.
|
||||
extra_search_path = ["extensions"]
|
||||
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||
# for accidental or malicious requests.
|
||||
max_rows = 1000
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||
# server_version;` on the remote database to check.
|
||||
major_version = 14
|
||||
|
||||
[studio]
|
||||
# Port to use for Supabase Studio.
|
||||
port = 54323
|
||||
|
||||
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||
[inbucket]
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
|
||||
[auth]
|
||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||
# in emails.
|
||||
site_url = "http://localhost:3000"
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = ["https://localhost:3000"]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one
|
||||
# week).
|
||||
jwt_expiry = 3600
|
||||
# Allow/disallow new user signups to your project.
|
||||
enable_signup = true
|
||||
|
||||
[auth.email]
|
||||
# Allow/disallow new user signups via email to your project.
|
||||
enable_signup = true
|
||||
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||
# addresses. If disabled, only the new email is required to confirm.
|
||||
double_confirm_changes = true
|
||||
# If enabled, users need to confirm their email address before signing in.
|
||||
enable_confirmations = false
|
||||
|
||||
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||
# `discord`, `facebook`, `github`, `gitlab`, `google`, `twitch`, `twitter`, `slack`, `spotify`.
|
||||
[auth.external.apple]
|
||||
enabled = false
|
||||
client_id = ""
|
||||
secret = ""
|
||||
@@ -1 +0,0 @@
|
||||
!.vscode
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"deno.enable": true,
|
||||
"deno.lint": true,
|
||||
"deno.unstable": true
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey',
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@^1.33.2'
|
||||
|
||||
export const supabaseClient = createClient(
|
||||
// Supabase API URL - env var exported by default when deployed.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API ANON KEY - env var exported by default when deployed.
|
||||
Deno.env.get('SUPABASE_ANON_KEY') ?? ''
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// 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 { corsHeaders } from '../_shared/cors.ts'
|
||||
|
||||
console.log(`Function "browser-with-cors" up and running!`)
|
||||
|
||||
serve(async (req) => {
|
||||
// This is needed if you're planning to invoke your function from a browser.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
try {
|
||||
const { name } = await req.json()
|
||||
const data = {
|
||||
message: `Hello ${name}!`,
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// To invoke:
|
||||
// curl -i --location --request POST 'http://localhost:54321/functions/v1/browser-with-cors' \
|
||||
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
|
||||
// --header 'Content-Type: application/json' \
|
||||
// --data '{"name":"Functions"}'
|
||||
@@ -1,41 +0,0 @@
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// 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 { supabaseClient } from '../_shared/supabaseClient.ts'
|
||||
import { corsHeaders } from '../_shared/cors.ts'
|
||||
|
||||
console.log(`Function "select-from-table-with-auth-rls" up and running!`)
|
||||
|
||||
serve(async (req: Request) => {
|
||||
// This is needed if you're planning to invoke your function from a browser.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
try {
|
||||
// Set the Auth context of the user that called the function.
|
||||
// This way your row-level-security (RLS) policies are applied.
|
||||
supabaseClient.auth.setAuth(req.headers.get('Authorization')!.replace('Bearer ', ''))
|
||||
|
||||
const { data, error } = await supabaseClient.from('users').select('*')
|
||||
console.log({ data, error })
|
||||
|
||||
return new Response(JSON.stringify({ data, error }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// To invoke:
|
||||
// curl -i --location --request POST 'http://localhost:54321/functions/v1/select-from-table-with-auth-rls' \
|
||||
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
|
||||
// --header 'Content-Type: application/json' \
|
||||
// --data '{"name":"Functions"}'
|
||||
@@ -1,16 +0,0 @@
|
||||
# send-email-smtp
|
||||
|
||||
## Deploy
|
||||
|
||||
```
|
||||
supabase link --project-ref your-project-ref
|
||||
supabase secrets set SMTP_HOSTNAME="your.hostname.com" SMTP_PORT="2587" SMTP_USERNAME="your_username" SMTP_PASSWORD="your_password" SMTP_FROM="no-reply@example.com"
|
||||
supabase functions deploy send-email-smtp
|
||||
```
|
||||
|
||||
Note: `SMTP_PORT` must be a port other than `25`, `465`, and `587` as Deno Deploy does not support outgoing connections to ports. AWS SES (port 2587) is recommended.
|
||||
|
||||
## Test locally
|
||||
|
||||
- `cp ./supabase/.env.local.example ./supabase/.env.local`
|
||||
- `supabase functions serve send-email-smtp --env-file ./supabase/.env.local`
|
||||
@@ -1,47 +0,0 @@
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// 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 { SmtpClient } from 'https://deno.land/x/denomailer@0.12.0/mod.ts'
|
||||
|
||||
const smtp = new SmtpClient()
|
||||
|
||||
console.log(`Function "send-email-smtp" up and running!`)
|
||||
|
||||
serve(async (req) => {
|
||||
await smtp.connect({
|
||||
hostname: Deno.env.get('SMTP_HOSTNAME')!,
|
||||
port: Number(Deno.env.get('SMTP_PORT')!),
|
||||
username: Deno.env.get('SMTP_USERNAME')!,
|
||||
password: Deno.env.get('SMTP_PASSWORD')!,
|
||||
})
|
||||
|
||||
try {
|
||||
await smtp.send({
|
||||
from: Deno.env.get('SMTP_FROM')!,
|
||||
to: 'testr@test.de',
|
||||
subject: `Hello from Supabase Edge Functions`,
|
||||
content: `Hello Functions \o/`,
|
||||
})
|
||||
} catch (error: any) {
|
||||
return new Response(error.message, { status: 500 })
|
||||
}
|
||||
|
||||
await smtp.close()
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
done: true,
|
||||
}),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// To invoke:
|
||||
// curl -i --location --request POST 'http://localhost:54321/functions/v1/' \
|
||||
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
|
||||
// --header 'Content-Type: application/json' \
|
||||
// --data '{"name":"Functions"}'
|
||||
@@ -1,21 +0,0 @@
|
||||
# stripe-webhooks
|
||||
|
||||
Also check out our full Stripe Payments examples for [React Native (Expo)](https://github.com/supabase-community/expo-stripe-payments-with-supabase-functions) and [Flutter](https://github.com/supabase-community/flutter-stripe-payments-with-supabase-functions).
|
||||
|
||||
## Setup env vars
|
||||
|
||||
- `cp supabase/.env.local.example supabase/.env.local`
|
||||
|
||||
## Test locally
|
||||
|
||||
- Terminal 1:
|
||||
- `supabase functions serve --no-verify-jwt stripe-webhooks --env-file ./supabase/.env.local`
|
||||
- Terminal 2:
|
||||
- `stripe listen --forward-to localhost:54321/functions/v1/`
|
||||
- Terminal 3 (optional):
|
||||
- `stripe trigger payment_intent.succeeded`
|
||||
|
||||
## Deploy
|
||||
|
||||
- `supabase functions deploy --no-verify-jwt stripe-webhooks`
|
||||
- `supabase secrets set --env-file ./supabase/.env.local`
|
||||
@@ -1,60 +0,0 @@
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// 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'
|
||||
|
||||
// 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'
|
||||
|
||||
const stripe = Stripe(Deno.env.get('STRIPE_API_KEY'), {
|
||||
// This is needed to use the Fetch API rather than relying on the Node http
|
||||
// package.
|
||||
httpClient: Stripe.createFetchHttpClient(),
|
||||
})
|
||||
// This is needed in order to use the Web Crypto API in Deno.
|
||||
const cryptoProvider = Stripe.createSubtleCryptoProvider()
|
||||
|
||||
console.log(`Function "stripe-webhooks" up and running!`)
|
||||
|
||||
serve(async (request) => {
|
||||
const signature = request.headers.get('Stripe-Signature')
|
||||
|
||||
// First step is to verify the event. The .text() method must be used as the
|
||||
// verification relies on the raw request body rather than the parsed JSON.
|
||||
const body = await request.text()
|
||||
let receivedEvent
|
||||
try {
|
||||
receivedEvent = await stripe.webhooks.constructEventAsync(
|
||||
body,
|
||||
signature,
|
||||
Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET'),
|
||||
undefined,
|
||||
cryptoProvider
|
||||
)
|
||||
} catch (err) {
|
||||
return new Response(err.message, { status: 400 })
|
||||
}
|
||||
console.log(`🔔 Event received: ${receivedEvent.id}`)
|
||||
|
||||
// Secondly, we use this event to query the Stripe API in order to avoid
|
||||
// handling any forged event. If available, we use the idempotency key.
|
||||
const requestOptions =
|
||||
receivedEvent.request && receivedEvent.request.idempotency_key
|
||||
? {
|
||||
idempotencyKey: receivedEvent.request.idempotency_key,
|
||||
}
|
||||
: {}
|
||||
|
||||
let retrievedEvent
|
||||
try {
|
||||
retrievedEvent = await stripe.events.retrieve(receivedEvent.id, requestOptions)
|
||||
} catch (err) {
|
||||
return new Response(err.message, { status: 400 })
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ id: retrievedEvent.id, status: 'ok' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
# telegram-bot
|
||||
|
||||
Try it out: https://t.me/supabase_example_bot
|
||||
|
||||

|
||||
|
||||
## Deploy
|
||||
|
||||
1. Run `supabase functions deploy --no-verify-jwt telegram-bot`
|
||||
2. Get your Telegram token from https://t.me/BotFather
|
||||
3. Run `supabase secrets set TELEGRAM_BOT_TOKEN=your_token FUNCTION_SECRET=random_secret`
|
||||
4. Set your bot's webhook url to `https://<PROJECT_NAME>.functions.supabase.co/telegram-bot` (Replacing `<...>` with respective values). In order to do that, run this url (in your browser, for example): `https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook?url=https://<PROJECT_NAME>.functions.supabase.co/telegram-bot?secret=<FUNCTION_SECRET>`
|
||||
5. That's it, go ahead and chat with your bot 🤖💬
|
||||
|
Before Width: | Height: | Size: 9.9 MiB |
@@ -1,35 +0,0 @@
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// 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"
|
||||
|
||||
console.log(`Function "telegram-bot" up and running!`)
|
||||
|
||||
import { Bot, webhookCallback } from "https://deno.land/x/grammy@v1.8.3/mod.ts";
|
||||
|
||||
const bot = new Bot(Deno.env.get('TELEGRAM_BOT_TOKEN') || '');
|
||||
|
||||
bot.command("start", (ctx) => ctx.reply("Welcome! Up and running."));
|
||||
|
||||
bot.command('ping', (ctx) => ctx.reply(`Pong! ${new Date()} ${Date.now()}`))
|
||||
|
||||
const handleUpdate = webhookCallback(bot, "std/http");
|
||||
|
||||
serve(async (req) => {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET'))
|
||||
return new Response('not allowed', { status: 405 })
|
||||
|
||||
return await handleUpdate(req);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})
|
||||
|
||||
// To invoke:
|
||||
// curl -i --location --request POST 'http://localhost:54321/functions/v1/' \
|
||||
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
|
||||
// --header 'Content-Type: application/json' \
|
||||
// --data '{"name":"Functions"}'
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* USERS
|
||||
* Note: This table contains user data. Users should only be able to view and update their own data.
|
||||
*/
|
||||
create table users (
|
||||
-- UUID from auth.users
|
||||
id uuid references auth.users not null primary key,
|
||||
full_name text,
|
||||
avatar_url text
|
||||
);
|
||||
alter table users enable row level security;
|
||||
create policy "Can view own user data." on users for select using (auth.uid() = id);
|
||||
create policy "Can update own user data." on users for update using (auth.uid() = id);
|
||||
|
||||
/**
|
||||
* This trigger automatically creates a user entry when a new user signs up via Supabase Auth.
|
||||
*/
|
||||
create function public.handle_new_user()
|
||||
returns trigger as $$
|
||||
begin
|
||||
insert into public.users (id, full_name, avatar_url)
|
||||
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute procedure public.handle_new_user();
|
||||
@@ -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
|
||||