Merge pull request #5863 from supabase/master

studio prod deploy
This commit is contained in:
Joshen Lim authored and GitHub committed 2022-03-09 14:20:00 +08:00
commit 8f7ea55de4
14 files changed
+20567 -310

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# 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 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
+5910 -4
View File
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
import { FC, useState } from 'react'
import { observer } from 'mobx-react-lite'
import { Badge, Typography, Toggle } from '@supabase/ui'
import { Badge, Toggle } from '@supabase/ui'
import { useStore } from 'hooks'
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
@@ -82,19 +82,23 @@ const ExtensionCard: FC<Props> = ({ extension }) => {
return (
<div
className="
border border-border-secondary-light dark:border-border-secondary-dark
border border-panel-border-light dark:border-panel-border-dark
shadow-sm
overflow-hidden
rounded
flex flex-col"
flex flex-col
"
>
<div
className="
p-4 px-6 flex
bg-panel-header-light dark:bg-panel-header-dark
border-b border-border-secondary-light dark:border-border-secondary-dark"
border-b border-panel-border-light dark:border-panel-border-dark
"
>
<div className="m-0 h-5 uppercase flex-1">
<Typography.Title level={5}>{extension.name}</Typography.Title>
</div>
<h3 className="m-0 h-5 uppercase flex-1 truncate text-base text-scale-1200 truncate">
{extension.name}
</h3>
{loading ? (
<img className="loading-spinner" src="/img/spinner.gif"></img>
) : (
@@ -44,18 +44,25 @@ const PolicyRow = ({
onSelectDeletePolicy = () => {},
}) => {
return (
<div className="group">
<Panel.Content className="flex gap-2 justify-between py-4 border-b border-panel-border-light dark:border-panel-border-dark">
<div className="flex flex-col lg:flex-row lg:items-center gap-3">
<div className="font-mono text-xs text-scale-900">{policy.command}</div>
<div className="flex flex-col lg:flex-row gap-2">
<span className="text-sm text-scale-1200 truncate">{policy.name}</span>
<span className="text-sm text-scale-1100 truncate">
{policy.definition || policy.check}
</span>
</div>
</div>
<Panel.Content
className="
w-full
flex gap-2
lg:items-center
py-4
border-b
border-panel-border-light
dark:border-panel-border-dark
"
>
<div className="font-mono text-xs text-scale-900">{policy.command}</div>
<div className="flex flex-col lg:flex-row gap-2 truncate grow">
<span className="text-sm text-scale-1200 truncate max-w-xs">{policy.name}</span>
<span className="text-sm text-scale-1100 truncate">
{policy.definition || policy.check}
</span>
</div>
<div className="">
<Dropdown
side="bottom"
align="end"
@@ -65,7 +72,6 @@ const PolicyRow = ({
<Dropdown.Item
icon={<IconEdit size={14} />}
type="outline"
className="mx-2"
onClick={() => onSelectEditPolicy(policy)}
>
Edit
@@ -87,8 +93,8 @@ const PolicyRow = ({
icon={<IconMoreVertical />}
/>
</Dropdown>
</Panel.Content>
</div>
</div>
</Panel.Content>
)
}
+14299 -100
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -17,12 +17,12 @@
"@radix-ui/react-tooltip": "^0.1.6",
"@sentry/browser": "^6.15.0",
"@sentry/nextjs": "^6.15.0",
"@supabase/grid": "v2.5.0-alpha.8",
"@supabase/grid": "v2.5.0-alpha.12",
"@supabase/postgres-meta": "^0.26.1",
"@supabase/react-data-grid": "^7.1.0-beta.7",
"@supabase/shared-types": "^0.1.15",
"@supabase/supabase-js": "^1.29.1",
"@supabase/ui": "0.37.0-alpha.47",
"@supabase/ui": "0.37.0-alpha.50",
"ajv": "^8.6.3",
"analytics-node": "^3.5.0",
"clipboard": "^2.0.8",
+38 -33
View File
@@ -44,40 +44,55 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
SMTP_USER: null,
SMTP_PASS: null,
SMTP_MAX_FREQUENCY: 0,
EXTERNAL_EMAIL_ENABLED: true,
EXTERNAL_PHONE_ENABLED: true,
EXTERNAL_APPLE_ENABLED: false,
EXTERNAL_APPLE_CLIENT_ID: null,
EXTERNAL_APPLE_SECRET: null,
EXTERNAL_AZURE_ENABLED: false,
EXTERNAL_AZURE_CLIENT_ID: null,
EXTERNAL_AZURE_SECRET: null,
EXTERNAL_AZURE_URL: null,
EXTERNAL_BITBUCKET_ENABLED: false,
EXTERNAL_BITBUCKET_CLIENT_ID: null,
EXTERNAL_BITBUCKET_SECRET: null,
EXTERNAL_DISCORD_ENABLED: false,
EXTERNAL_DISCORD_CLIENT_ID: null,
EXTERNAL_DISCORD_SECRET: null,
EXTERNAL_FACEBOOK_ENABLED: false,
EXTERNAL_FACEBOOK_CLIENT_ID: null,
EXTERNAL_FACEBOOK_SECRET: null,
EXTERNAL_GITHUB_ENABLED: false,
EXTERNAL_GITHUB_CLIENT_ID: null,
EXTERNAL_GITHUB_SECRET: null,
EXTERNAL_GITLAB_ENABLED: false,
EXTERNAL_GITLAB_CLIENT_ID: null,
EXTERNAL_GITLAB_SECRET: null,
EXTERNAL_GITLAB_REDIRECT_URI: null,
EXTERNAL_GOOGLE_ENABLED: false,
EXTERNAL_GOOGLE_CLIENT_ID: null,
EXTERNAL_GOOGLE_SECRET: null,
EXTERNAL_LINKEDIN_ENABLED: false,
EXTERNAL_LINKEDIN_CLIENT_ID: null,
EXTERNAL_LINKEDIN_SECRET: null,
EXTERNAL_NOTION_ENABLED: false,
EXTERNAL_NOTION_CLIENT_ID: null,
EXTERNAL_NOTION_SECRET: null,
EXTERNAL_BITBUCKET_ENABLED: false,
EXTERNAL_BITBUCKET_CLIENT_ID: null,
EXTERNAL_BITBUCKET_SECRET: null,
EXTERNAL_GOOGLE_ENABLED: false,
EXTERNAL_GOOGLE_CLIENT_ID: null,
EXTERNAL_GOOGLE_SECRET: null,
EXTERNAL_GITLAB_ENABLED: false,
EXTERNAL_GITLAB_CLIENT_ID: null,
EXTERNAL_GITLAB_SECRET: null,
EXTERNAL_GITLAB_REDIRECT_URI: null,
EXTERNAL_AZURE_ENABLED: false,
EXTERNAL_AZURE_CLIENT_ID: null,
EXTERNAL_AZURE_SECRET: null,
EXTERNAL_FACEBOOK_ENABLED: false,
EXTERNAL_FACEBOOK_CLIENT_ID: null,
EXTERNAL_FACEBOOK_SECRET: null,
EXTERNAL_SPOTIFY_ENABLED: false,
EXTERNAL_SPOTIFY_CLIENT_ID: null,
EXTERNAL_SPOTIFY_SECRET: null,
EXTERNAL_SLACK_ENABLED: false,
EXTERNAL_SLACK_CLIENT_ID: null,
EXTERNAL_SLACK_SECRET: null,
EXTERNAL_TWITTER_ENABLED: false,
EXTERNAL_TWITTER_CLIENT_ID: null,
EXTERNAL_TWITTER_SECRET: null,
EXTERNAL_APPLE_ENABLED: false,
EXTERNAL_APPLE_CLIENT_ID: null,
EXTERNAL_APPLE_SECRET: null,
EXTERNAL_DISCORD_ENABLED: false,
EXTERNAL_DISCORD_CLIENT_ID: null,
EXTERNAL_DISCORD_SECRET: null,
EXTERNAL_TWITCH_ENABLED: false,
EXTERNAL_TWITCH_CLIENT_ID: null,
EXTERNAL_TWITCH_SECRET: null,
EXTERNAL_ZOOM_ENABLED: false,
EXTERNAL_ZOOM_CLIENT_ID: null,
EXTERNAL_ZOOM_SECRET: null,
MAILER_SUBJECTS_INVITE: 'You have been invited',
MAILER_SUBJECTS_CONFIRMATION: 'Confirm Your Signup',
MAILER_SUBJECTS_RECOVERY: 'Reset Your Password',
@@ -98,12 +113,8 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
MAILER_TEMPLATES_MAGIC_LINK: null,
MAILER_TEMPLATES_MAGIC_LINK_CONTENT:
'<h2>Magic Link</h2>\n\n<p>Follow this link to login:</p>\n<p><a href="{{ .ConfirmationURL }}">Log In</a></p>',
EXTERNAL_TWITCH_ENABLED: false,
EXTERNAL_TWITCH_CLIENT_ID: null,
EXTERNAL_TWITCH_SECRET: null,
PASSWORD_MIN_LENGTH: 6,
SMTP_SENDER_NAME: null,
EXTERNAL_EMAIL_ENABLED: true,
EXTERNAL_PHONE_ENABLED: true,
SMS_AUTOCONFIRM: false,
SMS_MAX_FREQUENCY: 0,
SMS_OTP_EXP: 0,
@@ -118,12 +129,6 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
SECURITY_CAPTCHA_SECRET: null,
RATE_LIMIT_EMAIL_SENT: 0,
MAILER_SECURE_EMAIL_CHANGE_ENABLED: true,
EXTERNAL_SPOTIFY_ENABLED: false,
EXTERNAL_SPOTIFY_CLIENT_ID: null,
EXTERNAL_SPOTIFY_SECRET: null,
EXTERNAL_SLACK_ENABLED: false,
EXTERNAL_SLACK_CLIENT_ID: null,
EXTERNAL_SLACK_SECRET: null,
SMS_MESSAGEBIRD_ACCESS_KEY: null,
SMS_MESSAGEBIRD_ORIGINATOR: null,
SMS_VONAGE_API_KEY: null,
@@ -120,12 +120,14 @@ const Settings = () => {
'URI_ALLOW_LIST',
'JWT_EXP',
'DISABLE_SIGNUP',
'PASSWORD_MIN_LENGTH',
])}
model={{
SITE_URL: model.SITE_URL || undefined,
URI_ALLOW_LIST: model.URI_ALLOW_LIST || undefined,
DISABLE_SIGNUP: model.DISABLE_SIGNUP,
JWT_EXP: model.JWT_EXP || undefined,
PASSWORD_MIN_LENGTH: model.PASSWORD_MIN_LENGTH || undefined,
}}
onSubmit={(model: any) => onFormSubmit(model)}
>
@@ -140,6 +142,7 @@ const Settings = () => {
errorMessage="Must be a comma separated list of exact URIs. No spaces."
/>
<NumField name="JWT_EXP" step="1" />
<NumField name="PASSWORD_MIN_LENGTH" step="1" />
<ToggleField name="DISABLE_SIGNUP" />
</SchemaFormPanel>
</div>
@@ -399,6 +402,7 @@ const Settings = () => {
'EXTERNAL_AZURE_ENABLED',
'EXTERNAL_AZURE_CLIENT_ID',
'EXTERNAL_AZURE_SECRET',
'EXTERNAL_AZURE_URL',
'EXTERNAL_BITBUCKET_ENABLED',
'EXTERNAL_BITBUCKET_CLIENT_ID',
'EXTERNAL_BITBUCKET_SECRET',
@@ -435,6 +439,9 @@ const Settings = () => {
'EXTERNAL_SPOTIFY_ENABLED',
'EXTERNAL_SPOTIFY_CLIENT_ID',
'EXTERNAL_SPOTIFY_SECRET',
'EXTERNAL_ZOOM_ENABLED',
'EXTERNAL_ZOOM_CLIENT_ID',
'EXTERNAL_ZOOM_SECRET',
])}
model={{
EXTERNAL_APPLE_ENABLED: model.EXTERNAL_APPLE_ENABLED,
@@ -443,6 +450,7 @@ const Settings = () => {
EXTERNAL_AZURE_ENABLED: model.EXTERNAL_AZURE_ENABLED,
EXTERNAL_AZURE_CLIENT_ID: model.EXTERNAL_AZURE_CLIENT_ID || undefined,
EXTERNAL_AZURE_SECRET: model.EXTERNAL_AZURE_SECRET || undefined,
EXTERNAL_AZURE_URL: model.EXTERNAL_AZURE_URL || undefined,
EXTERNAL_BITBUCKET_ENABLED: model.EXTERNAL_BITBUCKET_ENABLED,
EXTERNAL_BITBUCKET_CLIENT_ID: model.EXTERNAL_BITBUCKET_CLIENT_ID || undefined,
EXTERNAL_BITBUCKET_SECRET: model.EXTERNAL_BITBUCKET_SECRET || undefined,
@@ -479,6 +487,9 @@ const Settings = () => {
EXTERNAL_SPOTIFY_ENABLED: model.EXTERNAL_SPOTIFY_ENABLED,
EXTERNAL_SPOTIFY_CLIENT_ID: model.EXTERNAL_SPOTIFY_CLIENT_ID || undefined,
EXTERNAL_SPOTIFY_SECRET: model.EXTERNAL_SPOTIFY_SECRET || undefined,
EXTERNAL_ZOOM_ENABLED: model.EXTERNAL_ZOOM_ENABLED,
EXTERNAL_ZOOM_CLIENT_ID: model.EXTERNAL_ZOOM_CLIENT_ID || undefined,
EXTERNAL_ZOOM_SECRET: model.EXTERNAL_ZOOM_SECRET || undefined,
}}
onChangeModel={(model) => setExternalProvidersModel(model)}
onReset={() => onFormReset()}
@@ -539,6 +550,11 @@ const Settings = () => {
showInlineError
errorMessage="Please enter the secret."
/>
<AutoField
name="EXTERNAL_AZURE_URL"
showInlineError
errorMessage="Please enter the azure tenant url."
/>
</>
)}
<Divider light />
@@ -889,6 +905,35 @@ const Settings = () => {
/>
</>
)}
<Divider light />
<ToggleField
name="EXTERNAL_ZOOM_ENABLED"
addOns={
externalProvidersModel.EXTERNAL_ZOOM_ENABLED && (
<a
className="pl-4 text-scale-900"
href="https://developers.zoom.us/"
target="_blank"
>
Create new credentials
</a>
)
}
/>
{externalProvidersModel.EXTERNAL_ZOOM_ENABLED && (
<>
<AutoField
name="EXTERNAL_ZOOM_CLIENT_ID"
showInlineError
errorMessage="Please enter the client id."
/>
<SecretField
name="EXTERNAL_ZOOM_SECRET"
showInlineError
errorMessage="Please enter the secret."
/>
</>
)}
</SchemaFormPanel>
</div>
@@ -22,6 +22,7 @@
"EXTERNAL_TWITTER_ENABLED",
"EXTERNAL_SLACK_ENABLED",
"EXTERNAL_SPOTIFY_ENABLED",
"EXTERNAL_ZOOM_ENABLED",
"SMS_AUTOCONFIRM",
"SMS_MAX_FREQUENCY",
"SMS_OTP_EXP",
@@ -110,6 +111,10 @@
"title": "Azure secret",
"type": "string"
},
"EXTERNAL_AZURE_URL": {
"title": "Azure tenant URL",
"type": "string"
},
"EXTERNAL_BITBUCKET_ENABLED": {
"title": "Bitbucket enabled",
"type": "boolean"
@@ -258,6 +263,18 @@
"title": "Spotify secret",
"type": "string"
},
"EXTERNAL_ZOOM_ENABLED": {
"title": "Zoom enabled",
"type": "boolean"
},
"EXTERNAL_ZOOM_CLIENT_ID": {
"title": "Zoom client ID",
"type": "string"
},
"EXTERNAL_ZOOM_SECRET": {
"title": "Zoom secret",
"type": "string"
},
"SMTP_ADMIN_EMAIL": {
"title": "SMTP Admin Email",
"type": "string"
@@ -375,6 +392,10 @@
"title": "Body",
"type": "string"
},
"PASSWORD_MIN_LENGTH": {
"title": "Minimum password length",
"type": "integer"
},
"SMS_AUTOCONFIRM": {
"title": "Enable phone confirmations",
"type": "boolean",
+89
View File
@@ -0,0 +1,89 @@
---
id: sla
title: Service Level Agreement
---
The following Service Level Agreement, which is incorporated into and forms part of the Subscription Agreement between Supabase, Inc. ("Supabase") and Customer (the "Agreement"), will apply to the Services for Enterprise Customers specified in an Order Form during the applicable Subscription Term:
## Platform
### 1. Uptime Commitment
Supabase will provide Actual Availability for at least ninety-nine and nine tenths percent (99.9%) of the total time in each calendar month during the Subscription Term, as measured by Supabase (the **"Uptime Commitment"**).
### 2. Service Credits
If the Uptime Commitment is not met during any particular calendar month during the Subscription Term, then Customer will be eligible for a service credit ("Service Credit"), provided that Customer reports to Supabase such failure to meet the Uptime Commitment and requests such Service Credit in accordance with this Exhibit. The amount of any Service Credit due hereunder shall be calculated as follows:
X * Y, where X = the total fees due from Customer to Supabase for the affected Services for the relevant calendar month (regardless of when billed or payable), and Y = the Credit Percentage corresponding with the Actual Availability provided (as a percentage of total time) for the relevant calendar month, as set forth in the table below.
| Actual Availability | Credit Percentage |
|----------------------------------------------------|-------------------|
| Less than 99.9% but greater than or equal to 99.0% | 10% |
| Less than 99.0% but greater than or equal to 98.0% | 15% |
| Less than 98.0% but greater than or equal to 96.0% | 20% |
| Less than 96.0% | 30% |
### 3. Credit Requests and Payment
To request a Service Credit, Customer must send an email to Supabase at support@supabase.io within thirty (30) days of the end of the month in which the Uptime Commitment was not met. Customer must include either its account ID or registered email address, and the previously reported dates and times that there was no Service Availability. If Supabase confirms that Customer is eligible for a Service Credit, Supabase will issue a credit to Customer’s account within thirty (30) days. Service Credits are not refunds, cannot be exchanged into a cash amount, and may only be used against future billing charges. Except as set forth in Section 4 below, the Service Credits shall be Customer’s sole and exclusive remedy, and Supabase’s sole and exclusive liability, for any failure by Supabase to meet the Uptime Commitment.
### 4. Definitions
All capitalized words used but not defined in this Service Level Agreement have the meaning set forth in the Agreement.
#### 4.1 Scheduled Availability
"Scheduled Availability" means the time, in minutes, that the applicable Services are generally accessible and available to Customer’s Permitted Users.
#### 4.2 Unscheduled Downtime
"Unscheduled Downtime" means the time, in minutes, that the applicable Services are not generally accessible and available to Customer’s Permitted Users, excluding inaccessibility or unavailability due to Customer’s or Permitted Users’ acts or omissions, force majeure events, scheduled maintenance disclosed with at least 24 hours’ notice by email, hacking or virus attacks, or reasonable emergency maintenance.
#### 4.3 Actual Availability
"Actual Availability" means Scheduled Availability less Unscheduled Downtime.
## Support
Supabase Support Service Level Agreements.
### 1. Urgent
**Critical Issue**
Defect resulting in full or partial system outage or a condition that makes Supabase unusable
or unavailable in production for all of Customer’s Users.
### 2. High
**Significant Business Disruption**
Issue resulting in a situation meaning major functionality is impacted and
significant performance degradation is experienced. Issue impacts significant proportion of user base and / or major
Supabase functionality.
### 3. Normal)
**Minor Feature or Functional Issue / General Question**
Issue results in a component of Supabase not
performing as expected or documented. An inquiry by a Customer representative regarding a general technical issue
or general question.
### 4. Low
**Minor Issue / Feature Request**
An Information request about Supabase or feature request.
## Target response times
| Severity Level | Standard | Priority | Priority Plus |
|----------------|--------------------------------------------|----------------------------------|-----------------------------------|
| 1. Urgent | 1 business hour<br />24/7 × 365 | 1 business hour<br />24/7 × 365 | 1 business hour<br />24/7 × 365 |
| 2. High | 4 business hours<br />Monday - Friday | 2 business hours<br />Monday - Friday | 2 business hours<br />24/7 × 365 |
| 3. Normal | 1 business day<br />Monday - Friday | 1 business day<br />Monday - Friday | 12 business hours<br />Monday - Friday |
| 4. Low | 2 business days<br />Monday - Friday | 2 business days<br />Monday - Friday | 1 business day<br />Monday - Friday |
Business hours are from 6am to 6pm (local time), except where otherwise stated.
+104 -123
View File
@@ -1,13 +1,13 @@
---
id: with-react
title: "Quickstart: React"
title: 'Quickstart: React'
description: Learn how to use Supabase in your React App.
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
## Intro
## Intro
This example provides the steps to build a simple user management app (from scratch!) using Supabase and React. It includes:
@@ -21,7 +21,7 @@ By the end of this guide you'll have an app which allows users to login and upda
![Supabase User Management example](/img/user-management-demo.png)
<!-- ## Video demo
<!-- ## Video demo
Coming soon.
@@ -29,7 +29,7 @@ Use the annotated timeline to step through this tutorial.
## Instant deploy
Want to deploy a completed version of this example without following the guide?
Want to deploy a completed version of this example without following the guide?
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Freact-user-management&project-name=supabase-user-management&repository-name=supabase-user-management&demo-title=Supabase%20User%20Management&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-react-user-management.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FZ3HkQqe.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=nextjs-user-management)
@@ -44,20 +44,19 @@ If you want to do it yourself, let's get started: -->
## Project set up
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
and then creating a "schema" inside the database.
### Create a project
### Create a project
1. Go to [app.supabase.io](https://app.supabase.io).
1. Click on "New Project".
1. Enter your project details.
1. Wait for the new database to launch.
### Set up the database schema
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
or you can just copy/paste the SQL from below and run it yourself.
<Tabs
@@ -75,7 +74,7 @@ values={[
```
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/sql-user-management-starter.mp4" type="video/mp4" muted playsInline />
<source src="/docs/videos/sql-user-management-starter.mp4" type="video/mp4" muted playsInline />
</video>
</TabItem>
@@ -133,11 +132,9 @@ create policy "Anyone can upload an avatar."
</TabItem>
</Tabs>
### Get the API Keys
### Get the API Keys
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
We just need to get the URL and `anon` key from the API settings.
<Tabs
@@ -155,7 +152,7 @@ values={[
```
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
</TabItem>
@@ -167,7 +164,7 @@ Let's start building the React app from scratch.
### Initialize a React app
We can use [Create React App](https://create-react-app.dev/docs/getting-started/) to initialize
We can use [Create React App](https://create-react-app.dev/docs/getting-started/) to initialize
an app called `supabase-react`:
```bash
@@ -181,7 +178,7 @@ Then let's install the only additional dependency: [supabase-js](https://github.
npm install @supabase/supabase-js
```
And finally we want to save the environment variables in a `.env`.
And finally we want to save the environment variables in a `.env`.
All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
```bash title=".env"
@@ -201,7 +198,7 @@ const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
```
And one optional step is to update the CSS file `src/index.css` to make the app look nice.
And one optional step is to update the CSS file `src/index.css` to make the app look nice.
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/react-user-management/src/index.css).
### Set up a Login component
@@ -216,7 +213,9 @@ export default function Auth() {
const [loading, setLoading] = useState(false)
const [email, setEmail] = useState('')
const handleLogin = async (email) => {
const handleLogin = async (e) => {
e.preventDefault()
try {
setLoading(true)
const { error } = await supabase.auth.signIn({ email })
@@ -231,30 +230,27 @@ export default function Auth() {
return (
<div className="row flex flex-center">
<div className="col-6 form-widget">
<div className="col-6 form-widget" aria-live="polite">
<h1 className="header">Supabase + React</h1>
<p className="description">Sign in via magic link with your email below</p>
<div>
<input
className="inputField"
type="email"
placeholder="Your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<button
onClick={(e) => {
e.preventDefault()
handleLogin(email)
}}
className={'button block'}
disabled={loading}
>
{loading ? <span>Loading</span> : <span>Send magic link</span>}
</button>
</div>
{loading ? (
'Sending magic link...'
) : (
<form onSubmit={handleLogin}>
<label htmlFor="email">Email</label>
<input
id="email"
className="inputField"
type="email"
placeholder="Your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button className="button block" aria-live="polite">
Send magic link
</button>
</form>
)}
</div>
</div>
)
@@ -271,7 +267,7 @@ Let's create a new component for that called `Account.js`.
import { useState, useEffect } from 'react'
import { supabase } from './supabaseClient'
export default function Account({ session }) {
const Account = ({ session }) => {
const [loading, setLoading] = useState(true)
const [username, setUsername] = useState(null)
const [website, setWebsite] = useState(null)
@@ -281,7 +277,7 @@ export default function Account({ session }) {
getProfile()
}, [session])
async function getProfile() {
const getProfile = async () => {
try {
setLoading(true)
const user = supabase.auth.user()
@@ -308,7 +304,9 @@ export default function Account({ session }) {
}
}
async function updateProfile({ username, website, avatar_url }) {
const updateProfile = async (e) => {
e.preventDefault()
try {
setLoading(true)
const user = supabase.auth.user()
@@ -336,52 +334,47 @@ export default function Account({ session }) {
}
return (
<div className="form-widget">
<div>
<label htmlFor="email">Email</label>
<input id="email" type="text" value={session.user.email} disabled />
</div>
<div>
<label htmlFor="username">Name</label>
<input
id="username"
type="text"
value={username || ''}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="website">Website</label>
<input
id="website"
type="website"
value={website || ''}
onChange={(e) => setWebsite(e.target.value)}
/>
</div>
<div>
<button
className="button block primary"
onClick={() => updateProfile({ username, website, avatar_url })}
disabled={loading}
>
{loading ? 'Loading ...' : 'Update'}
</button>
</div>
<div>
<button className="button block" onClick={() => supabase.auth.signOut()}>
Sign Out
</button>
</div>
<div aria-live="polite">
{loading ? (
'Saving ...'
) : (
<form onSubmit={updateProfile} className="form-widget">
<div>Email: {session.user.email}</div>
<div>
<label htmlFor="username">Name</label>
<input
id="username"
type="text"
value={username || ''}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="website">Website</label>
<input
id="website"
type="url"
value={website || ''}
onChange={(e) => setWebsite(e.target.value)}
/>
</div>
<div>
<button className="button block primary" disabled={loading}>
Update profile
</button>
</div>
</form>
)}
<button type="button" className="button block" onClick={() => supabase.auth.signOut()}>
Sign Out
</button>
</div>
)
}
export default Account
```
### Launch!
Now that we have all the components in place, let's update `App.js`:
@@ -393,7 +386,7 @@ import { supabase } from './supabaseClient'
import Auth from './Auth'
import Account from './Account'
export default function Home() {
export default () => {
const [session, setSession] = useState(null)
useEffect(() => {
@@ -420,7 +413,6 @@ npm start
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
![Supabase React](/img/supabase-react-demo.png)
## Bonus: Profile photos
@@ -434,8 +426,9 @@ Let's create an avatar for the user so that they can upload a profile photo. We
```jsx title="src/Avatar.js"
import { useEffect, useState } from 'react'
import { supabase } from './supabaseClient'
import VisuallyHidden from '@reach/visually-hidden'
export default function Avatar({ url, size, onUpload }) {
export default ({ url, size, onUpload }) => {
const [avatarUrl, setAvatarUrl] = useState(null)
const [uploading, setUploading] = useState(false)
@@ -443,7 +436,7 @@ export default function Avatar({ url, size, onUpload }) {
if (url) downloadImage(url)
}, [url])
async function downloadImage(path) {
const downloadImage = async (path) => {
try {
const { data, error } = await supabase.storage.from('avatars').download(path)
if (error) {
@@ -456,8 +449,7 @@ export default function Avatar({ url, size, onUpload }) {
}
}
async function uploadAvatar(event) {
const uploadAvatar = async (event) => {
try {
setUploading(true)
@@ -470,9 +462,7 @@ export default function Avatar({ url, size, onUpload }) {
const fileName = `${Math.random()}.${fileExt}`
const filePath = `${fileName}`
let { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file)
let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
if (uploadError) {
throw uploadError
@@ -487,44 +477,36 @@ export default function Avatar({ url, size, onUpload }) {
}
return (
<div>
{avatarUrl ? (
<img
src={avatarUrl}
alt="Avatar"
className="avatar image"
style={{ height: size, width: size }}
/>
) : (
<div className="avatar no-image" style={{ height: size, width: size }} />
)}
<div style={{ width: size }}>
<div style={{ width: size }} aria-live="polite">
<img
src={avatarUrl ? avatarUrl : `https://place-hold.it/${size}x${size}`}
alt={avatarUrl ? 'Avatar' : 'No image'}
className="avatar image"
style={{ height: size, width: size }}
/>
{uploading ? "Uploading..." : (
<label className="button primary block" htmlFor="single">
{uploading ? 'Uploading ...' : 'Upload'}
Upload an avatar
</label>
<input
style={{
visibility: 'hidden',
position: 'absolute',
}}
type="file"
id="single"
accept="image/*"
onChange={uploadAvatar}
disabled={uploading}
/>
</div>
<VisuallyHidden>
<input
type="file"
id="single"
accept="image/*"
onChange={uploadAvatar}
disabled={uploading}
/>
</VisuallyHidden>
)}
</div>
)
}
```
### Add the new widget
And then we can add the widget to the Account page:
```jsx title="src/Account.js"
// Import the new component
import Avatar from './Avatar'
@@ -545,7 +527,6 @@ return (
{/* ... */}
</div>
)
```
## Next steps
+1
View File
@@ -210,6 +210,7 @@ module.exports = {
'company/terms',
'company/privacy',
'company/aup',
'company/sla',
],
},
],
+24 -24
View File
@@ -892,55 +892,55 @@ pages:
isSpotlight: true
js: |
```js
const { data: user, error } = await supabase.auth.api.updateUserById({
uid: '6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
attributes: { email: 'new@email.com' } },
})
const { data: user, error } = await supabase.auth.api.updateUserById(
'6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
{ email: 'new@email.com' }
)
```
- name: Updates a user's password.
isSpotlight: true
js: |
```js
const { data: user, error } = await supabase.auth.api.updateUserById({
uid: '6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
attributes: { password: 'new@email.com' } },
})
const { data: user, error } = await supabase.auth.api.updateUserById(
'6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
{ password: 'new_password' }
)
```
- name: Updates a user's metadata.
isSpotlight: true
js: |
```js
const { data: user, error } = await supabase.auth.api.updateUserById({
uid: '6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
attributes: { user_metadata: { hello: 'world' } },
})
const { data: user, error } = await supabase.auth.api.updateUserById(
'6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
{ user_metadata: { hello: 'world' } }
)
```
- name: Updates a user's app_metadata.
isSpotlight: true
js: |
```js
const { data: user, error } = await supabase.auth.api.updateUserById({
uid: '6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
attributes: { app_metadata: { plan: 'trial' } },
})
const { data: user, error } = await supabase.auth.api.updateUserById(
'6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
{ app_metadata: { plan: 'trial' } }
)
```
- name: Confirms a user's email address.
isSpotlight: true
js: |
```js
const { data: user, error } = await supabase.auth.api.updateUserById({
uid: '6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
attributes: { email_confirm: true },
})
const { data: user, error } = await supabase.auth.api.updateUserById(
'6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
{ email_confirm: true }
)
```
- name: Confirms a user's phone number.
isSpotlight: true
js: |
```js
const { data: user, error } = await supabase.auth.api.updateUserById({
uid: '6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
attributes: { phone_number: true },
})
const { data: user, error } = await supabase.auth.api.updateUserById(
'6aa5d0d4-2a9f-4483-b6c8-0cf4c6c98ac4',
{ phone_number: true }
)
```
select():
+1 -1
View File
@@ -1,5 +1,5 @@
{
"show": true,
"show": false,
"text": "Supabase Launch Week III",
"cta": "Supabase acquires Logflare",
"link": "/blog/2021/12/02/supabase-acquires-logflare"