mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 07:09:07 +03:00
34 files changed
+669
-1226
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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')
|
||||
|
||||
@@ -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 }
|
||||
@@ -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,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}`
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Generated
+37
-854
File diff suppressed because it is too large.
Load diff
+1
-2
@@ -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
@@ -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
@@ -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.
|
||||
<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.
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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/))
|
||||
})
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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'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) => {
|
||||
|
||||
Reference in new issue
Block a user