Merge pull request #4822 from supabase/master

Prod deploy for studio
This commit is contained in:
Joshen Lim authored and GitHub committed 2022-01-06 16:29:21 +08:00
commit dd6d3d2ce8
34 files changed
+669 -1226

No files matched your search

+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 Supabase
Copyright 2021 Supabase
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+1 -1
View File
@@ -154,5 +154,5 @@ services:
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./volumes/db/data:/var/lib/postgresql/data
# - ./volumes/db/data:/var/lib/postgresql/data
- ./volumes/db/init:/docker-entrypoint-initdb.d
+7 -18
View File
@@ -4,9 +4,8 @@ import Head from 'next/head'
import { observer } from 'mobx-react-lite'
import { Button, IconGitHub, Typography } from '@supabase/ui'
import { API_URL } from 'lib/constants'
import { useStore } from 'hooks'
import { auth, GOTRUE_ENABLED } from 'lib/gotrue'
import { auth } from 'lib/gotrue'
const Landing = () => {
const { ui } = useStore()
@@ -55,13 +54,9 @@ const Landing = () => {
type="success"
className="ml-8 font-medium hover:text-gray-300 focus:outline-none focus:text-gray-300 transition duration-150 ease-in-out"
>
{GOTRUE_ENABLED ? (
<Button onClick={handleGithubSignIn} icon={<IconGitHub />}>
Sign In With Github
</Button>
) : (
<Link href={`${API_URL}/login`}>Sign In</Link>
)}
<Button onClick={handleGithubSignIn} icon={<IconGitHub />}>
Sign In With Github
</Button>
</Typography.Text>
</div>
</nav>
@@ -78,15 +73,9 @@ const Landing = () => {
</p>
</Typography.Text>
<div className="mt-5 sm:mt-8 sm:flex sm:justify-center space-x-3">
{GOTRUE_ENABLED ? (
<Button onClick={handleGithubSignIn} size="large" icon={<IconGitHub />}>
Sign In With Github
</Button>
) : (
<Link href={`${API_URL}/login`}>
<Button size="large">Sign In</Button>
</Link>
)}
<Button onClick={handleGithubSignIn} size="large" icon={<IconGitHub />}>
Sign In With Github
</Button>
<Link href="https://supabase.com/docs">
<Button size="large" type="default">
Docs
@@ -9,33 +9,43 @@ import { isNil } from 'lodash'
interface Props {
isCustomQuery: boolean
data?: LogData[]
data?: Array<LogData | Object>
}
type LogMap = { [id: string]: LogData }
/**
* Logs table view with focus side panel
*
* When in custom data display mode, the side panel will not open when focusing on logs.
*/
const LogTable = ({ isCustomQuery, data }: Props) => {
const LogTable = ({ isCustomQuery, data = [] }: Props) => {
const [focusedLog, setFocusedLog] = useState<LogData | null>(null)
const columns = ['timestamp', 'event_message'].map((v) => ({
const columnNames = Object.keys(data[0] || {})
// whether the data structure is LogData format.
const hasLogDataFormat =
columnNames.includes('timestamp') &&
columnNames.includes('event_message') &&
columnNames.length === 4
const columns = (hasLogDataFormat ? ['timestamp', 'event_message'] : columnNames).map((v) => ({
key: v,
name: v,
width: v === 'timestamp' ? 210 : undefined,
width: hasLogDataFormat && v === 'timestamp' ? 210 : undefined,
resizable: true,
headerRenderer: () => {
return <div className="flex items-center text-xs font-mono h-full">{v}</div>
},
formatter: ({ row }: any) => {
let value = row[v]
if (v === 'timestamp') {
if (hasLogDataFormat && v === 'timestamp') {
value = dayjs(Number(row['timestamp']) / 1000).toISOString()
}
return (
<p
className={[
'block whitespace-wrap font-mono',
`${row.id === focusedLog?.id ? 'font-bold' : ''}`,
`${v === 'timestamp' ? 'text-green-500' : ''}`,
`${hasLogDataFormat && row.id === focusedLog?.id ? 'font-bold' : ''}`,
`${hasLogDataFormat && v === 'timestamp' ? 'text-green-500' : ''}`,
].join(' ')}
>
{value}
@@ -44,20 +54,23 @@ const LogTable = ({ isCustomQuery, data }: Props) => {
},
}))
const logMap: { [id: string]: LogData } = useMemo(() => {
return (data || []).reduce((acc: any, d) => {
const logMap = useMemo(() => {
if (!hasLogDataFormat) return {} as LogMap
const logData = data as LogData[]
return logData.reduce((acc: LogMap, d: LogData) => {
acc[d.id] = d
return acc
}, {})
}, {}) as LogMap
}, [JSON.stringify(data)])
const strLogMap = JSON.stringify(logMap)
const stringData = JSON.stringify(data)
useEffect(() => {
if (!hasLogDataFormat) return
if (isNil(data)) return
if (focusedLog && !(focusedLog.id in logMap)) {
setFocusedLog(null)
}
}, [strLogMap])
}, [stringData])
if (!data) return null
@@ -65,15 +78,18 @@ const LogTable = ({ isCustomQuery, data }: Props) => {
const maxHeight = isCustomQuery ? 'calc(100vh - 42px - 10rem)' : 'calc(100vh - 42px - 3rem)'
const logDataRows = useMemo(() => {
// sort in timestamp descending order
if (!hasLogDataFormat) return data
return Object.values(logMap).sort((a, b) => b.timestamp - a.timestamp)
}, [strLogMap])
}, [stringData])
return (
<section className="flex flex-1 flex-row" style={{ maxHeight }}>
<DataGrid
style={{ height: '100%' }}
className="flex-grow flex-1"
onSelectedCellChange={({ idx, rowIdx }) => setFocusedLog(data[rowIdx])}
onSelectedCellChange={({ idx, rowIdx }) => {
if (!hasLogDataFormat) return
setFocusedLog(data[rowIdx] as LogData)
}}
noRowsFallback={
<div className="p-4">
<Typography.Text type="secondary" small className="font-mono">
@@ -82,10 +98,22 @@ const LogTable = ({ isCustomQuery, data }: Props) => {
</div>
}
columns={columns as any}
rowClass={(r) => `${r.id === focusedLog?.id ? 'bg-green-800' : 'cursor-pointer'}`}
rowClass={(r) => {
if (!hasLogDataFormat) return 'cursor-pointer'
const row = r as LogData
return `${row.id === focusedLog?.id ? 'bg-green-800' : 'cursor-pointer'}`
}}
rows={logDataRows}
rowKeyGetter={(r) => r.id}
onRowClick={(r) => setFocusedLog(logMap[r.id])}
rowKeyGetter={(r) => {
if (!hasLogDataFormat) return Object.keys(r)[0]
const row = r as LogData
return row.id
}}
onRowClick={(r) => {
if (!hasLogDataFormat) return
const row = r as LogData
setFocusedLog(logMap[row.id])
}}
/>
{focusedLog && (
<div className="w-2/5 flex flex-col">
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { useStore } from 'hooks'
import WithSidebar from './WithSidebar'
import { auth, GOTRUE_ENABLED } from 'lib/gotrue'
import { auth } from 'lib/gotrue'
/**
* layout for dashboard homepage, account and org settings
@@ -25,16 +25,14 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
router.reload()
}
let baseLogoutLink = {
const baseLogoutLink = {
icon: '/icons/feather/power.svg',
label: 'Logout',
href: `${API_URL}/logout`,
key: `${API_URL}/logout`,
}
let logoutLink = GOTRUE_ENABLED
? { ...baseLogoutLink, href: undefined, onClick: onClickLogout }
: baseLogoutLink
const logoutLink = { ...baseLogoutLink, href: undefined, onClick: onClickLogout }
const organizationsLinks = app.organizations
.list()
@@ -0,0 +1,53 @@
import {
DEFAULT_MINIMUM_PASSWORD_STRENGTH,
PASSWORD_STRENGTH_COLOR,
PASSWORD_STRENGTH_PERCENTAGE,
} from 'lib/constants'
import { Typography } from '@supabase/ui'
interface Props {
passwordStrengthScore: number
passwordStrengthMessage: string
password: string
}
const PasswordStrengthBar = ({
passwordStrengthScore = 0,
passwordStrengthMessage = '',
password = '',
}: Props) => {
return (
<>
{password && (
<div
aria-valuemax={100}
aria-valuemin={0}
aria-valuenow={(PASSWORD_STRENGTH_PERCENTAGE as any)[passwordStrengthScore]}
aria-valuetext={(PASSWORD_STRENGTH_PERCENTAGE as any)[passwordStrengthScore]}
role="progressbar"
className="mb-2 bg-bg-alt-light dark:bg-bg-alt-dark rounded overflow-hidden transition-all border dark:border-dark"
>
<div
style={{
width: (PASSWORD_STRENGTH_PERCENTAGE as any)[passwordStrengthScore],
}}
className={`relative h-2 w-full ${
(PASSWORD_STRENGTH_COLOR as any)[passwordStrengthScore]
} transition-all duration-500 ease-out shadow-inner`}
></div>
</div>
)}
<Typography.Text
className={
passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH ? 'text-green-600' : ''
}
>
{passwordStrengthMessage
? passwordStrengthMessage
: 'This is the password to your postgres database, so it must be a strong password and hard to guess.'}
</Typography.Text>
</>
)
}
export default PasswordStrengthBar
+19 -41
View File
@@ -1,11 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import { Claims } from '@auth0/nextjs-auth0'
import { readOnly } from './supabaseClient'
import { SupaResponse, User } from 'types'
import { auth0 } from './auth0'
import { flattenNamespaceOnUser } from './apiHelpers'
import { config } from 'process'
import { getAuth0Id, getAuthUser, getIdentity, GOTRUE_ENABLED } from 'lib/gotrue'
import { getAuth0Id, getAuthUser, getIdentity } from 'lib/gotrue'
/**
* Use this method on api routes to check if user is authenticated and having required permissions.
@@ -32,10 +28,6 @@ export async function apiAuthenticate(
const { slug: orgSlug, ref: projectRef } = req.query
try {
// Check that they are logged in
// If no error throw from getAccessToken. That's good, we can continue
if (!(GOTRUE_ENABLED)) await auth0.getAccessToken(req, res)
const user = await fetchUser(req, res)
if (!user) {
return { error: new Error('The user does not exist') } as unknown as SupaResponse<User>
@@ -59,35 +51,24 @@ async function fetchUser(req: NextApiRequest, res: NextApiResponse): Promise<any
let user_id_auth0 = null
let gotrue_id = null
let email = null
if (GOTRUE_ENABLED) {
const token = req.headers.authorization
if (!token) {
return res.status(401).end('Unauthorized: missing access token')
}
let { user: gotrue_user, error: authError } = await getAuthUser(token)
if (authError) {
throw authError
}
if (gotrue_user !== null) {
gotrue_id = gotrue_user?.id
email = gotrue_user.email
let { identity, error } = getIdentity(gotrue_user)
if (error) throw error
if (identity?.provider !== undefined) {
user_id_auth0 = getAuth0Id(identity?.provider, identity?.id)
}
const token = req.headers.authorization
if (!token) {
return res.status(401).end('Unauthorized: missing access token')
}
let { user: gotrue_user, error: authError } = await getAuthUser(token)
if (authError) {
throw authError
}
if (gotrue_user !== null) {
gotrue_id = gotrue_user?.id
email = gotrue_user.email
let { identity, error } = getIdentity(gotrue_user)
if (error) throw error
if (identity?.provider !== undefined) {
user_id_auth0 = getAuth0Id(identity?.provider, identity?.id)
}
} else {
const session = auth0.getSession(req, res)
if (!session) {
return null
}
const { user: auth0_user } = session
const flattened = flattenNamespaceOnUser('https://supabase.io', auth0_user)
user_id_supabase = flattened.user_id_supabase
user_id_auth0 = flattened.user_id_auth0
email = flattened.email
}
if (user_id_supabase) {
@@ -103,14 +84,11 @@ async function fetchUser(req: NextApiRequest, res: NextApiResponse): Promise<any
`
)
const { data } = GOTRUE_ENABLED
? await query.eq('gotrue_id', gotrue_id).single()
: await await query.eq('auth0_id', user_id_auth0).single()
const { data } = await query.eq('gotrue_id', gotrue_id).single()
return data
}
async function checkMemberPermission(req: NextApiRequest, user: Claims) {
async function checkMemberPermission(req: NextApiRequest, user: any) {
const org = await getOrganization(req)
if (!org) {
throw new Error('User organization does not exist')
-32
View File
@@ -1,32 +0,0 @@
import { initAuth0 } from '@auth0/nextjs-auth0'
import { IS_PLATFORM } from '../constants'
let auth0: any
if (IS_PLATFORM) {
auth0 = initAuth0({
baseURL: process.env.NEXT_PUBLIC_API_DOMAIN,
issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
secret: process.env.SESSION_COOKIE_SECRET,
authorizationParams: {
scope: 'openid profile email offline_access',
audience: process.env.AUTH0_AUDIENCE,
},
routes: {
callback: `/platform/auth0-callback`,
postLogoutRedirect: '/',
},
session: {
rollingDuration: 60 * 60 * 24,
absoluteDuration: 60 * 60 * 24 * 7,
cookie: {
sameSite: 'lax',
domain: process.env.COOKIE_DOMAIN || 'localhost',
},
},
})
}
export { auth0 }
-15
View File
@@ -1,15 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next'
// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
export default function runMiddleware(req: NextApiRequest, res: NextApiResponse, fn: any) {
return new Promise((resolve, reject) => {
fn(req, res, (result: any) => {
if (result instanceof Error) {
return reject(result)
}
return resolve(result)
})
})
}
+1 -2
View File
@@ -1,4 +1,3 @@
import { GOTRUE_ENABLED } from 'lib/gotrue'
import { tryParseJson } from 'lib/helpers'
import { isUndefined } from 'lodash'
import { SupaResponse } from 'types/base'
@@ -110,7 +109,7 @@ export function constructHeaders(requestId: string, optionHeaders?: { [prop: str
}
const hasAuthHeader = !isUndefined(optionHeaders) && 'Authorization' in optionHeaders
if (GOTRUE_ENABLED && !hasAuthHeader) {
if (!hasAuthHeader) {
const accessToken = getAccessToken()
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
}
+1 -1
View File
@@ -117,7 +117,7 @@ export const PASSWORD_STRENGTH = {
export const PASSWORD_STRENGTH_COLOR = {
0: 'bg-red-500',
1: 'bg-red-400',
1: 'bg-red-500',
2: 'bg-yellow-500',
3: 'bg-yellow-500',
4: 'bg-green-500',
+1 -2
View File
@@ -1,6 +1,5 @@
import { GoTrueClient, User } from '@supabase/gotrue-js'
export const GOTRUE_ENABLED = (process.env.NEXT_PUBLIC_GOTRUE_ENABLED === 'true') || false
export const GOTRUE_URL = process.env.NEXT_PUBLIC_GOTRUE_URL || 'http://localhost:8000/auth/v1'
export const auth = new GoTrueClient({
@@ -8,7 +7,7 @@ export const auth = new GoTrueClient({
autoRefreshToken: true,
})
export const getAuthUser = async (token: String): Promise<any> => {
export const getAuthUser = async (token: String): Promise<any> => {
try {
const { data: user, error } = await auth.api.getUser(token.replace('Bearer ', ''))
if (error) throw error
+37
View File
@@ -1,3 +1,6 @@
import { post } from 'lib/common/fetch'
import { PASSWORD_STRENGTH, DEFAULT_MINIMUM_PASSWORD_STRENGTH, API_URL } from 'lib/constants'
export const tryParseJson = (jsonString: any) => {
try {
let parsed = JSON.parse(jsonString)
@@ -161,3 +164,37 @@ export const copyToClipboard = (str: string, callback = () => {}) => {
console.warn('Unable to copy to clipboard')
}
}
export async function passwordStrength(value: string) {
let message = ''
let warning = ''
let strength = 0
if (value && value !== '') {
const response = await post(`${API_URL}/profile/password-check`, { password: value })
if (!response.error) {
const { result } = response
const score = (PASSWORD_STRENGTH as any)[result.score]
const suggestions = result.feedback?.suggestions ? result.feedback.suggestions.join(' ') : ''
// set message :string
message = `${score} ${suggestions}`
// set strength :number
strength = result.score
// warning message for anything below 4 strength :string
if (result.score < DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
warning = `${
result?.feedback?.warning ? result?.feedback?.warning + '.' : ''
} You need a stronger password.`
}
}
}
return {
message,
warning,
strength,
}
}
+37 -854
View File
File diff suppressed because it is too large. Load diff
+1 -2
View File
@@ -10,7 +10,6 @@
"deploy:staging": "VERCEL_ORG_ID=team_E6KJ1W561hMTjon1QSwOh0WO VERCEL_PROJECT_ID=QmcmhbiAtCMFTAHCuGgQscNbke4TzgWULECctNcKmxWCoT vercel --prod -A .vercel/staging.json"
},
"dependencies": {
"@auth0/nextjs-auth0": "^1.6.1",
"@headlessui/react": "^1.4.1",
"@monaco-editor/react": "^4.3.1",
"@next/bundle-analyzer": "^12.0.4",
@@ -21,7 +20,7 @@
"@supabase/grid": "^2.3.9",
"@supabase/postgres-meta": "^0.26.1",
"@supabase/react-data-grid": "^7.1.0-beta.7",
"@supabase/supabase-js": "^1.28.0",
"@supabase/supabase-js": "^1.29.1",
"@supabase/ui": "^0.35.0",
"ajv": "^8.6.3",
"analytics-node": "^3.5.0",
+63 -6
View File
@@ -10,18 +10,20 @@ import 'styles/contextMenu.scss'
import Head from 'next/head'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import type { AppProps } from 'next/app'
import { useRouter } from 'next/router'
import { FC, useEffect, useState } from 'react'
import { Subscription } from '@supabase/gotrue-js'
import { Toaster, ToastBar, toast } from 'react-hot-toast'
import { Button, IconX } from '@supabase/ui'
import { RootStore } from 'stores'
import { StoreProvider } from 'hooks'
import { getParameterByName } from 'lib/common/fetch'
import { GOTRUE_ERRORS } from 'lib/constants'
import { auth } from 'lib/gotrue'
import PageTelemetry from 'components/ui/PageTelemetry'
import FlagProvider from 'components/ui/Flag/FlagProvider'
import Connecting from 'components/ui/Loading'
const PortalRootWithNoSSR = dynamic(
// @ts-ignore
@@ -69,6 +71,59 @@ const PortalToast = () => (
</PortalRootWithNoSSR>
)
function doesTokenDataExist() {
// ignore if server-side
if (typeof window === 'undefined') return false
// check tokenData on localstorage
const tokenData = window?.localStorage['supabase.auth.token']
return tokenData != undefined && typeof tokenData === 'string'
}
/**
* On app first load, gotrue client may take a while to refresh access token
* We have to wait for that process to complete before showing children components
*/
const GotrueWrapper: FC = ({ children }) => {
const [loading, setLoading] = useState(doesTokenDataExist())
useEffect(() => {
let subscription: Subscription | null
let timer: any
const currentSession = auth.session()
function tokenRefreshed() {
setLoading(false)
// clean subscription
if (subscription) subscription.unsubscribe()
// clean timer
if (timer) clearTimeout(timer)
}
if (currentSession != undefined && currentSession != null) {
// if there is an active session, go ahead
setLoading(false)
} else {
// else wait for TOKEN_REFRESHED event before continue
const response = auth.onAuthStateChange((_event, session) => {
if (loading && _event === 'TOKEN_REFRESHED') {
tokenRefreshed()
}
})
subscription = response.data ?? null
// we need a timeout here, in case token refresh fails
timer = setTimeout(() => setLoading(false), 5 * 1000)
}
return () => {
if (subscription) subscription.unsubscribe()
if (timer) clearTimeout(timer)
}
}, [])
return <>{loading ? <Connecting /> : children}</>
}
function MyApp({ Component, pageProps }: AppProps) {
const [rootStore] = useState(() => new RootStore())
const router = useRouter()
@@ -92,9 +147,11 @@ function MyApp({ Component, pageProps }: AppProps) {
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<link rel="stylesheet" type="text/css" href="/css/fonts.css" />
</Head>
<PageTelemetry>
<Component {...pageProps} />
</PageTelemetry>
<GotrueWrapper>
<PageTelemetry>
<Component {...pageProps} />
</PageTelemetry>
</GotrueWrapper>
<PortalToast />
</FlagProvider>
</StoreProvider>
+45 -101
View File
@@ -7,14 +7,7 @@ import { useRef, useState, useEffect } from 'react'
import { debounce, isUndefined, values } from 'lodash'
import { toJS } from 'mobx'
import { observer } from 'mobx-react-lite'
import {
Button,
Typography,
Listbox,
IconUsers,
IconAlertCircle,
IconDollarSign,
} from '@supabase/ui'
import { Button, Typography, Listbox, IconUsers, IconAlertCircle } from '@supabase/ui'
import { API_URL } from 'lib/constants'
import { post } from 'lib/common/fetch'
@@ -23,9 +16,6 @@ import {
REGIONS,
REGIONS_DEFAULT,
DEFAULT_MINIMUM_PASSWORD_STRENGTH,
PASSWORD_STRENGTH,
PASSWORD_STRENGTH_COLOR,
PASSWORD_STRENGTH_PERCENTAGE,
PRICING_PLANS,
PRICING_PLANS_DEFAULT,
DEFAULT_FREE_PROJECTS_LIMIT,
@@ -38,6 +28,8 @@ import { getURL } from 'lib/helpers'
import FormField from 'components/to-be-cleaned/forms/FormField'
import Panel from 'components/to-be-cleaned/Panel'
import InformationBox from 'components/ui/InformationBox'
import { passwordStrength } from 'lib/helpers'
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
interface StripeCustomer {
paymentMethods: any
@@ -164,22 +156,10 @@ export const Wizard = observer(() => {
}
async function checkPasswordStrength(value: any) {
let passwordStrength = ''
if (value && value !== '') {
const response = await post(`${API_URL}/profile/password-check`, { password: value })
if (!response.error) {
const { result } = response
const score = (PASSWORD_STRENGTH as any)[result.score]
const suggestions = result.feedback?.suggestions
? result.feedback.suggestions.join(' ')
: ''
passwordStrength = `${score} ${suggestions}`
setPasswordStrengthScore(result.score)
setPasswordStrengthWarning(result.feedback.warning ? result.feedback.warning : '')
}
}
setPasswordStrengthMessage(passwordStrength)
const { message, warning, strength } = await passwordStrength(value)
setPasswordStrengthScore(strength)
setPasswordStrengthWarning(warning)
setPasswordStrengthMessage(message)
}
const onClickNext = async () => {
@@ -265,11 +245,7 @@ export const Wizard = observer(() => {
))}
</Listbox>
{!currentOrg?.is_owner ? (
<NotOrganizationOwnerWarning />
) : (
<>{isSelectFreeTier && isOverFreeProjectLimit && <FreeProjectLimitWarning />}</>
)}
{!currentOrg?.is_owner && <NotOrganizationOwnerWarning />}
</Panel.Content>
{canCreateProject && (
@@ -295,48 +271,13 @@ export const Wizard = observer(() => {
value={dbPass}
onChange={onDbPassChange}
description={
<>
{dbPass && (
<div
aria-valuemax={100}
aria-valuemin={0}
aria-valuenow={
(PASSWORD_STRENGTH_PERCENTAGE as any)[passwordStrengthScore]
}
aria-valuetext={
(PASSWORD_STRENGTH_PERCENTAGE as any)[passwordStrengthScore]
}
role="progressbar"
className="mb-2 bg-bg-alt-light dark:bg-bg-alt-dark rounded overflow-hidden transition-all border dark:border-dark"
>
<div
style={{
width: (PASSWORD_STRENGTH_PERCENTAGE as any)[passwordStrengthScore],
}}
className={`relative h-2 w-full ${
(PASSWORD_STRENGTH_COLOR as any)[passwordStrengthScore]
} transition-all duration-500 ease-out shadow-inner`}
></div>
</div>
)}
<span
className={
passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH
? 'text-green-600'
: ''
}
>
{passwordStrengthMessage
? passwordStrengthMessage
: 'This is the password to your postgres database, so it must be a strong password and hard to guess.'}
</span>
</>
}
errorMessage={
passwordStrengthWarning
? `${passwordStrengthWarning}. ${passwordErrorMessage}.`
: passwordErrorMessage
<PasswordStrengthBar
passwordStrengthScore={passwordStrengthScore}
password={dbPass}
passwordStrengthMessage={passwordStrengthMessage}
/>
}
errorMessage={passwordStrengthWarning}
/>
</Panel.Content>
@@ -351,36 +292,39 @@ export const Wizard = observer(() => {
description="Select a region close to you for the best performance."
/>
</Panel.Content>
<Panel.Content className="Form section-block--body has-inputs-centered ">
<Listbox
label="Pricing Plan"
layout="horizontal"
value={dbPricingPlan}
onChange={onDbPricingPlanChange}
// @ts-ignore
descriptionText={
<>
Select a plan that suits your needs.&nbsp;
<a className="underline" target="_blank" href="https://supabase.com/pricing">
More details
</a>
</>
}
>
{Object.entries(PRICING_PLANS).map(([k, v]) => (
<Listbox.Option key={k} label={v} value={v}>
{`${v}${v === PRICING_PLANS.PRO ? ' - $25/month' : ''}`}
</Listbox.Option>
))}
</Listbox>
{!isSelectFreeTier && isEmptyPaymentMethod && (
<EmptyPaymentMethodWarning stripeCustomerId={stripeCustomerId} />
)}
</Panel.Content>
</>
)}
{currentOrg?.is_owner && (
<Panel.Content className="Form section-block--body has-inputs-centered ">
<Listbox
label="Pricing Plan"
layout="horizontal"
value={dbPricingPlan}
onChange={onDbPricingPlanChange}
// @ts-ignore
descriptionText={
<>
Select a plan that suits your needs.&nbsp;
<a className="underline" target="_blank" href="https://supabase.com/pricing">
More details
</a>
</>
}
>
{Object.entries(PRICING_PLANS).map(([k, v]) => (
<Listbox.Option key={k} label={v} value={v}>
{`${v}${v === PRICING_PLANS.PRO ? ' - $25/month' : ''}`}
</Listbox.Option>
))}
</Listbox>
{isSelectFreeTier && isOverFreeProjectLimit && <FreeProjectLimitWarning />}
{!isSelectFreeTier && isEmptyPaymentMethod && (
<EmptyPaymentMethodWarning stripeCustomerId={stripeCustomerId} />
)}
</Panel.Content>
)}
</>
</Panel>
</WizardLayout>
+134 -19
View File
@@ -1,25 +1,26 @@
import useSWR from 'swr'
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import { FC, useState } from 'react'
import { isUndefined } from 'lodash'
import { useRouter } from 'next/router'
import useSWR from 'swr'
import { FC, useState, useRef, useEffect } from 'react'
import { toJS } from 'mobx'
import { observer } from 'mobx-react-lite'
import { Typography, Input, Button, IconDownload, IconArrowRight, Tabs } from '@supabase/ui'
import { useRouter } from 'next/router'
import { debounce } from 'lodash'
import { Typography, Input, Button, IconDownload, IconArrowRight, Tabs, Modal } from '@supabase/ui'
import { API_URL } from 'lib/constants'
import { useStore, withAuth } from 'hooks'
import { get } from 'lib/common/fetch'
import { TIME_PERIODS_INFRA } from 'lib/constants'
import { pluckObjectFields } from 'lib/helpers'
import { get, patch } from 'lib/common/fetch'
import { pluckObjectFields, passwordStrength } from 'lib/helpers'
import { API_URL, DEFAULT_MINIMUM_PASSWORD_STRENGTH, TIME_PERIODS_INFRA } from 'lib/constants'
import { SettingsLayout } from 'components/layouts'
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
import Panel from 'components/to-be-cleaned/Panel'
import { ProjectUsageMinimal } from 'components/to-be-cleaned/Usage'
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
import { ProjectUsageMinimal } from 'components/to-be-cleaned/Usage'
dayjs.extend(customParseFormat)
dayjs.extend(timezone)
@@ -135,6 +136,114 @@ const Usage: FC<any> = ({ project }) => {
)
}
const ResetDbPassword: FC<any> = () => {
const { ui } = useStore()
const projectRef = ui.selectedProject?.ref
const [showResetDbPass, setShowResetDbPass] = useState<boolean>(false)
const [isUpdatingPassword, setIsUpdatingPassword] = useState<boolean>(false)
const [password, setPassword] = useState<string>('')
const [passwordStrengthMessage, setPasswordStrengthMessage] = useState<string>('')
const [passwordStrengthWarning, setPasswordStrengthWarning] = useState<string>('')
const [passwordStrengthScore, setPasswordStrengthScore] = useState<number>(0)
useEffect(() => {
if (showResetDbPass) {
setIsUpdatingPassword(false)
setPassword('')
setPasswordStrengthMessage('')
setPasswordStrengthWarning('')
setPasswordStrengthScore(0)
}
}, [showResetDbPass])
async function checkPasswordStrength(value: any) {
const { message, warning, strength } = await passwordStrength(value)
setPasswordStrengthScore(strength)
setPasswordStrengthWarning(warning)
setPasswordStrengthMessage(message)
}
const delayedCheckPasswordStrength = useRef(
debounce((value) => checkPasswordStrength(value), 300)
).current
const onDbPassChange = (e: any) => {
const value = e.target.value
setPassword(value)
if (value == '') {
setPasswordStrengthScore(-1)
setPasswordStrengthMessage('')
} else delayedCheckPasswordStrength(value)
}
const confirmResetDbPass = async () => {
if (passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
setIsUpdatingPassword(true)
const res = await patch(`${API_URL}/projects/${projectRef}/db-password`, { password })
if (!res.error) {
ui.setNotification({ category: 'success', message: res.message })
setShowResetDbPass(false)
} else {
ui.setNotification({ category: 'error', message: 'Failed to reset password' })
}
}
}
return (
<>
<Panel>
<Panel.Content>
<div className="grid grid-cols-1 lg:grid-cols-2 items-center">
<div>
<Typography.Text className="block">Database password</Typography.Text>
<div style={{ maxWidth: '420px' }}>
<Typography.Text type="secondary" className="opacity-50">
<p className="opacity-50">
You can use this password to connect directly to your Postgres database.
</p>
</Typography.Text>
</div>
</div>
<div className="flex items-end justify-end">
<Button type="default" onClick={() => setShowResetDbPass(true)}>
Reset Database Password
</Button>
</div>
</div>
</Panel.Content>
</Panel>
<Modal
title="Reset database password"
confirmText="Reset password"
alignFooter="right"
size="medium"
visible={showResetDbPass}
loading={isUpdatingPassword}
onCancel={() => setShowResetDbPass(false)}
onConfirm={() => confirmResetDbPass()}
>
<div className="w-full space-y-8 mb-8">
<Input
type="password"
onChange={onDbPassChange}
error={passwordStrengthWarning}
// @ts-ignore
descriptionText={
<PasswordStrengthBar
passwordStrengthScore={passwordStrengthScore}
passwordStrengthMessage={passwordStrengthMessage}
password={password}
/>
}
/>
</div>
</Modal>
</>
)
}
const DownloadCertificate: FC<any> = ({ createdAt }) => {
// instances before 3 : 08 pm sgt 29th April don't have certs installed
if (new Date(createdAt) < new Date('2021-04-30')) return null
@@ -142,10 +251,10 @@ const DownloadCertificate: FC<any> = ({ createdAt }) => {
return (
<Panel>
<Panel.Content>
<div className="w-full flex items-center justify-between">
<div className="grid grid-cols-1 lg:grid-cols-2 items-center">
<div>
<Typography.Text className="block">SSL Connection</Typography.Text>
<div style={{ maxWidth: '320px' }}>
<div style={{ maxWidth: '420px' }}>
<Typography.Text type="secondary" className="opacity-50">
<p className="opacity-50">
Use this cert when connecting to your database to prevent snooping and
@@ -154,13 +263,15 @@ const DownloadCertificate: FC<any> = ({ createdAt }) => {
</Typography.Text>
</div>
</div>
<Button type="default" icon={<IconDownload />}>
<a
href={`https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt`}
>
Download Certificate
</a>
</Button>
<div className="flex items-end justify-end">
<Button type="default" icon={<IconDownload />}>
<a
href={`https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt`}
>
Download Certificate
</a>
</Button>
</div>
</div>
</Panel.Content>
</Panel>
@@ -168,6 +279,9 @@ const DownloadCertificate: FC<any> = ({ createdAt }) => {
}
const GeneralSettings: FC<any> = ({ projectRef }) => {
const { ui } = useStore()
const isOwner = ui.selectedOrganization?.is_owner
const { data, error }: any = useSWR(`${API_URL}/props/project/${projectRef}/settings`, get)
if (data?.error || error) {
@@ -264,6 +378,7 @@ const GeneralSettings: FC<any> = ({ projectRef }) => {
</Panel.Content>
</Panel>
</section>
{isOwner && <ResetDbPassword />}
<DownloadCertificate createdAt={connectionInfo.inserted_at} />
</div>
<div>
@@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from 'react'
import { NextPage } from 'next'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { Typography, IconLoader, IconAlertCircle, IconRewind, Button } from '@supabase/ui'
import { Typography, IconLoader, IconAlertCircle, IconRewind, Button, IconInfo } from '@supabase/ui'
import { withAuth } from 'hooks'
import { get } from 'lib/common/fetch'
@@ -23,6 +23,7 @@ import { uuidv4 } from 'lib/helpers'
import useSWRInfinite from 'swr/infinite'
import { isUndefined } from 'lodash'
import Flag from 'components/ui/Flag/Flag'
import { useFlag } from 'hooks'
/**
* Acts as a container component for the entire log display
@@ -30,6 +31,7 @@ import Flag from 'components/ui/Flag/Flag'
*
*/
export const LogPage: NextPage = () => {
const logsCustomSql = useFlag('logsCustomSql')
const router = useRouter()
const { ref, type } = router.query
@@ -40,11 +42,13 @@ export const LogPage: NextPage = () => {
const [params, setParams] = useState({
type: '',
search_query: '',
sql: '',
where: '',
timestamp_start: '',
timestamp_end: '',
})
const title = `Logs - ${LOG_TYPE_LABEL_MAPPING[type as string]}`
const isSelectQuery = logsCustomSql && editorValue.toLowerCase().includes('select') ? true : false
useEffect(() => {
setParams({ ...params, type: type as string })
@@ -124,15 +128,24 @@ export const LogPage: NextPage = () => {
const onSelectTemplate = (template: LogTemplate) => {
setMode(template.mode)
if (template.mode === 'simple') {
setParams((prev) => ({ ...prev, search_query: template.searchString, where: '' }))
setParams((prev) => ({ ...prev, search_query: template.searchString, sql: '', where: '' }))
} else {
setEditorValue(template.searchString)
setParams((prev) => ({ ...prev, where: template.searchString, search_query: '' }))
setParams((prev) => ({
...prev,
where: isSelectQuery ? '' : template.searchString,
sql: isSelectQuery ? template.searchString : '',
search_query: '',
}))
setEditorId(uuidv4())
}
}
const handleEditorSubmit = () => {
setParams((prev) => ({ ...prev, where: editorValue }))
setParams((prev) => ({
...prev,
where: isSelectQuery ? '' : editorValue,
sql: isSelectQuery ? editorValue : '',
}))
}
const handleSearch = (v: string) => {
setParams((prev) => ({ ...prev, search_query: v || '' }))
@@ -163,21 +176,35 @@ export const LogPage: NextPage = () => {
onInputRun={handleRefresh}
/>
</div>
<div className="flex flex-row gap-x-2 justify-end p-2">
{editorValue && (
<Button
type="text"
onClick={() => {
setEditorValue('')
setEditorId(uuidv4())
}}
>
Clear
<div className="flex flex-row justify-end p-2 w-full">
<Flag name="logsCustomSql">
{isSelectQuery && (
<div className="flex flex-grow flex-row items-center gap-x-1">
{/* // we don't have a slim Alert component yet */}
<IconInfo size="tiny" />
<Typography.Text small={true} type="secondary">
Custom queries are restricted to a {type === 'database' ? '2 hour' : '7 day'}{' '}
querying window.
</Typography.Text>
</div>
)}
</Flag>
<div className="flex flex-row gap-x-2 justify-end p-2">
{editorValue && (
<Button
type="text"
onClick={() => {
setEditorValue('')
setEditorId(uuidv4())
}}
>
Clear
</Button>
)}
<Button type={editorValue ? 'secondary' : 'text'} onClick={handleEditorSubmit}>
Run
</Button>
)}
<Button type={editorValue ? 'secondary' : 'text'} onClick={handleEditorSubmit}>
Run
</Button>
</div>
</div>
</React.Fragment>
)}
@@ -201,16 +228,18 @@ export const LogPage: NextPage = () => {
<LogTable data={logData} isCustomQuery={mode === 'custom'} />
{/* Footer section of log ui, appears below table */}
<div className="p-2">
<Flag name="logsLoadOlder">
<Button
// trigger page increase
onClick={() => setSize(size + 1)}
icon={<IconRewind />}
type="secondary"
>
Load older
</Button>
</Flag>
{!isSelectQuery && (
<Flag name="logsLoadOlder">
<Button
// trigger page increase
onClick={() => setSize(size + 1)}
icon={<IconRewind />}
type="secondary"
>
Load older
</Button>
</Flag>
)}
</div>
</div>
</div>
+13 -28
View File
@@ -9,6 +9,7 @@ import { Dictionary } from '@supabase/grid'
import { useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { passwordStrength } from 'lib/helpers'
import {
PROVIDERS,
REGIONS,
@@ -22,8 +23,9 @@ import {
fetchVercelProject,
prepareVercelEvns,
} from 'components/to-be-cleaned/Integration/Vercel.utils'
import Loading from 'components/ui/Loading'
import VercelIntegrationLayout from 'components/layouts/VercelIntegrationLayout'
import Loading from 'components/ui/Loading'
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
const PASSWORD_STRENGTH = {
0: "That's terrible.",
@@ -165,6 +167,7 @@ const CreateProject = observer(() => {
const delayedCheckPasswordStrength = useRef(
debounce((value: string) => checkPasswordStrength(value), 300)
).current
const canSubmit =
projectName != '' &&
passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH &&
@@ -189,22 +192,9 @@ const CreateProject = observer(() => {
}
async function checkPasswordStrength(value: string) {
let passwordStrength = ''
if (value && value !== '') {
const response = await post(`${API_URL}/profile/password-check`, { password: value })
if (!response.error) {
const { result } = response
const score = (PASSWORD_STRENGTH as any)[result.score]
const warning = result.feedback.warning ? `${result.feedback.warning}.` : ''
const suggestions = result.feedback?.suggestions
? result.feedback.suggestions.join(' ')
: ''
passwordStrength = `${score} ${warning} ${suggestions}`
setPasswordStrengthScore(result.score)
}
}
setPasswordStrengthMessage(passwordStrength)
const { message, strength } = await passwordStrength(value)
setPasswordStrengthScore(strength)
setPasswordStrengthMessage(message)
}
async function createSupabaseProject(dbSql: string) {
@@ -294,7 +284,7 @@ const CreateProject = observer(() => {
<label htmlFor="dbPass" className="block w-full text-base normal-case">
Database password
</label>
<div className="mt-1">
<div className="mt-1 space-y-2">
<input
type="password"
name="dbPass"
@@ -304,16 +294,11 @@ const CreateProject = observer(() => {
placeholder="· · · · · · · · · · · · ·"
className="shadow-sm focus:ring-green-500 focus:border-green-500 block w-full sm:text-sm border-gray-300 rounded-md"
/>
{passwordStrengthMessage && (
<Typography.Text type="secondary">
<p className="py-2">{passwordStrengthMessage}</p>
</Typography.Text>
)}
{passwordStrengthScore >= 0 && passwordStrengthScore < DEFAULT_MINIMUM_PASSWORD_STRENGTH && (
<Typography.Text type="danger">
<p>You need a stronger password</p>
</Typography.Text>
)}
<PasswordStrengthBar
password={dbPass}
passwordStrengthScore={passwordStrengthScore}
passwordStrengthMessage={passwordStrengthMessage}
/>
</div>
</div>
<div className="py-2 pb-4">
+43 -19
View File
@@ -1,14 +1,20 @@
import LogTable from 'components/interfaces/Settings/Logs/LogTable'
import { render, fireEvent, waitFor, screen } from '@testing-library/react'
test('can display log data', async () => {
render(<LogTable data={[{
id: "seome-uuid",
timestamp: 1621323232312,
event_message: "some event happened",
metadata: {
my_key: "something_value"
}
}]} />)
render(
<LogTable
data={[
{
id: 'some-uuid',
timestamp: 1621323232312,
event_message: 'some event happened',
metadata: {
my_key: 'something_value',
},
},
]}
/>
)
await waitFor(() => screen.getByText(/happened/))
const row = screen.getByText(/happened/)
@@ -17,17 +23,35 @@ test('can display log data', async () => {
await waitFor(() => screen.getByText(/something_value/))
})
test ("dedupes log lines with exact id", async ()=>{
render(<LogTable data={[{
id: "seome-uuid",
timestamp: 1621323232312,
event_message: "some event happened",
},{
id: "seome-uuid",
timestamp: 1621323232312,
event_message: "some event happened",
}]} />)
test('dedupes log lines with exact id', async () => {
// chronological mode requires 4 columns
render(
<LogTable
data={[
{
id: 'some-uuid',
timestamp: 1621323232312,
event_message: 'some event happened',
metadata: {}
},
{
id: 'some-uuid',
timestamp: 1621323232312,
event_message: 'some event happened',
metadata: {}
},
]}
/>
)
// should only have one element, this line will fail if there are >1 element
await waitFor(() => screen.getByText(/happened/))
})
})
test('can display custom columns and headers based on data input', async () => {
render(<LogTable data={[{ some_header: 'some_data', kinda: 123456 }]} />)
await waitFor(() => screen.getByText(/some_header/))
await waitFor(() => screen.getByText(/some_data/))
await waitFor(() => screen.getByText(/kinda/))
await waitFor(() => screen.getByText(/123456/))
})
+57
View File
@@ -31,6 +31,9 @@ useMonaco.mockImplementation((v) => v)
jest.mock('components/ui/Flag/Flag')
import Flag from 'components/ui/Flag/Flag'
Flag.mockImplementation(({ children }) => <>{children}</>)
jest.mock('hooks')
import { useFlag } from 'hooks'
useFlag.mockReturnValue(true)
import { SWRConfig } from 'swr'
jest.mock('pages/project/[ref]/settings/logs/[type]')
@@ -202,6 +205,60 @@ test('where clause will trigger a log refresh', async () => {
await waitFor(() => screen.getByText(/happened/))
})
test('custom sql querying', async () => {
get.mockImplementation((url) => {
if (url.includes('sql=') && url.includes('select')) {
return {
data: [
{
my_count: 12345,
},
],
}
}
return { data: [] }
})
const { container } = render(<LogPage />)
let editor = container.querySelector('.monaco-editor')
expect(editor).toBeFalsy()
// TODO: abstract this out into a toggle selection helper
const toggle = getToggleByText(/via query/)
expect(toggle).toBeTruthy()
userEvent.click(toggle)
// type into the query editor
await waitFor(() => {
editor = container.querySelector('.monaco-editor')
expect(editor).toBeTruthy()
})
editor = container.querySelector('.monaco-editor')
userEvent.type(editor, 'select count(*) as my_count from edge_logs')
// should show sandbox warning alert
await waitFor(() => screen.getByText(/restricted to a 7 day querying window/))
// should trigger query
userEvent.click(screen.getByText('Run'))
await waitFor(
() => {
expect(get).toHaveBeenCalledWith(expect.stringContaining('sql='))
expect(get).toHaveBeenCalledWith(expect.stringContaining('select'))
expect(get).toHaveBeenCalledWith(expect.stringContaining('edge_logs'))
expect(get).not.toHaveBeenCalledWith(expect.stringContaining('where'))
},
{ timeout: 1000 }
)
await waitFor(() => screen.getByText(/my_count/)) //column header
await waitFor(() => screen.getByText(/12345/)) // row value
// clicking on the row value should not show log selection panel
userEvent.click(screen.getByText(/12345/))
await waitFor(() => expect(() => screen.getByText(/Metadata/)).toThrow())
// should not see chronological features
await waitFor(() => screen.queryByText(/Load older/)) //column header
})
test('load older btn will fetch older logs', async () => {
get.mockImplementation((url) => {
if (url.includes('count')) {
+3 -3
View File
@@ -17,7 +17,7 @@ Apple OAuth consists of six broad steps:
- Obtaining a `Services Id` - this will serve as the `client_id`.
- Obtaining a `secret key` that will be used to get our `client_secret`.
- Generating the `client_secret` using the `secret key`.
- Add your `client id` and `client secret` keys to your [Supabase Project](https://supabase.com).
- Add your `client id` and `client secret` keys to your [Supabase Project](https://app.supabase.io).
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js).
## Steps
@@ -64,7 +64,7 @@ The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
@@ -170,7 +170,7 @@ puts token
### Add your OAuth credentials to Supabase
- Go to your [Supabase Dashboard](https://supabase.com).
- Go to your [Supabase Dashboard](https://app.supabase.io).
- In the left sidebar, click the `Authentication` icon (near the top).
- Click `Settings` from the list to go to the `Authentication Settings` page.
- Enter the final (hosted) URL of your app under `Site URL` (this is important).
+3 -3
View File
@@ -14,7 +14,7 @@ To enable Bitbucket Auth for your project, you need to set up a BitBucket OAuth
Setting up Bitbucket logins for your application consists of 3 parts:
- Create and configure a Bitbucket OAuth Consumer on [Bitbucket](https://bitbucket.org)
- Add your Bitbucket OAuth Consumer keys to your [Supabase Project](https://supabase.com)
- Add your Bitbucket OAuth Consumer keys to your [Supabase Project](https://app.supabase.io)
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js)
## Steps
@@ -32,7 +32,7 @@ The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
@@ -59,7 +59,7 @@ The next step requires a callback URL, which looks like this:
### Add your Bitbucket credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://supabase.com)
- Go to your [Supabase Project Dashboard](https://app.supabase.io)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
+3 -3
View File
@@ -14,7 +14,7 @@ To enable Discord Auth for your project, you need to set up a Discord Applicatio
Setting up Discord logins for your application consists of 3 parts:
- Create and configure a Discord Application [Discord Developer Portal](https://discord.com/developers)
- Add your Discord OAuth Consumer keys to your [Supabase Project](https://supabase.com)
- Add your Discord OAuth Consumer keys to your [Supabase Project](https://app.supabase.io)
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js)
## Steps
@@ -36,7 +36,7 @@ In the next step you require a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
@@ -58,7 +58,7 @@ In the next step you require a callback URL, which looks like this:
### Add your Discord credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://supabase.com)
- Go to your [Supabase Project Dashboard](https://app.supabase.io)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
+3 -3
View File
@@ -14,7 +14,7 @@ To enable Facebook Auth for your project, you need to set up a Facebook OAuth ap
Setting up Facebook logins for your application consists of 3 parts:
- Create and configure a Facebook Application on the [Facebook Developers Site](https://developers.facebook.com)
- Add your Facebook keys to your [Supabase Project](https://supabase.com)
- Add your Facebook keys to your [Supabase Project](https://app.supabase.io)
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js)
## Steps
@@ -40,7 +40,7 @@ The next step requires a callback URI, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
@@ -76,7 +76,7 @@ You can read more about access levels [here](https://developers.facebook.com/doc
### Enter your Facebook App ID and Secret into your Supabase Project
- Go to your [Supabase Project Dashboard](https://supabase.com)
- Go to your [Supabase Project Dashboard](https://app.supabase.io)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
+3 -3
View File
@@ -14,7 +14,7 @@ To enable GitHub Auth for your project, you need to set up a GitHub OAuth applic
Setting up GitHub logins for your application consists of 3 parts:
- Create and configure a GitHub OAuth App on [GitHub](https://github.com)
- Add your GitHub OAuth keys to your [Supabase Project](https://supabase.com)
- Add your GitHub OAuth keys to your [Supabase Project](https://app.supabase.io)
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js)
## Steps
@@ -41,7 +41,7 @@ The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
@@ -69,7 +69,7 @@ Copy your new OAuth credentials
### Enter your GitHub credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://supabase.com)
- Go to your [Supabase Project Dashboard](https://app.supabase.io)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
+3 -3
View File
@@ -14,7 +14,7 @@ To enable GitLab Auth for your project, you need to set up a GitLab OAuth applic
Setting up GitLab logins for your application consists of 3 parts:
- Create and configure a GitLab Application on [GitLab](https://gitlab.com)
- Add your GitLab Application keys to your [Supabase Project](https://supabase.com)
- Add your GitLab Application keys to your [Supabase Project](https://app.supabase.io)
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js)
## Steps
@@ -32,7 +32,7 @@ The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
@@ -56,7 +56,7 @@ The next step requires a callback URL, which looks like this:
### Add your GitLab credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- In the left sidebar, click the `Authentication` icon (near the top).
- Click `Settings` from the list to go to the `Authentication Settings` page.
- Enter the final (hosted) URL of your app under `Site URL` (this is important).
+3 -3
View File
@@ -14,7 +14,7 @@ To enable Google Auth for your project, you need to set up a Google OAuth applic
Setting up Google logins for your application consists of 3 parts:
- Create and configure a Google Project on the [Google Cloud Platform Console](https://console.cloud.google.com/home/dashboard)
- Add your Google OAuth keys to your [Supabase Project](https://supabase.com)
- Add your Google OAuth keys to your [Supabase Project](https://app.supabase.io)
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js)
## Steps
@@ -55,7 +55,7 @@ The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
@@ -84,7 +84,7 @@ Copy your new OAuth credentials
### Enter your Google credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://supabase.com)
- Go to your [Supabase Project Dashboard](https://app.supabase.io)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
+3 -3
View File
@@ -14,7 +14,7 @@ To enable Slack Auth for your project, you need to set up a Slack OAuth applicat
Setting up Slack logins for your application consists of 3 parts:
- Create and configure a Slack Project and App on the [Slack Developer Dashboard](https://api.slack.com/apps).
- Add your Slack `API Key` and `API Secret Key` to your [Supabase Project](https://supabase.com).
- Add your Slack `API Key` and `API Secret Key` to your [Supabase Project](https://app.supabase.io).
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js).
## Steps
@@ -32,7 +32,7 @@ The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
@@ -68,7 +68,7 @@ Under `Redirect URLs`:
### Enter your Slack credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- In the left sidebar, click the `Authentication` icon (near the top).
- Click `Settings` from the list to go to the `Authentication Settings` page.
- Enter the final (hosted) URL of your app under `Site URL` (this is important).
+3 -3
View File
@@ -14,7 +14,7 @@ To enable Spotify Auth for your project, you need to set up a Spotify OAuth appl
Setting up Spotify logins for your application consists of 3 parts:
- Create and configure a Spotify Project and App on the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/).
- Add your Spotify `API Key` and `API Secret Key` to your [Supabase Project](https://supabase.com).
- Add your Spotify `API Key` and `API Secret Key` to your [Supabase Project](https://app.supabase.io).
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js).
## Steps
@@ -32,7 +32,7 @@ The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
@@ -63,7 +63,7 @@ Under `Redirect URIs`:
### Enter your Spotify credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- In the left sidebar, click the `Authentication` icon (near the top).
- Click `Settings` from the list to go to the `Authentication Settings` page.
- Enter the final (hosted) URL of your app under `Site URL` (this is important).
+3 -3
View File
@@ -14,7 +14,7 @@ To enable Twitch Auth for your project, you need to set up a Twitch Application
Setting up Twitch logins for your application consists of 3 parts:
- Create and configure a Twitch Application [Twitch Developer Console](https://dev.twitch.tv/console)
- Add your Twitch OAuth Consumer keys to your [Supabase Project](https://supabase.com)
- Add your Twitch OAuth Consumer keys to your [Supabase Project](https://app.supabase.io)
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js)
## Steps
@@ -37,7 +37,7 @@ In the next step you require a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
@@ -74,7 +74,7 @@ In the next step you require a callback URL, which looks like this:
### Add your Twitch credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://supabase.com)
- Go to your [Supabase Project Dashboard](https://app.supabase.io)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
+3 -3
View File
@@ -14,7 +14,7 @@ To enable Twitter Auth for your project, you need to set up a Twitter OAuth appl
Setting up Twitter logins for your application consists of 3 parts:
- Create and configure a Twitter Project and App on the [Twitter Developer Dashboard](https://developer.twitter.com/en/portal/dashboard).
- Add your Twitter `API Key` and `API Secret Key` to your [Supabase Project](https://supabase.com).
- Add your Twitter `API Key` and `API Secret Key` to your [Supabase Project](https://app.supabase.io).
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js).
## Steps
@@ -32,7 +32,7 @@ The next step requires a callback URL, which looks like this:
`https://<project-ref>.supabase.co/auth/v1/callback`
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
@@ -64,7 +64,7 @@ The next step requires a callback URL, which looks like this:
### Enter your Twitter credentials into your Supabase Project
- Go to your [Supabase Project Dashboard](https://supabase.com).
- Go to your [Supabase Project Dashboard](https://app.supabase.io).
- In the left sidebar, click the `Authentication` icon (near the top).
- Click `Settings` from the list to go to the `Authentication Settings` page.
- Enter the final (hosted) URL of your app under `Site URL` (this is important).
+16
View File
@@ -392,6 +392,22 @@ export default function IndexPage() {
<div className="bg-gray-50 dark:bg-gray-800 border-t dark:border-gray-600">
<div className="container mx-auto px-6 lg:px-16 xl:px-20 relative py-16 sm:py-18 md:py-24 lg:py-24">
<Typography.Title level={2}>Frequently asked questions</Typography.Title>
<Typography className="w-5/12 mb-4">
Can&apos;t find the answer to your question, ask someone in the community either on our
Discord or Github.
</Typography>
<Link href="https://discord.supabase.com">
<a>
<Button type="default" className="mr-2" size="small">
Discord
</Button>
</a>
</Link>
<Link href="https://github.com/supabase/supabase/discussions">
<a>
<Button size="small">Github</Button>
</a>
</Link>
<div className="mt-16">
<div className="grid grid-cols-2 gap-y-10 gap-x-10">
{pricingFaq.map((faq) => {