mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Merge branch 'master' of github.com:supabase/supabase
This commit is contained in:
commit
1cda2c3ce0
40 files changed
+1130
-672
No files matched your search
@@ -1,53 +1,21 @@
|
||||
import providers from '../data/authProviders'
|
||||
import ButtonCard from './ButtonCard'
|
||||
import { IconPanel } from 'ui'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function AuthProviders({ type }: { type: string }) {
|
||||
const filterProviders = providers.filter((item) => item.authType === type)
|
||||
|
||||
export default function AuthProviders() {
|
||||
return (
|
||||
<div className="grid md:grid-cols-12 gap-4">
|
||||
{providers.map((x) => (
|
||||
<div key={x.name} className="col-span-6">
|
||||
<ButtonCard to={x.href} title={x.name}>
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex justify-between">
|
||||
<p className="mt-0">{x.name}</p>
|
||||
<p className="mt-0">
|
||||
{x.official ? (
|
||||
<span className={`badge badge--official`}>Official</span>
|
||||
) : (
|
||||
<span className={`badge badge--unofficial`}>Unofficial</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||||
<div
|
||||
className="code-block"
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
<span>Platform:</span>
|
||||
<span>{x.platform.toString()}</span>
|
||||
</div>
|
||||
<div
|
||||
className="code-block"
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
<span>Self-Hosted:</span>
|
||||
<span>{x.selfHosted.toString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<div className="grid grid-cols-12 gap-10 not-prose py-8">
|
||||
{filterProviders.map((x) => (
|
||||
<Link href={`${x.href}`} key={x.name} passHref>
|
||||
<a className="col-span-6 lg:col-span-4 xl:col-span-3">
|
||||
<IconPanel title={x.name} icon={x.logo} />
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -30,12 +30,12 @@ const questions = [
|
||||
'How do I connect to my database?',
|
||||
'How do I run migrations? ',
|
||||
'How do I listen to changes in a table?',
|
||||
'How do I setup authentication?',
|
||||
'How do I set up authentication?',
|
||||
]
|
||||
|
||||
function getEdgeFunctionUrl() {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL.replace(/\/$/, '')
|
||||
const isPlatform = supabaseUrl.match(/(supabase\.co)|(supabase\.in)/)
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '')
|
||||
const isPlatform = supabaseUrl?.match(/(supabase\.co)|(supabase\.in)/)
|
||||
|
||||
if (isPlatform) {
|
||||
const [schemeAndProjectId, domain, tld] = supabaseUrl.split('.')
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import createConfigCatClient from 'configcat-js'
|
||||
// import createConfigCatClient from 'configcat-js'
|
||||
import FlagContext from './FlagContext'
|
||||
|
||||
const FlagProvider: FC = ({ children }) => {
|
||||
const [store, setStore] = useState({})
|
||||
|
||||
const { Provider } = FlagContext
|
||||
|
||||
useEffect(() => {
|
||||
getFlags()
|
||||
}, [])
|
||||
// useEffect(() => {
|
||||
// getFlags()
|
||||
// }, [])
|
||||
|
||||
const getFlags = async () => {
|
||||
const setFlagValues = async () => {
|
||||
const flagValues = await client.getAllValuesAsync()
|
||||
const flagStore: any = {}
|
||||
// const getFlags = async () => {
|
||||
// const setFlagValues = async () => {
|
||||
// const flagValues = await client.getAllValuesAsync()
|
||||
// const flagStore: any = {}
|
||||
|
||||
flagValues.forEach((item: any) => {
|
||||
flagStore[item.settingKey] = item.settingValue
|
||||
})
|
||||
setStore(flagStore)
|
||||
}
|
||||
// flagValues.forEach((item: any) => {
|
||||
// flagStore[item.settingKey] = item.settingValue
|
||||
// })
|
||||
// setStore(flagStore)
|
||||
// }
|
||||
|
||||
const client = createConfigCatClient(process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '', {
|
||||
configChanged: setFlagValues,
|
||||
pollIntervalSeconds: 600,
|
||||
})
|
||||
// const client = createConfigCatClient(process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '', {
|
||||
// configChanged: setFlagValues,
|
||||
// pollIntervalSeconds: 600,
|
||||
// })
|
||||
|
||||
await setFlagValues()
|
||||
}
|
||||
// await setFlagValues()
|
||||
// }
|
||||
|
||||
return <Provider value={store}>{children}</Provider>
|
||||
}
|
||||
|
||||
@@ -658,6 +658,11 @@ export const resources = {
|
||||
url: '/guides/resources/migrating-to-supabase/heroku',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Render',
|
||||
url: '/guides/resources/migrating-to-supabase/render',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -720,6 +725,7 @@ export const migrate = {
|
||||
{ name: 'Firestore Data', url: '/guides/migrations/firestore-data', items: [] },
|
||||
{ name: 'Firebase Storage', url: '/guides/migrations/firebase-storage', items: [] },
|
||||
{ name: 'Heroku', url: '/guides/migrations/heroku', items: [] },
|
||||
{ name: 'Render', url: '/guides/migrations/render', items: [] },
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,172 +1,193 @@
|
||||
const authProviders = [
|
||||
{
|
||||
name: 'Apple',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/apple-icon',
|
||||
href: '/guides/auth/social-login/auth-apple',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Azure',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/microsoft-icon',
|
||||
href: '/guides/auth/social-login/auth-azure',
|
||||
official: false,
|
||||
supporter: 'TBD',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Bitbucket',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/bitbucket-icon',
|
||||
href: '/guides/auth/social-login/auth-bitbucket',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Discord',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/discord-icon',
|
||||
href: '/guides/auth/social-login/auth-discord',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Facebook',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/facebook-icon',
|
||||
href: '/guides/auth/social-login/auth-facebook',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/github-icon',
|
||||
href: '/guides/auth/social-login/auth-github',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'GitLab',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/gitlab-icon',
|
||||
href: '/guides/auth/social-login/auth-gitlab',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Google',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/google-icon',
|
||||
href: '/guides/auth/social-login/auth-google',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Keycloak',
|
||||
logo: '/docs/img/icons/keycloak-icon',
|
||||
href: '/guides/auth/social-login/auth-keycloak',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'LinkedIn',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/linkedin-icon',
|
||||
href: '/guides/auth/social-login/auth-linkedin',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'MessageBird',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/messagebird-icon',
|
||||
href: '/guides/auth/phone-login/messagebird',
|
||||
official: false,
|
||||
supporter: 'MessageBird',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'phone'
|
||||
},
|
||||
{
|
||||
name: 'Notion',
|
||||
// logo: '/img/libraries/notion-icon.svg',
|
||||
logo: '/docs/img/icons/notion-icon',
|
||||
href: '/guides/auth/social-login/auth-notion',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Slack',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/slack-icon',
|
||||
href: '/guides/auth/social-login/auth-slack',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Spotify',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/spotify-icon',
|
||||
href: '/guides/auth/social-login/auth-spotify',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Twitter',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/twitter-icon',
|
||||
href: '/guides/auth/social-login/auth-twitter',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Twitch',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/twitch-icon',
|
||||
href: '/guides/auth/social-login/auth-twitch',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Zoom',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/zoom-icon',
|
||||
href: '/guides/auth/social-login/auth-zoom',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'social',
|
||||
},
|
||||
{
|
||||
name: 'Twilio',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
logo: '/docs/img/icons/twilio-icon',
|
||||
href: '/guides/auth/phone-login/twilio',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'phone',
|
||||
},
|
||||
{
|
||||
name: 'Vonage',
|
||||
logo: '/docs/img/icons/vonage-icon',
|
||||
href: '/guides/auth/phone-login/vonage',
|
||||
official: false,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
selfHosted: true,
|
||||
authType: 'phone',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@ Wrap an Action to check that the user has a valid session. If they're not logged
|
||||
```ts title=src/routes/posts/+page.server.ts
|
||||
import type { Actions } from './$types'
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
|
||||
import { error, invalid } from '@sveltejs/kit'
|
||||
import { error, fail } from '@sveltejs/kit'
|
||||
|
||||
export const actions: Actions = {
|
||||
createPost: async (event) => {
|
||||
@@ -276,7 +276,7 @@ export const actions: Actions = {
|
||||
.insert({ content })
|
||||
|
||||
if (createPostError) {
|
||||
return invalid(500, {
|
||||
return fail(500, {
|
||||
supabaseErrorMessage: createPostError.message,
|
||||
})
|
||||
}
|
||||
@@ -293,7 +293,7 @@ If you try to submit a form with the action `?/createPost` without a valid sessi
|
||||
|
||||
```ts
|
||||
import type { Actions } from './$types'
|
||||
import { invalid, redirect } from '@sveltejs/kit'
|
||||
import { fail, redirect } from '@sveltejs/kit'
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
|
||||
import { AuthApiError } from '@supabase/supabase-js'
|
||||
|
||||
@@ -313,14 +313,14 @@ export const actions: Actions = {
|
||||
|
||||
if (error) {
|
||||
if (error instanceof AuthApiError && error.status === 400) {
|
||||
return invalid(400, {
|
||||
return fail(400, {
|
||||
error: 'Invalid credentials.',
|
||||
values: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
}
|
||||
return invalid(500, {
|
||||
return fail(500, {
|
||||
error: 'Server error. Try again later.',
|
||||
values: {
|
||||
email,
|
||||
|
||||
@@ -44,8 +44,16 @@ You can authenticate your users in several ways:
|
||||
|
||||
We provide a suite of Providers and login methods, as well as [Auth helpers](/docs/guides/auth/auth-helpers/).
|
||||
|
||||
#### Social Auth
|
||||
|
||||
<div className="container" style={{ padding: 0 }}>
|
||||
<AuthProviders />
|
||||
<AuthProviders type="social" />
|
||||
</div>
|
||||
|
||||
#### Phone Auth
|
||||
|
||||
<div className="container" style={{ padding: 0 }}>
|
||||
<AuthProviders type="phone" />
|
||||
</div>
|
||||
|
||||
### Configure third-party providers
|
||||
|
||||
@@ -135,6 +135,12 @@ export const migrationGuides = [
|
||||
href: '/guides/resources/migrating-to-supabase/heroku',
|
||||
description: 'Migrate your Heroku Postgres database to Supabase.',
|
||||
},
|
||||
{
|
||||
title: 'Render',
|
||||
icon: '/docs/img/icons/render-icon',
|
||||
href: '/guides/resources/migrating-to-supabase/render',
|
||||
description: 'Migrate your Render Postgres database to Supabase.',
|
||||
},
|
||||
]
|
||||
|
||||
export const postgres = [
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Migrate from Render to Supabase',
|
||||
description: 'Migrate your Render Postgres database to Supabase.',
|
||||
}
|
||||
|
||||
Render is a popular Web Hosting service in the online services category that also has a managed Postgres service. Render has a great developer experience, allowing users to deploy straight from GitHub or GitLab. This is the core of their product and they do it really well. However, when it comes to Postgres databases, it may not be the best option.
|
||||
|
||||
Supabase is one of the best free alternative to Render Postgres. Supabase provide all the backend features developers need to build a product: a Postgres database, authentication, instant APIs, edge functions, realtime subscriptions, and storage. Postgres is the core of Supabase—for example, you can use row-level security and there are more than 40 Postgres extensions available.
|
||||
|
||||
This guide demonstrates how to migrate from Render to Supabase to get the most out of Postgres while gaining access to all the features you need to build a project.
|
||||
|
||||
## Retrieve your Render database credentials [#retrieve-render-credentials]
|
||||
|
||||
1. Log in to your [Render account](https://render.com) and select the project you want to migrate.
|
||||
1. Click **Dashboard** in the menu and click in your **Postgres** database.
|
||||
1. Scroll down in the **Info** tab.
|
||||
1. Click on **PSQL Command** and edit it adding the content after `PSQL_COMMAND=`.
|
||||
|
||||

|
||||
Example:
|
||||
|
||||
```bash
|
||||
%env PSQL_COMMAND=PGPASSWORD=RgaMDfTS_password_FTPa7 psql -h dpg-a_server_in.oregon-postgres.render.com -U my_db_pxl0_user my_db_pxl0
|
||||
```
|
||||
|
||||
## Retrieve your Supabase Host [#retrieve-supabase-host]
|
||||
|
||||
1. If you're new to Supabase, [create a project](https://app.supabase.com).
|
||||
1. Go to the [Database settings](https://app.supabase.com/project/_/settings/database) for your project in the Supabase Dashboard.
|
||||
1. Under **Connection Info**, note your Host (`$SUPABASE_HOST`).
|
||||
1. Save your password or [reset it](https://app.supabase.com/project/_/settings/database) if you forgot it.
|
||||
|
||||

|
||||
|
||||
## Migrate the database
|
||||
|
||||
The fastest way to migrate your database is with the Supabase migration tool on [Google Colab](https://colab.research.google.com/github/mansueli/Supa-Migrate/blob/main/Migrate_Postgres_Supabase.ipynb). Alternatively, you can use the [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) and [psql](https://www.postgresql.org/docs/current/app-psql.html) command line tools, which are included in a full PostgreSQL installation.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="colab"
|
||||
>
|
||||
<TabPanel id="colab" label="Migrate using Colab">
|
||||
|
||||
1. Set the environment variables (`PSQL_COMMAND`, `SUPABASE_HOST`, `SUPABASE_PASSWORD`) in the Colab notebook.
|
||||
1. Run the first two steps in [the notebook](https://colab.research.google.com/github/mansueli/Supa-Migrate/blob/main/Migrate_Postgres_Supabase.ipynb) in order. The first sets the variables and the second installs PSQL and the migration script.
|
||||
1. Run the third step to start the migration. This will take a few minutes.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="cli" label="Migrate using CLI tools">
|
||||
|
||||
### Export your Render database to a file in console [#export-render-database]
|
||||
|
||||
Use `pg_dump` with your Render credentials to export your Render database to a file (e.g., `render_dump.sql`).
|
||||
|
||||
```bash
|
||||
pg_dump --clean --if-exists --quote-all-identifiers \
|
||||
-h $RENDER_HOST -U $RENDER_USER -d $RENDER_DATABASE \
|
||||
--no-owner --no-privileges > render_dump.sql
|
||||
```
|
||||
|
||||
### Import the database to your Supabase project [#import-database-to-supabase]
|
||||
|
||||
Use `psql` to import the Render database file to your Supabase project.
|
||||
|
||||
```bash
|
||||
psql -h $SUPA_URL -U postgres --file render_dump.sql -p 6543 -d postgres
|
||||
```
|
||||
|
||||
### Additional options
|
||||
|
||||
- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command.
|
||||
- To exclude a schema: `--exclude-schema=PATTERN`.
|
||||
- To only migrate a single table: `--table=PATTERN`.
|
||||
- To exclude a table: `--exclude-table=PATTERN`.
|
||||
|
||||
Run `pg_dump --help` for a full list of options.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 789 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 764 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="17.4653" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
|
||||
<rect x="13.1709" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
|
||||
<rect x="13.1709" y="0.828125" width="2.81974" height="2.81974" fill="#46E7AA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.285156 7.2979C0.285156 3.72312 3.18309 0.825195 6.75786 0.825195H11.6109V3.65042H6.75786C4.74341 3.65042 3.11038 5.28345 3.11038 7.2979V16.259H0.285156V7.2979Z" fill="#46E7AA"/>
|
||||
<rect x="8.87671" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
|
||||
<rect x="4.58228" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
|
||||
<rect x="0.288086" y="17.6948" width="2.81974" height="2.81974" fill="#46E7AA"/>
|
||||
<rect x="17.4653" y="13.478" width="2.81974" height="2.81974" fill="#46E7AA"/>
|
||||
<rect x="17.4653" y="9.26123" width="2.81974" height="2.81974" fill="#46E7AA"/>
|
||||
<rect x="17.4653" y="5.04492" width="2.81974" height="2.81974" fill="#46E7AA"/>
|
||||
<rect x="17.4653" y="0.828125" width="2.81974" height="2.81974" fill="#46E7AA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -7,6 +7,7 @@
|
||||
.DocSearch-Container {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
@apply fixed top-0 left-0 z-[200] flex h-screen w-screen flex-col p-4 backdrop-blur-sm sm:p-6 md:p-[10vh] lg:p-[12vh];
|
||||
@apply cursor-auto #{!important};
|
||||
}
|
||||
|
||||
.DocSearch-LoadingIndicator svg {
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 667 KiB |
@@ -2,7 +2,7 @@ import { Application, Router } from 'https://deno.land/x/oak/mod.ts'
|
||||
|
||||
const router = new Router()
|
||||
router
|
||||
// Note: path will be prefixed with function name
|
||||
// Note: path should be prefixed with function name
|
||||
.get('/oak-server', (context) => {
|
||||
context.response.body = 'This is an example Oak server running on Edge Functions!'
|
||||
})
|
||||
|
||||
Generated
+37
-30
@@ -14115,20 +14115,6 @@
|
||||
"resolved": "packages/config",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/configcat-common": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-5.3.0.tgz",
|
||||
"integrity": "sha512-yM9SNuG8XQa0aHNK5eUhtzAD+a6yFEOQw0qg6f/ZvIWGHkY+7FYvDbSkvMHW1ERQaUmAIt2QLZB6rMJzFCDCmg=="
|
||||
},
|
||||
"node_modules/configcat-js": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-5.10.0.tgz",
|
||||
"integrity": "sha512-ta827/ONGd9sIKIRItQPS+URTb90BYjy+YXv5F0dF9T0X7tUZWt0b9V4Liy2Vlq+VaJR3DqrjyZyeD9vG7Z0Bg==",
|
||||
"deprecated": "Please upgrade to version 6 or higher.",
|
||||
"dependencies": {
|
||||
"configcat-common": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/console-browserify": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
|
||||
@@ -38931,7 +38917,7 @@
|
||||
"clipboard": "^2.0.8",
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
"configcat-js": "^5.6.0",
|
||||
"configcat-js": "^7.0.0",
|
||||
"dayjs": "^1.11.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"generate-password": "^1.7.0",
|
||||
@@ -39196,6 +39182,23 @@
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/configcat-common": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-7.0.1.tgz",
|
||||
"integrity": "sha512-9SxX4/6NzK1l+2iDWLyr/0tsli/eKzSJhBIepptF+GPy/7CprnA0coQz8fF8Gx1IF3wBZsJg0vRaC3LtNa0vRg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/configcat-js": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-7.0.0.tgz",
|
||||
"integrity": "sha512-ECVS5rV4wzU+r+81Fc1Rho1pvbSMpZXUKpO/Ei0Ms+Qmo21U8x1NVMWF0hIplHmDdEsCtEIa5AgXVceDURGAuA==",
|
||||
"dependencies": {
|
||||
"configcat-common": "^7.0.1",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"studio/node_modules/d3-array": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
|
||||
@@ -46181,7 +46184,7 @@
|
||||
"@sinclair/typebox": "^0.25.1",
|
||||
"pg": "^8.7.1",
|
||||
"pg-format": "^1.0.4",
|
||||
"pgsql-parser": "^13.3.0",
|
||||
"pgsql-parser": "13.4.0",
|
||||
"postgres-array": "^3.0.1",
|
||||
"prettier": "^2.6.0",
|
||||
"prettier-plugin-sql": "^0.12.1"
|
||||
@@ -49533,19 +49536,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"configcat-common": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-5.3.0.tgz",
|
||||
"integrity": "sha512-yM9SNuG8XQa0aHNK5eUhtzAD+a6yFEOQw0qg6f/ZvIWGHkY+7FYvDbSkvMHW1ERQaUmAIt2QLZB6rMJzFCDCmg=="
|
||||
},
|
||||
"configcat-js": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-5.10.0.tgz",
|
||||
"integrity": "sha512-ta827/ONGd9sIKIRItQPS+URTb90BYjy+YXv5F0dF9T0X7tUZWt0b9V4Liy2Vlq+VaJR3DqrjyZyeD9vG7Z0Bg==",
|
||||
"requires": {
|
||||
"configcat-common": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"console-browserify": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
|
||||
@@ -65797,7 +65787,7 @@
|
||||
"clipboard": "^2.0.8",
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
"configcat-js": "^5.6.0",
|
||||
"configcat-js": "^7.0.0",
|
||||
"dayjs": "^1.11.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"generate-password": "^1.7.0",
|
||||
@@ -65960,6 +65950,23 @@
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"configcat-common": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-7.0.1.tgz",
|
||||
"integrity": "sha512-9SxX4/6NzK1l+2iDWLyr/0tsli/eKzSJhBIepptF+GPy/7CprnA0coQz8fF8Gx1IF3wBZsJg0vRaC3LtNa0vRg==",
|
||||
"requires": {
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"configcat-js": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/configcat-js/-/configcat-js-7.0.0.tgz",
|
||||
"integrity": "sha512-ECVS5rV4wzU+r+81Fc1Rho1pvbSMpZXUKpO/Ei0Ms+Qmo21U8x1NVMWF0hIplHmDdEsCtEIa5AgXVceDURGAuA==",
|
||||
"requires": {
|
||||
"configcat-common": "^7.0.1",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"d3-array": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
|
||||
|
||||
@@ -18,8 +18,7 @@ function classNames(...classes: any) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export interface Props
|
||||
extends Omit<React.InputHTMLAttributes<HTMLButtonElement>, 'size'> {
|
||||
export interface Props extends Omit<React.InputHTMLAttributes<HTMLButtonElement>, 'size'> {
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
descriptionText?: string
|
||||
@@ -73,14 +72,8 @@ function Listbox({
|
||||
|
||||
const triggerRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const {
|
||||
formContextOnChange,
|
||||
values,
|
||||
errors,
|
||||
handleBlur,
|
||||
touched,
|
||||
fieldLevelValidation,
|
||||
} = useFormContext()
|
||||
const { formContextOnChange, values, errors, handleBlur, touched, fieldLevelValidation } =
|
||||
useFormContext()
|
||||
|
||||
if (values && !value) {
|
||||
defaultValue = values[id || name]
|
||||
@@ -257,9 +250,7 @@ function Listbox({
|
||||
className={__styles.options_container}
|
||||
>
|
||||
<div>
|
||||
<SelectContext.Provider
|
||||
value={{ onChange: handleOnChange, selected }}
|
||||
>
|
||||
<SelectContext.Provider value={{ onChange: handleOnChange, selected }}>
|
||||
{children}
|
||||
</SelectContext.Provider>
|
||||
</div>
|
||||
@@ -313,9 +304,7 @@ function SelectOption({
|
||||
<div className={__styles.option_inner}>
|
||||
{addOnBefore && addOnBefore({ active, selected })}
|
||||
<span>
|
||||
{typeof children === 'function'
|
||||
? children({ active, selected })
|
||||
: children}
|
||||
{typeof children === 'function' ? children({ active, selected }) : children}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -326,10 +315,7 @@ function SelectOption({
|
||||
active ? __styles.option_check_active : ''
|
||||
)}
|
||||
>
|
||||
<IconCheck
|
||||
className={__styles.option_check_icon}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<IconCheck className={__styles.option_check_icon} aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</DropdownMenuPrimitive.Item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import { Button, IconExternalLink } from 'ui'
|
||||
|
||||
interface Props {
|
||||
showTemplates: boolean
|
||||
@@ -8,15 +8,20 @@ interface Props {
|
||||
}
|
||||
|
||||
const PolicyEditorFooter: FC<Props> = ({ showTemplates, onViewTemplates, onReviewPolicy }) => (
|
||||
<div className="flex w-full items-center justify-end gap-2 border-t px-6 py-4 dark:border-dark">
|
||||
{showTemplates && (
|
||||
<Button type="default" onClick={onViewTemplates}>
|
||||
View templates
|
||||
<div className="flex justify-between items-center border-t px-6 py-4 dark:border-dark">
|
||||
<a href='https://supabase.com/docs/learn/auth-deep-dive/auth-policies' target="_blank">
|
||||
<Button type='link' icon={<IconExternalLink size={14} strokeWidth={1.5} />}>Documentation</Button>
|
||||
</a>
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
{showTemplates && (
|
||||
<Button type="default" onClick={onViewTemplates}>
|
||||
View templates
|
||||
</Button>
|
||||
)}
|
||||
<Button type="primary" onClick={onReviewPolicy}>
|
||||
Review
|
||||
</Button>
|
||||
)}
|
||||
<Button type="primary" onClick={onReviewPolicy}>
|
||||
Review
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -130,8 +130,9 @@ export const USAGE_BASED_PRODUCTS = [
|
||||
costPerUnit: 0.1,
|
||||
tooltip: (
|
||||
<span>
|
||||
Every single serverless function invocation independent of response status is counted.
|
||||
Billing is based on the sum of all invocations throughout your billing period.
|
||||
We continuously monitor the amount of serverless functions in your project. Billing is
|
||||
based on the maximum amount of functions at any point in time throughout your billing
|
||||
period.
|
||||
</span>
|
||||
),
|
||||
},
|
||||
@@ -144,9 +145,8 @@ export const USAGE_BASED_PRODUCTS = [
|
||||
costPerUnit: 0.000002,
|
||||
tooltip: (
|
||||
<span>
|
||||
We continuously monitor the amount of serverless functions in your project. Billing is
|
||||
based on the maximum amount of functions at any point in time throughout your billing
|
||||
period.
|
||||
Every single serverless function invocation independent of response status is counted.
|
||||
Billing is based on the sum of all invocations throughout your billing period.
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import { FC } from 'react'
|
||||
import { Input, Tabs } from 'ui'
|
||||
import { pluckObjectFields } from 'lib/helpers'
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
|
||||
import Panel from 'components/ui/Panel'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import ResetDbPassword from './ResetDbPassword'
|
||||
|
||||
const DatabaseSettings: FC<any> = ({ projectRef }) => {
|
||||
const { data, isLoading, isError } = useProjectSettingsQuery({ projectRef })
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="mx-auto p-6 text-center sm:w-full md:w-3/4">
|
||||
<p className="text-scale-1000">Error loading database settings</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<section className="space-y-6">
|
||||
<h3 className="text-scale-1200 mb-2 text-xl">Database Settings</h3>
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection info
|
||||
</h5>
|
||||
}
|
||||
className="!m-0"
|
||||
>
|
||||
<Panel.Content className="space-y-8">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="grid gap-2 items-center md:grid md:grid-cols-12 md:gap-x-4 w-full"
|
||||
>
|
||||
<ShimmeringLoader className="h-4 w-1/3 col-span-4" delayIndex={i} />
|
||||
<ShimmeringLoader className="h-8 w-full col-span-8" delayIndex={i} />
|
||||
</div>
|
||||
))}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
|
||||
<ResetDbPassword disabled={true} />
|
||||
|
||||
<section className="space-y-6">
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection string
|
||||
</h5>
|
||||
}
|
||||
className="!m-0"
|
||||
>
|
||||
<Panel.Content>
|
||||
<Tabs type="underlined" size="small">
|
||||
<Tabs.Panel id="psql" label="PSQL">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="uri" label="URI">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="golang" label="Golang">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="jdbc" label="JDBC">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="dotnet" label=".NET">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="nodejs" label="Nodejs">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="php" label="PHP">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="python" label="Python">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { project } = data
|
||||
|
||||
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
|
||||
const connectionInfo = pluckObjectFields(project, DB_FIELDS)
|
||||
|
||||
const uriConnString =
|
||||
`postgresql://${connectionInfo.db_user}:[YOUR-PASSWORD]@` +
|
||||
`${connectionInfo.db_host}:${connectionInfo.db_port.toString()}` +
|
||||
`/${connectionInfo.db_name}`
|
||||
const golangConnString =
|
||||
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD] ` +
|
||||
`host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
|
||||
` dbname=${connectionInfo.db_name}`
|
||||
const psqlConnString =
|
||||
`psql -h ${connectionInfo.db_host} -p ` +
|
||||
`${connectionInfo.db_port.toString()} -d ${connectionInfo.db_name} ` +
|
||||
`-U ${connectionInfo.db_user}`
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<section className="space-y-6">
|
||||
<h3 className="text-scale-1200 mb-2 text-xl">Database Settings</h3>
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection info
|
||||
</h5>
|
||||
}
|
||||
className="!m-0"
|
||||
>
|
||||
<Panel.Content className="space-y-6">
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_host}
|
||||
label="Host"
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_name}
|
||||
label="Database name"
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_port.toString()}
|
||||
label="Port"
|
||||
/>
|
||||
|
||||
<Input
|
||||
layout="horizontal"
|
||||
className="input-mono table-input-cell text-base"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_user}
|
||||
label="User"
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
disabled
|
||||
readOnly
|
||||
value={'[The password you provided when you created this project]'}
|
||||
label="Password"
|
||||
/>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
|
||||
<ResetDbPassword />
|
||||
|
||||
<section className="space-y-6">
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection string
|
||||
</h5>
|
||||
}
|
||||
className="!m-0"
|
||||
>
|
||||
<Panel.Content>
|
||||
<Tabs type="underlined" size="small">
|
||||
<Tabs.Panel id="psql" label="PSQL">
|
||||
<Input copy readOnly disabled value={psqlConnString} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="uri" label="URI">
|
||||
<Input copy readOnly disabled value={uriConnString} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="golang" label="Golang">
|
||||
<Input copy readOnly disabled value={golangConnString} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="jdbc" label="JDBC">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`jdbc:postgresql://${
|
||||
connectionInfo.db_host
|
||||
}:${connectionInfo.db_port.toString()}` +
|
||||
`/${connectionInfo.db_name}?user=${connectionInfo.db_user}&password=[YOUR-PASSWORD]`
|
||||
}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="dotnet" label=".NET">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`User Id=${connectionInfo.db_user};Password=[YOUR-PASSWORD];` +
|
||||
`Server=${connectionInfo.db_host};Port=${connectionInfo.db_port.toString()};` +
|
||||
`Database=${connectionInfo.db_name}`
|
||||
}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="nodejs" label="Nodejs">
|
||||
<Input copy readOnly disabled value={uriConnString} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="php" label="PHP">
|
||||
<Input copy readOnly disabled value={golangConnString} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="python" label="Python">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD]` +
|
||||
` host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
|
||||
` database=${connectionInfo.db_name}`
|
||||
}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatabaseSettings
|
||||
@@ -0,0 +1,180 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { FC, useState, useRef, useEffect } from 'react'
|
||||
import { debounce } from 'lodash'
|
||||
import generator from 'generate-password'
|
||||
import { Input, Button, Modal } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { checkPermissions, useParams, useStore } from 'hooks'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { passwordStrength } from 'lib/helpers'
|
||||
import { API_URL, DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants'
|
||||
|
||||
import Panel from 'components/ui/Panel'
|
||||
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
|
||||
|
||||
const ResetDbPassword: FC<any> = ({ disabled = false }) => {
|
||||
const { ui, app, meta } = useStore()
|
||||
const { ref } = useParams()
|
||||
|
||||
const canResetDbPassword = checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
|
||||
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 (!ref) return
|
||||
|
||||
if (passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
|
||||
setIsUpdatingPassword(true)
|
||||
const res = await patch(`${API_URL}/projects/${ref}/db-password`, { password })
|
||||
if (!res.error) {
|
||||
await app.projects.fetchDetail(ref, (project) => meta.setProjectDetails(project))
|
||||
ui.setNotification({ category: 'success', message: res.message })
|
||||
setShowResetDbPass(false)
|
||||
} else {
|
||||
ui.setNotification({ category: 'error', message: 'Failed to reset password' })
|
||||
}
|
||||
setIsUpdatingPassword(false)
|
||||
}
|
||||
}
|
||||
|
||||
function generateStrongPassword() {
|
||||
const password = generator.generate({
|
||||
length: 16,
|
||||
numbers: true,
|
||||
uppercase: true,
|
||||
})
|
||||
setPassword(password)
|
||||
delayedCheckPasswordStrength(password)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel className="!m-0">
|
||||
<Panel.Content>
|
||||
<div className="grid grid-cols-1 items-center lg:grid-cols-3">
|
||||
<div className="col-span-2 space-y-1">
|
||||
<p className="block">Database password</p>
|
||||
<p className="text-sm opacity-50">
|
||||
You can use this password to connect directly to your Postgres database.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="default"
|
||||
disabled={!canResetDbPassword || disabled}
|
||||
onClick={() => setShowResetDbPass(true)}
|
||||
>
|
||||
Reset Database Password
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canResetDbPassword && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to reset the database password
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
<Modal
|
||||
hideFooter
|
||||
header={<h5 className="text-sm text-scale-1200">Reset database password</h5>}
|
||||
confirmText="Reset password"
|
||||
alignFooter="right"
|
||||
size="medium"
|
||||
visible={showResetDbPass}
|
||||
loading={isUpdatingPassword}
|
||||
onCancel={() => setShowResetDbPass(false)}
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="w-full space-y-8 py-8">
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
copy={password.length > 0}
|
||||
onChange={onDbPassChange}
|
||||
error={passwordStrengthWarning}
|
||||
// @ts-ignore
|
||||
descriptionText={
|
||||
<PasswordStrengthBar
|
||||
passwordStrengthScore={passwordStrengthScore}
|
||||
passwordStrengthMessage={passwordStrengthMessage}
|
||||
password={password}
|
||||
generateStrongPassword={generateStrongPassword}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
<Modal.Content>
|
||||
<div className="flex space-x-2 pb-2">
|
||||
<Button type="default" onClick={() => setShowResetDbPass(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={isUpdatingPassword}
|
||||
disabled={isUpdatingPassword}
|
||||
onClick={() => confirmResetDbPass()}
|
||||
>
|
||||
Reset password
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetDbPassword
|
||||
-1
@@ -7,7 +7,6 @@ import { useStore, useParams } from 'hooks'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { checkIfPrivate, getAddressEndRange } from './NetworkRestrictions.utils'
|
||||
import { useNetworkRestrictionsApplyMutation } from 'data/network-restrictions/network-retrictions-apply-mutation'
|
||||
import { get } from 'lib/common/fetch'
|
||||
|
||||
interface Props {
|
||||
restrictedIps: string[]
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ const NetworkRestrictions = ({}) => {
|
||||
title="Network Restrictions"
|
||||
description="Allow specific IP ranges to have access to your database."
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-2 mb-6">
|
||||
<Link href="https://supabase.com/docs/guides/platform/network-restrictions">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import Link from 'next/link'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useStore } from 'hooks'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, IconDownload, Toggle, IconExternalLink, IconLoader, Alert } from 'ui'
|
||||
import {
|
||||
FormHeader,
|
||||
FormPanel,
|
||||
FormSection,
|
||||
FormSectionContent,
|
||||
FormSectionLabel,
|
||||
} from 'components/ui/Forms'
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { useSSLEnforcementQuery } from 'data/ssl-enforcement/ssl-enforcement-query'
|
||||
import { useSSLEnforcementUpdateMutation } from 'data/ssl-enforcement/ssl-enforcement-update-mutation'
|
||||
|
||||
const SSLConfiguration = () => {
|
||||
const { ui } = useStore()
|
||||
const { ref } = useParams()
|
||||
const [isEnforced, setIsEnforced] = useState(false)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const { data: projectSettings } = useProjectSettingsQuery({ projectRef: ref })
|
||||
const { data: sslEnforcementConfiguration, isLoading } = useSSLEnforcementQuery({
|
||||
projectRef: ref,
|
||||
})
|
||||
const { mutateAsync: updateSSLEnforcement } = useSSLEnforcementUpdateMutation()
|
||||
|
||||
const hasAccessToSSLEnforcement = !sslEnforcementConfiguration?.isNotAllowed
|
||||
const env = process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? 'prod' : 'staging'
|
||||
const hasSSLCertificate =
|
||||
projectSettings?.project !== undefined &&
|
||||
new Date(projectSettings.project.inserted_at) >= new Date('2021-04-30')
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && sslEnforcementConfiguration) {
|
||||
setIsEnforced(
|
||||
sslEnforcementConfiguration.appliedSuccessfully &&
|
||||
sslEnforcementConfiguration.currentConfig.database
|
||||
)
|
||||
}
|
||||
}, [isLoading])
|
||||
|
||||
const toggleSSLEnforcement = async () => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
|
||||
setIsEnforced(!isEnforced)
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
await updateSSLEnforcement({
|
||||
projectRef: ref,
|
||||
requestedConfig: { database: !isEnforced },
|
||||
})
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to update SSL enforcement: ${error.message}`,
|
||||
})
|
||||
setIsEnforced(isEnforced)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAccessToSSLEnforcement) return <></>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<FormHeader title="SSL Configuration" description="" />
|
||||
<div className="flex items-center space-x-2 mb-6">
|
||||
<Link href="https://supabase.com/docs/guides/platform/ssl-enforcement">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<FormPanel>
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel
|
||||
className="lg:col-span-7"
|
||||
description={
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-scale-1000">
|
||||
Reject non-SSL connections to your database
|
||||
</p>
|
||||
{!sslEnforcementConfiguration?.appliedSuccessfully && (
|
||||
<Alert
|
||||
withIcon
|
||||
variant="warning"
|
||||
title="SSL enforcement was not updated successfully"
|
||||
>
|
||||
Please try updating again, or contact{' '}
|
||||
<Link href="/support/new">
|
||||
<a target="_blank" className="underline">
|
||||
support
|
||||
</a>
|
||||
</Link>{' '}
|
||||
if this error persists
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Enforce SSL on incoming connections
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-5">
|
||||
<div className="flex items-center justify-end mt-2.5 space-x-2">
|
||||
{(isLoading || isSubmitting) && (
|
||||
<IconLoader className="animate-spin" strokeWidth={1.5} size={16} />
|
||||
)}
|
||||
<Toggle
|
||||
checked={isEnforced}
|
||||
disabled={isLoading || isSubmitting}
|
||||
onChange={toggleSSLEnforcement}
|
||||
/>
|
||||
</div>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
|
||||
<div className="grid grid-cols-1 items-center lg:grid-cols-2 p-8">
|
||||
<div className="space-y-2">
|
||||
<p className="block text-sm">SSL Certificate</p>
|
||||
<div style={{ maxWidth: '420px' }}>
|
||||
<p className="text-sm opacity-50">
|
||||
Use this certificate when connecting to your database to prevent snooping and
|
||||
man-in-the-middle attacks.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button type="default" disabled={!hasSSLCertificate} icon={<IconDownload />}>
|
||||
<a
|
||||
href={`https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt`}
|
||||
>
|
||||
Download Certificate
|
||||
</a>
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!hasSSLCertificate && (
|
||||
<Tooltip.Content align="center" side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200 w-[250px]',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
Projects before 15:08 (GMT+08), 29th April 2021 do not have SSL certificates
|
||||
installed
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</div>
|
||||
</FormPanel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SSLConfiguration
|
||||
@@ -1,4 +1,5 @@
|
||||
import ConnectionPooling from './ConnectionPooling'
|
||||
import NetworkRestrictions from './NetworkRestrictions/NetworkRestrictions'
|
||||
import DatabaseSettings from './DatabaseSettings/DatabaseSettings'
|
||||
|
||||
export { ConnectionPooling, NetworkRestrictions }
|
||||
export { ConnectionPooling, DatabaseSettings, NetworkRestrictions }
|
||||
+1
-1
@@ -122,7 +122,7 @@ const CustomDomainVerify = ({ projectRef, customDomain, settings }: CustomDomain
|
||||
>
|
||||
Please add a CAA record allowing "digicert.com" to issue certificates for{' '}
|
||||
<code className="text-xs">{customDomain.hostname}</code>. For example:{' '}
|
||||
<code className="text-xs">0 issue "digitcert.com"</code>
|
||||
<code className="text-xs">0 issue "digicert.com"</code>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
export const CATEGORY_OPTIONS = [
|
||||
{
|
||||
value: 'problem',
|
||||
value: 'Problem',
|
||||
label: 'Issues with APIs / client libraries',
|
||||
description: "Issues with your project's API and client libraries",
|
||||
query: undefined,
|
||||
},
|
||||
{
|
||||
value: 'dashboard_bug',
|
||||
value: 'Dashboard_bug',
|
||||
label: 'Dashboard bug',
|
||||
description: 'Issues with the Supabase dashboard',
|
||||
query: undefined,
|
||||
},
|
||||
{
|
||||
value: 'database_unresponsive',
|
||||
value: 'Database_unresponsive',
|
||||
label: 'Database unresponsive',
|
||||
description: 'Issues with connecting to your database',
|
||||
query: 'Unable to connect',
|
||||
},
|
||||
{
|
||||
value: 'performance',
|
||||
value: 'Performance',
|
||||
label: 'Performance issues',
|
||||
description: 'Reporting of performance issues is only available on the Pro tier',
|
||||
query: 'Performance',
|
||||
},
|
||||
{
|
||||
value: 'sales',
|
||||
value: 'Sales',
|
||||
label: 'Sales enquiry',
|
||||
description: 'Questions about pricing, paid plans and Enterprise plans',
|
||||
query: undefined,
|
||||
},
|
||||
{
|
||||
value: 'billing',
|
||||
value: 'Billing',
|
||||
label: 'Billing',
|
||||
description: 'Issues with credit card charges | invoices | overcharging',
|
||||
query: undefined,
|
||||
},
|
||||
{
|
||||
value: 'problem',
|
||||
value: 'Problem',
|
||||
label: 'Abuse report',
|
||||
description: 'Report abuse of a Supabase project or Supabase brand',
|
||||
query: undefined,
|
||||
},
|
||||
{
|
||||
value: 'refund',
|
||||
value: 'Refund',
|
||||
label: 'Refund enquiry',
|
||||
description: 'Formal enquiry form for requesting refunds',
|
||||
query: undefined,
|
||||
|
||||
-1
@@ -375,7 +375,6 @@ const ColumnEditor: FC<Props> = ({
|
||||
)}
|
||||
|
||||
<ForeignKeySelector
|
||||
tables={tables}
|
||||
column={columnFields}
|
||||
visible={isEditingRelation}
|
||||
closePanel={() => setIsEditingRelation(false)}
|
||||
|
||||
+55
-14
@@ -1,16 +1,16 @@
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { get, find, isEmpty, sortBy } from 'lodash'
|
||||
import { Dictionary } from 'components/grid'
|
||||
import { SidePanel, Input, Listbox, IconHelpCircle } from 'ui'
|
||||
import type { PostgresTable, PostgresColumn } from '@supabase/postgres-meta'
|
||||
import { SidePanel, Input, Listbox, IconHelpCircle, IconDatabase } from 'ui'
|
||||
import type { PostgresTable, PostgresColumn, PostgresSchema } from '@supabase/postgres-meta'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import ActionBar from '../ActionBar'
|
||||
import { ForeignKey } from './ForeignKeySelector.types'
|
||||
import { ColumnField } from '../SidePanelEditor.types'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
|
||||
interface Props {
|
||||
tables: PostgresTable[]
|
||||
column: ColumnField
|
||||
metadata?: any
|
||||
visible: boolean
|
||||
@@ -18,15 +18,19 @@ interface Props {
|
||||
saveChanges: (value: { table: PostgresTable; column: PostgresColumn } | undefined) => void
|
||||
}
|
||||
|
||||
const ForeignKeySelector: FC<Props> = ({
|
||||
tables = [] as PostgresTable[],
|
||||
column,
|
||||
visible = false,
|
||||
closePanel,
|
||||
saveChanges,
|
||||
}) => {
|
||||
const ForeignKeySelector: FC<Props> = ({ column, visible = false, closePanel, saveChanges }) => {
|
||||
const { meta } = useStore()
|
||||
const [errors, setErrors] = useState<any>({})
|
||||
const [selectedForeignKey, setSelectedForeignKey] = useState<ForeignKey>()
|
||||
const [selectedForeignKey, setSelectedForeignKey] = useState<ForeignKey>({
|
||||
schema: 'public',
|
||||
table: '',
|
||||
column: '',
|
||||
})
|
||||
|
||||
const schemas = meta.schemas.list()
|
||||
const tables = meta.tables.list(
|
||||
(table: PostgresTable) => table.schema === selectedForeignKey.schema
|
||||
)
|
||||
|
||||
const foreignKey = column?.foreignKey
|
||||
const selectedTable: PostgresTable | undefined = find(tables, {
|
||||
@@ -41,6 +45,7 @@ const ForeignKeySelector: FC<Props> = ({
|
||||
// Reset the state of the side panel
|
||||
if (visible) {
|
||||
setErrors({})
|
||||
|
||||
if (foreignKey) {
|
||||
setSelectedForeignKey({
|
||||
schema: foreignKey.target_table_schema,
|
||||
@@ -49,7 +54,7 @@ const ForeignKeySelector: FC<Props> = ({
|
||||
})
|
||||
} else {
|
||||
setSelectedForeignKey({
|
||||
schema: '',
|
||||
schema: 'public',
|
||||
table: '',
|
||||
column: '',
|
||||
})
|
||||
@@ -57,6 +62,12 @@ const ForeignKeySelector: FC<Props> = ({
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
const updateSelectedSchema = (schema: string) => {
|
||||
meta.tables.loadBySchema(schema)
|
||||
const updatedForeignKey = { ...selectedForeignKey, schema, table: '', column: '' } as ForeignKey
|
||||
setSelectedForeignKey(updatedForeignKey)
|
||||
}
|
||||
|
||||
const updateSelectedTable = (tableId: number) => {
|
||||
setErrors({})
|
||||
if (!tableId) {
|
||||
@@ -87,6 +98,9 @@ const ForeignKeySelector: FC<Props> = ({
|
||||
|
||||
const onSaveChanges = (resolve: () => void) => {
|
||||
const errors = {} as Dictionary<any>
|
||||
if (!selectedForeignKey?.table) {
|
||||
errors['table'] = 'Please select a table'
|
||||
}
|
||||
if (selectedForeignKey?.table && !selectedForeignKey.column) {
|
||||
errors['column'] = `The table ${selectedForeignKey.table} has no columns`
|
||||
}
|
||||
@@ -141,12 +155,38 @@ const ForeignKeySelector: FC<Props> = ({
|
||||
/>
|
||||
|
||||
<Listbox
|
||||
id="schema"
|
||||
label="Select a schema"
|
||||
value={selectedForeignKey.schema}
|
||||
error={errors.schema}
|
||||
onChange={(value: string) => updateSelectedSchema(value)}
|
||||
>
|
||||
{schemas.map((schema: PostgresSchema) => {
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={schema.id}
|
||||
value={schema.name}
|
||||
label={schema.name}
|
||||
addOnBefore={() => <IconDatabase size={16} strokeWidth={1.5} />}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* For aria searching to target the schema name instead of schema */}
|
||||
<span className="hidden">{schema.name}</span>
|
||||
<span className="text-scale-1200">{schema.name}</span>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
|
||||
<Listbox
|
||||
id="table"
|
||||
label="Select a table to reference to"
|
||||
value={selectedTable?.id}
|
||||
value={selectedTable?.id ?? 1}
|
||||
error={errors.table}
|
||||
onChange={(value: string) => updateSelectedTable(Number(value))}
|
||||
>
|
||||
<Listbox.Option key="empty" value="" label="---">
|
||||
<Listbox.Option key="empty" value={1} label="---">
|
||||
---
|
||||
</Listbox.Option>
|
||||
{/* @ts-ignore */}
|
||||
@@ -179,6 +219,7 @@ const ForeignKeySelector: FC<Props> = ({
|
||||
/>
|
||||
) : (
|
||||
<Listbox
|
||||
id="column"
|
||||
value={selectedColumn?.id}
|
||||
// @ts-ignore
|
||||
label={
|
||||
|
||||
-1
@@ -353,7 +353,6 @@ const ColumnManagement: FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
<ForeignKeySelector
|
||||
tables={tables}
|
||||
column={selectedColumnToEditRelation as ColumnField}
|
||||
visible={!isUndefined(selectedColumnToEditRelation)}
|
||||
closePanel={() => setSelectedColumnToEditRelation(undefined)}
|
||||
|
||||
@@ -43,7 +43,7 @@ const TableEditorMenu: FC<Props> = ({
|
||||
onDeleteTable = () => {},
|
||||
onDuplicateTable = () => {},
|
||||
}) => {
|
||||
const { meta, ui } = useStore()
|
||||
const { meta } = useStore()
|
||||
const { id, ref } = useParams()
|
||||
|
||||
const schemas: PostgresSchema[] = meta.schemas.list()
|
||||
|
||||
@@ -39,7 +39,7 @@ const FormSectionLabel = ({
|
||||
}) => {
|
||||
if (description !== undefined) {
|
||||
return (
|
||||
<div className={`flex flex-col space-y-4 col-span-12 lg:col-span-5 ${className}`}>
|
||||
<div className={`flex flex-col space-y-2 col-span-12 lg:col-span-5 ${className}`}>
|
||||
<label className="text-scale-1200 text-sm">{children}</label>
|
||||
{description}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const sslEnforcementKeys = {
|
||||
list: (projectRef: string | undefined) => ['projects', projectRef, 'ssl-enforcement'] as const,
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_ADMIN_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { sslEnforcementKeys } from './keys'
|
||||
|
||||
export type SSLEnforcementVariables = { projectRef?: string }
|
||||
|
||||
export type SSLEnforcementResponse = {
|
||||
appliedSuccessfully: boolean
|
||||
currentConfig: { database: boolean }
|
||||
error?: any
|
||||
isNotAllowed?: boolean
|
||||
}
|
||||
|
||||
export async function getSSLEnforcementConfiguration(
|
||||
{ projectRef }: SSLEnforcementVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
const response = (await get(`${API_ADMIN_URL}/projects/${projectRef}/ssl-enforcement`, {
|
||||
signal,
|
||||
})) as SSLEnforcementResponse
|
||||
|
||||
// Not allowed error is a valid response to denote if a project
|
||||
// has access to the SSL enforcement UI, so we'll handle it here
|
||||
if (response.error) {
|
||||
const isNotAllowedError =
|
||||
(response.error as any)?.code === 400 &&
|
||||
(response.error as any)?.message?.includes('not allowed to configure SSL enforcements')
|
||||
|
||||
if (isNotAllowedError) {
|
||||
return {
|
||||
appliedSuccessfully: false,
|
||||
currentConfig: { database: false },
|
||||
isNotAllowed: true,
|
||||
} as SSLEnforcementResponse
|
||||
} else {
|
||||
throw response.error
|
||||
}
|
||||
}
|
||||
|
||||
return response as SSLEnforcementResponse
|
||||
}
|
||||
|
||||
export type SSLEnforcementData = Awaited<ReturnType<typeof getSSLEnforcementConfiguration>>
|
||||
export type SSLEnforcementError = unknown
|
||||
|
||||
export const useSSLEnforcementQuery = <TData = SSLEnforcementData>(
|
||||
{ projectRef }: SSLEnforcementVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<SSLEnforcementData, SSLEnforcementError, TData> = {}
|
||||
) =>
|
||||
useQuery<SSLEnforcementData, SSLEnforcementError, TData>(
|
||||
sslEnforcementKeys.list(projectRef),
|
||||
({ signal }) => getSSLEnforcementConfiguration({ projectRef }, signal),
|
||||
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
|
||||
)
|
||||
|
||||
export const useSSLEnforcementPrefetch = ({ projectRef }: SSLEnforcementVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef) {
|
||||
client.prefetchQuery(sslEnforcementKeys.list(projectRef), ({ signal }) =>
|
||||
getSSLEnforcementConfiguration({ projectRef }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef])
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { put } from 'lib/common/fetch'
|
||||
import { API_ADMIN_URL } from 'lib/constants'
|
||||
import { sslEnforcementKeys } from './keys'
|
||||
|
||||
export type SSLEnforcementUpdateVariables = {
|
||||
projectRef: string
|
||||
requestedConfig: { database: boolean }
|
||||
}
|
||||
|
||||
export type SSLEnforcementUpdateResponse = {
|
||||
appliedSuccessfully: boolean
|
||||
currentConfig: { database: boolean }
|
||||
error?: any
|
||||
}
|
||||
|
||||
export async function updateSSLEnforcement({
|
||||
projectRef,
|
||||
requestedConfig,
|
||||
}: SSLEnforcementUpdateVariables) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
const response = (await put(`${API_ADMIN_URL}/projects/${projectRef}/ssl-enforcement`, {
|
||||
requestedConfig,
|
||||
})) as SSLEnforcementUpdateResponse
|
||||
if (response.error) throw response.error
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
type SSLEnforcementUpdateData = Awaited<ReturnType<typeof updateSSLEnforcement>>
|
||||
|
||||
export const useSSLEnforcementUpdateMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<SSLEnforcementUpdateData, unknown, SSLEnforcementUpdateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<SSLEnforcementUpdateData, unknown, SSLEnforcementUpdateVariables>(
|
||||
(vars) => updateSSLEnforcement(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(sslEnforcementKeys.list(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -3,4 +3,5 @@ export * from './delete'
|
||||
export * from './get'
|
||||
export * from './patch'
|
||||
export * from './post'
|
||||
export * from './put'
|
||||
export * from './head'
|
||||
@@ -0,0 +1,28 @@
|
||||
import { constructHeaders, handleError, handleResponse, handleResponseError } from './base'
|
||||
import { uuidv4 } from '../../helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
export async function put<T = any>(
|
||||
url: string,
|
||||
data: { [prop: string]: any },
|
||||
options?: { [prop: string]: any }
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
try {
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = await constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
|
||||
if (!response.ok) return handleResponseError(response, requestId)
|
||||
return handleResponse(response, requestId)
|
||||
} catch (error) {
|
||||
return handleError(error, requestId)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -39,7 +39,7 @@
|
||||
"clipboard": "^2.0.8",
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
"configcat-js": "^5.6.0",
|
||||
"configcat-js": "^7.0.0",
|
||||
"dayjs": "^1.11.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"generate-password": "^1.7.0",
|
||||
|
||||
@@ -138,14 +138,14 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
icon={<IconSearch size="tiny" />}
|
||||
/>
|
||||
</div>
|
||||
<Button type="link" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security"
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security"
|
||||
>
|
||||
<Button type="link" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
|
||||
What is RLS?
|
||||
</a>
|
||||
</Button>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Policies tables={filteredTables} hasTables={tables.length > 0} isLocked={isLocked} />
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { FC, useState, useRef, useEffect } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { debounce } from 'lodash'
|
||||
import generator from 'generate-password'
|
||||
import { Input, Button, IconDownload, Tabs, Modal } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { checkPermissions, useFlag, useParams, useStore } from 'hooks'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { pluckObjectFields, passwordStrength } from 'lib/helpers'
|
||||
import { API_URL, DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants'
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
|
||||
import { useFlag, useParams } from 'hooks'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { ConnectionPooling, NetworkRestrictions } from 'components/interfaces/Settings/Database'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import {
|
||||
ConnectionPooling,
|
||||
DatabaseSettings,
|
||||
NetworkRestrictions,
|
||||
} from 'components/interfaces/Settings/Database'
|
||||
|
||||
import SSLConfiguration from 'components/interfaces/Settings/Database/SSLConfiguration'
|
||||
|
||||
const ProjectSettings: NextPageWithLayout = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const networkRestrictions = useFlag('networkRestrictions')
|
||||
const sslEnforcement = useFlag('sslEnforcement')
|
||||
|
||||
return (
|
||||
<div className="1xl:px-28 mx-auto flex flex-col gap-4 px-5 pt-6 pb-14 lg:px-16 xl:px-24 2xl:px-32">
|
||||
<div className="content h-full w-full overflow-y-auto">
|
||||
<GeneralSettings projectRef={projectRef} />
|
||||
<div className="1xl:px-28 mx-auto flex flex-col px-5 pt-6 pb-14 lg:px-16 xl:px-24 2xl:px-32">
|
||||
<div className="content h-full w-full overflow-y-auto space-y-10">
|
||||
<DatabaseSettings projectRef={projectRef} />
|
||||
<ConnectionPooling />
|
||||
{sslEnforcement && <SSLConfiguration />}
|
||||
{networkRestrictions && <NetworkRestrictions />}
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,458 +31,3 @@ const ProjectSettings: NextPageWithLayout = () => {
|
||||
ProjectSettings.getLayout = (page) => <SettingsLayout title="Database">{page}</SettingsLayout>
|
||||
|
||||
export default observer(ProjectSettings)
|
||||
|
||||
const ResetDbPassword: FC<any> = ({ disabled = false }) => {
|
||||
const { ui, app, meta } = useStore()
|
||||
const { ref } = useParams()
|
||||
|
||||
const canResetDbPassword = checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
|
||||
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 (!ref) return
|
||||
|
||||
if (passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
|
||||
setIsUpdatingPassword(true)
|
||||
const res = await patch(`${API_URL}/projects/${ref}/db-password`, { password })
|
||||
if (!res.error) {
|
||||
await app.projects.fetchDetail(ref, (project) => meta.setProjectDetails(project))
|
||||
ui.setNotification({ category: 'success', message: res.message })
|
||||
setShowResetDbPass(false)
|
||||
} else {
|
||||
ui.setNotification({ category: 'error', message: 'Failed to reset password' })
|
||||
}
|
||||
setIsUpdatingPassword(false)
|
||||
}
|
||||
}
|
||||
|
||||
function generateStrongPassword() {
|
||||
const password = generator.generate({
|
||||
length: 16,
|
||||
numbers: true,
|
||||
uppercase: true,
|
||||
})
|
||||
setPassword(password)
|
||||
delayedCheckPasswordStrength(password)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
<div className="grid grid-cols-1 items-center lg:grid-cols-2">
|
||||
<div>
|
||||
<p className="block">Database password</p>
|
||||
<div style={{ maxWidth: '420px' }}>
|
||||
<p className="text-sm opacity-50">
|
||||
You can use this password to connect directly to your Postgres database.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="default"
|
||||
disabled={!canResetDbPassword || disabled}
|
||||
onClick={() => setShowResetDbPass(true)}
|
||||
>
|
||||
Reset Database Password
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canResetDbPassword && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to reset the database password
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
<Modal
|
||||
hideFooter
|
||||
header={<h5 className="text-sm text-scale-1200">Reset database password</h5>}
|
||||
confirmText="Reset password"
|
||||
alignFooter="right"
|
||||
size="medium"
|
||||
visible={showResetDbPass}
|
||||
loading={isUpdatingPassword}
|
||||
onCancel={() => setShowResetDbPass(false)}
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="w-full space-y-8 py-8">
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
copy={password.length > 0}
|
||||
onChange={onDbPassChange}
|
||||
error={passwordStrengthWarning}
|
||||
// @ts-ignore
|
||||
descriptionText={
|
||||
<PasswordStrengthBar
|
||||
passwordStrengthScore={passwordStrengthScore}
|
||||
passwordStrengthMessage={passwordStrengthMessage}
|
||||
password={password}
|
||||
generateStrongPassword={generateStrongPassword}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
<Modal.Content>
|
||||
<div className="flex space-x-2 pb-2">
|
||||
<Button type="default" onClick={() => setShowResetDbPass(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={isUpdatingPassword}
|
||||
disabled={isUpdatingPassword}
|
||||
onClick={() => confirmResetDbPass()}
|
||||
>
|
||||
Reset password
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const DownloadCertificate: FC<any> = ({ createdAt, disabled = false }) => {
|
||||
// instances before 3 : 08 pm sgt 29th April don't have certs installed
|
||||
if (new Date(createdAt) < new Date('2021-04-30')) return null
|
||||
const env = process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? 'prod' : 'staging'
|
||||
return (
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
<div className="grid grid-cols-1 items-center lg:grid-cols-2">
|
||||
<div>
|
||||
<p className="block">SSL Connection</p>
|
||||
<div style={{ maxWidth: '420px' }}>
|
||||
<p className="text-sm opacity-50">
|
||||
Use this certificate when connecting to your database to prevent snooping and
|
||||
man-in-the-middle attacks.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
<Button type="default" icon={<IconDownload />} disabled={disabled}>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
const GeneralSettings: FC<any> = ({ projectRef }) => {
|
||||
const { data, isLoading, isError } = useProjectSettingsQuery({ projectRef })
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="mx-auto p-6 text-center sm:w-full md:w-3/4">
|
||||
<p className="text-scale-1000">Error loading database settings</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<section className="space-y-6">
|
||||
<h3 className="text-scale-1200 mb-2 text-xl">Database Settings</h3>
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection info
|
||||
</h5>
|
||||
}
|
||||
>
|
||||
<Panel.Content className="space-y-8">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="grid gap-2 items-center md:grid md:grid-cols-12 md:gap-x-4 w-full"
|
||||
>
|
||||
<ShimmeringLoader className="h-4 w-1/3 col-span-4" delayIndex={i} />
|
||||
<ShimmeringLoader className="h-8 w-full col-span-8" delayIndex={i} />
|
||||
</div>
|
||||
))}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
<ResetDbPassword disabled={true} />
|
||||
<DownloadCertificate disabled={true} />
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<section className="space-y-6">
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection string
|
||||
</h5>
|
||||
}
|
||||
>
|
||||
<Panel.Content>
|
||||
<Tabs type="underlined" size="small">
|
||||
<Tabs.Panel id="psql" label="PSQL">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="uri" label="URI">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="golang" label="Golang">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="jdbc" label="JDBC">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="dotnet" label=".NET">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="nodejs" label="Nodejs">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="php" label="PHP">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="python" label="Python">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const { project } = data
|
||||
|
||||
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
|
||||
const connectionInfo = pluckObjectFields(project, DB_FIELDS)
|
||||
|
||||
const uriConnString =
|
||||
`postgresql://${connectionInfo.db_user}:[YOUR-PASSWORD]@` +
|
||||
`${connectionInfo.db_host}:${connectionInfo.db_port.toString()}` +
|
||||
`/${connectionInfo.db_name}`
|
||||
const golangConnString =
|
||||
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD] ` +
|
||||
`host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
|
||||
` dbname=${connectionInfo.db_name}`
|
||||
const psqlConnString =
|
||||
`psql -h ${connectionInfo.db_host} -p ` +
|
||||
`${connectionInfo.db_port.toString()} -d ${connectionInfo.db_name} ` +
|
||||
`-U ${connectionInfo.db_user}`
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<section className="space-y-6">
|
||||
<h3 className="text-scale-1200 mb-2 text-xl">Database Settings</h3>
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection info
|
||||
</h5>
|
||||
}
|
||||
>
|
||||
<Panel.Content className="space-y-6">
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_host}
|
||||
label="Host"
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_name}
|
||||
label="Database name"
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_port.toString()}
|
||||
label="Port"
|
||||
/>
|
||||
|
||||
<Input
|
||||
layout="horizontal"
|
||||
className="input-mono table-input-cell text-base"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_user}
|
||||
label="User"
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
disabled
|
||||
readOnly
|
||||
value={'[The password you provided when you created this project]'}
|
||||
label="Password"
|
||||
/>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
<ResetDbPassword />
|
||||
<DownloadCertificate createdAt={connectionInfo.inserted_at} />
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<section className="space-y-6">
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection string
|
||||
</h5>
|
||||
}
|
||||
>
|
||||
<Panel.Content>
|
||||
<Tabs type="underlined" size="small">
|
||||
<Tabs.Panel id="psql" label="PSQL">
|
||||
<Input copy readOnly disabled value={psqlConnString} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="uri" label="URI">
|
||||
<Input copy readOnly disabled value={uriConnString} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="golang" label="Golang">
|
||||
<Input copy readOnly disabled value={golangConnString} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="jdbc" label="JDBC">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`jdbc:postgresql://${
|
||||
connectionInfo.db_host
|
||||
}:${connectionInfo.db_port.toString()}` +
|
||||
`/${connectionInfo.db_name}?user=${connectionInfo.db_user}&password=[YOUR-PASSWORD]`
|
||||
}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="dotnet" label=".NET">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`User Id=${connectionInfo.db_user};Password=[YOUR-PASSWORD];` +
|
||||
`Server=${
|
||||
connectionInfo.db_host
|
||||
};Port=${connectionInfo.db_port.toString()};` +
|
||||
`Database=${connectionInfo.db_name}`
|
||||
}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="nodejs" label="Nodejs">
|
||||
<Input copy readOnly disabled value={uriConnString} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="php" label="PHP">
|
||||
<Input copy readOnly disabled value={golangConnString} />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="python" label="Python">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD]` +
|
||||
` host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
|
||||
` database=${connectionInfo.db_name}`
|
||||
}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user