From 6789079aeed86f5c96566ef5972933f169ce3eb2 Mon Sep 17 00:00:00 2001 From: GeekyAdams Date: Wed, 28 Jun 2023 21:05:12 +0100 Subject: [PATCH 001/246] feat: Link from dashboard to extension docs #15415 --- .../Database/Extensions/ExtensionCard.tsx | 31 ++++++++++++++----- studio/pages/project/[ref]/index.tsx | 9 +++--- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx b/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx index f7f3337f962..633fa5acd99 100644 --- a/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx +++ b/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx @@ -1,7 +1,8 @@ import { FC, useState } from 'react' import { observer } from 'mobx-react-lite' -import { Badge, IconLoader, Toggle } from 'ui' +import { Badge, IconLoader, Toggle, IconExternalLink } from 'ui' import { PermissionAction } from '@supabase/shared-types/out/constants' +import Link from 'next/link' import { checkPermissions, useStore } from 'hooks' import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' @@ -69,15 +70,29 @@ const ExtensionCard: FC = ({ extension }) => {
-

- {extension.name} -

+ +
+

+ {extension.name} +

+ + +
+
+ + {loading ? ( ) : ( @@ -100,7 +115,7 @@ const ExtensionCard: FC = ({ extension }) => {
{isOn && extension.schema && (
-
+
Schema: {`${extension.schema}`}
diff --git a/studio/pages/project/[ref]/index.tsx b/studio/pages/project/[ref]/index.tsx index f380381c48c..830dd12418b 100644 --- a/studio/pages/project/[ref]/index.tsx +++ b/studio/pages/project/[ref]/index.tsx @@ -1,3 +1,4 @@ +import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard' import { ClientLibrary, ExampleProject } from 'components/interfaces/Home' import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants' import ProjectUsageSection from 'components/interfaces/Home/ProjectUsageSection' @@ -18,8 +19,8 @@ const Home: NextPageWithLayout = () => { : 'Welcome to your project' return ( -
-
+
+

{projectName}

@@ -41,7 +42,7 @@ const Home: NextPageWithLayout = () => {

Client libraries

-
+
{CLIENT_LIBRARIES.map((library) => ( ))} @@ -51,7 +52,7 @@ const Home: NextPageWithLayout = () => {

Example projects

-
+
{EXAMPLE_PROJECTS.sort((a, b) => a.title.localeCompare(b.title)).map((project) => ( ))} From d6be833e0f1443e8f9e742e09be0930421b2b68e Mon Sep 17 00:00:00 2001 From: GeekyAdams Date: Wed, 28 Jun 2023 21:08:54 +0100 Subject: [PATCH 002/246] feat: Link from dashboard to extension docs #15415 --- studio/pages/project/[ref]/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/studio/pages/project/[ref]/index.tsx b/studio/pages/project/[ref]/index.tsx index 830dd12418b..63269ee9258 100644 --- a/studio/pages/project/[ref]/index.tsx +++ b/studio/pages/project/[ref]/index.tsx @@ -1,4 +1,3 @@ -import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard' import { ClientLibrary, ExampleProject } from 'components/interfaces/Home' import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants' import ProjectUsageSection from 'components/interfaces/Home/ProjectUsageSection' From f30a7181efb57cbfb7ff28a07c56271289f7d599 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Fri, 30 Jun 2023 11:29:28 -0230 Subject: [PATCH 003/246] Update studio/components/interfaces/Database/Extensions/ExtensionCard.tsx --- .../components/interfaces/Database/Extensions/ExtensionCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx b/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx index 633fa5acd99..9f0397540e1 100644 --- a/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx +++ b/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx @@ -88,7 +88,7 @@ const ExtensionCard: FC = ({ extension }) => { {extension.name} - +
From d0d295da9ecc2f0396e70080c53ea35cc7f8216f Mon Sep 17 00:00:00 2001 From: Zach Blume Date: Wed, 5 Jul 2023 20:16:46 -0400 Subject: [PATCH 004/246] Speed up `docker compose down` for postgREST --- docker/docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index fcb3c55d83c..38000962786 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -127,6 +127,10 @@ services: PGRST_DB_ANON_ROLE: anon PGRST_JWT_SECRET: ${JWT_SECRET} PGRST_DB_USE_LEGACY_GUCS: "false" + # The following explicit command quickens the `docker compose down` command from 10s to 0.3s: + # https://github.com/PostgREST/postgrest/issues/2851 + command: + command: "postgrest" realtime: container_name: realtime-dev.supabase-realtime From 310bcfe84c72024eac3b39fa3fa9e87844a125cf Mon Sep 17 00:00:00 2001 From: Zach Blume Date: Fri, 7 Jul 2023 16:52:17 -0400 Subject: [PATCH 005/246] Remove comment, simplify command --- docker/docker-compose.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 38000962786..fedb2609ac1 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -127,10 +127,7 @@ services: PGRST_DB_ANON_ROLE: anon PGRST_JWT_SECRET: ${JWT_SECRET} PGRST_DB_USE_LEGACY_GUCS: "false" - # The following explicit command quickens the `docker compose down` command from 10s to 0.3s: - # https://github.com/PostgREST/postgrest/issues/2851 - command: - command: "postgrest" + command: "postgrest" realtime: container_name: realtime-dev.supabase-realtime From 9e5e932c70d86339d24f9a0f6aab132fe962d280 Mon Sep 17 00:00:00 2001 From: Isaiah Hamilton <70828596+Isaiah-Hamilton@users.noreply.github.com> Date: Tue, 22 Aug 2023 23:22:33 -0400 Subject: [PATCH 006/246] improve join the community section --- .../Sections/TwitterSocialProof.tsx | 111 +++++------------- apps/www/components/TweetCard.tsx | 10 +- 2 files changed, 29 insertions(+), 92 deletions(-) diff --git a/apps/www/components/Sections/TwitterSocialProof.tsx b/apps/www/components/Sections/TwitterSocialProof.tsx index 45062e98ebc..a9a52d8ede0 100644 --- a/apps/www/components/Sections/TwitterSocialProof.tsx +++ b/apps/www/components/Sections/TwitterSocialProof.tsx @@ -1,31 +1,15 @@ -import { useRef } from 'react' - +import { useState } from 'react' import { useRouter } from 'next/router' - -import { Swiper, SwiperSlide } from 'swiper/react' -// import Swiper core and required modules -import SwiperCore, { Navigation, Pagination } from 'swiper' - -import { Button, IconMessageCircle, IconArrowLeft, IconArrowRight } from 'ui' - -import Examples from '../../data/tweets/Tweets.json' +import { Button, IconMessageCircle } from 'ui' +import Tweets from '../../data/tweets/Tweets.json' import TweetCard from '../TweetCard' - -// Import Swiper styles -import 'swiper/swiper.min.css' -import 'swiper/components/navigation/navigation.min.css' -import 'swiper/components/pagination/pagination.min.css' import Link from 'next/link' -// install Swiper modules -SwiperCore.use([Navigation, Pagination]) - function TwitterSocialProof() { // base path for images const { basePath } = useRouter() - const prevRef = useRef(null) - const nextRef = useRef(null) + const [showAll, setShowAll] = useState(false) return ( <> @@ -53,70 +37,31 @@ function TwitterSocialProof() {
-
-
- { - swiper.params.navigation.prevEl = prevRef.current - swiper.params.navigation.nextEl = nextRef.current - // swiper.navigation.update() - }} - breakpoints={{ - 320: { - slidesPerView: 1, - }, - 720: { - slidesPerView: 2, - }, - 920: { - slidesPerView: 3, - }, - 1024: { - slidesPerView: 4, - }, - 1208: { - slidesPerView: 5, - }, - }} - > - {Examples.map((tweet: any, i: number) => { - return ( - -
- - - - - -
-
- ) - })} -
-
- -
-
- -
+
+
+ {!showAll && ( +
+ )} + {Tweets.map((tweet: any, i: number) => ( +
+ + + + +
- + ))} + {!showAll && ( +
+ +
+ )}
diff --git a/apps/www/components/TweetCard.tsx b/apps/www/components/TweetCard.tsx index fe91dd10bb7..4f78c18094f 100644 --- a/apps/www/components/TweetCard.tsx +++ b/apps/www/components/TweetCard.tsx @@ -11,13 +11,9 @@ function TweetCard(props: TweetCard) { return (
@@ -35,11 +31,7 @@ function TweetCard(props: TweetCard) { ) : (
)} -

+

{props.handle}

Date: Fri, 25 Aug 2023 13:19:42 -0230 Subject: [PATCH 007/246] Fix duplicate key --- studio/components/interfaces/Support/Support.constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/components/interfaces/Support/Support.constants.ts b/studio/components/interfaces/Support/Support.constants.ts index 3bec483febf..1e16de175d5 100644 --- a/studio/components/interfaces/Support/Support.constants.ts +++ b/studio/components/interfaces/Support/Support.constants.ts @@ -36,7 +36,7 @@ export const CATEGORY_OPTIONS = [ query: undefined, }, { - value: 'Problem', + value: 'Abuse', label: 'Abuse report', description: 'Report abuse of a Supabase project or Supabase brand', query: undefined, From 66c0903364a987f73b2a7ca3d90a4ed0c31756df Mon Sep 17 00:00:00 2001 From: kishanhitk Date: Sat, 26 Aug 2023 14:45:56 +0530 Subject: [PATCH 008/246] chore: add note --- studio/data/projects/project-type-generation-query.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/studio/data/projects/project-type-generation-query.ts b/studio/data/projects/project-type-generation-query.ts index f950459d43f..8e14366dcc8 100644 --- a/studio/data/projects/project-type-generation-query.ts +++ b/studio/data/projects/project-type-generation-query.ts @@ -9,6 +9,8 @@ export type GenerateTypesVariables = { ref?: string } export async function generateTypes({ ref }: GenerateTypesVariables, signal?: AbortSignal) { if (!ref) throw new Error('Project ref is required') + // TODO: Something wrong with the typescript endpoint on local dev and self-hosted + const { data, error } = await get(`/v1/projects/{ref}/types/typescript`, { params: { path: { ref } }, signal, From be0c24dcc92d500f48b3ed342515b9b3ed736c4d Mon Sep 17 00:00:00 2001 From: GaryAustin1 <54564956+GaryAustin1@users.noreply.github.com> Date: Sat, 26 Aug 2023 10:25:04 -0500 Subject: [PATCH 009/246] Update auth-gotrue.mdx to use signInWithPassword and not signIn --- apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx b/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx index 85d22733009..ba702ac28ea 100644 --- a/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx +++ b/apps/docs/pages/learn/auth-deep-dive/auth-gotrue.mdx @@ -57,7 +57,7 @@ You'll have to make sure your google app is verified of course in order to reque But all the functionality of gotrue-js is also available in supabase-js, which uses gotrue-js internally when you do things like: ```jsx -const { user, session, error } = await supabase.auth.signIn({ +const { user, session, error } = await supabase.auth.signInWithPassword({ email: 'example@email.com', password: 'example-password', }) From 9ba9ab70977d4576029649a5031a68c01f51cec3 Mon Sep 17 00:00:00 2001 From: Neeraj Walia Date: Sun, 27 Aug 2023 00:08:49 +0530 Subject: [PATCH 010/246] Add bucket name validation --- studio/pages/api/storage/[ref]/buckets/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/studio/pages/api/storage/[ref]/buckets/index.ts b/studio/pages/api/storage/[ref]/buckets/index.ts index e426db1810c..e7a968948d5 100644 --- a/studio/pages/api/storage/[ref]/buckets/index.ts +++ b/studio/pages/api/storage/[ref]/buckets/index.ts @@ -33,6 +33,13 @@ const handleGet = async (req: NextApiRequest, res: NextApiResponse) => { const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { const { id, public: isPublicBucket } = req.body + // To validate bucket name, can be removed once the issue is fixed in supabase lib + const regex = /^[a-z0-9.-]+$/ + if (!regex.test(id)) { + return res.status(400).json({ error: { message: 'Bucket name invalid' } }) + } + // Bucket name validation ends here + const { data, error } = await supabase.storage.createBucket(id, { public: isPublicBucket }) if (error) { return res.status(400).json({ error: { message: error.message } }) From c9b7b98cf771dd8259c7b7dbcad2fed4a6fa3bbb Mon Sep 17 00:00:00 2001 From: JeremieV <45327819+JeremieV@users.noreply.github.com> Date: Sun, 27 Aug 2023 21:01:13 +0200 Subject: [PATCH 011/246] Fix typo in column-encryption.mdx --- apps/docs/pages/guides/database/column-encryption.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/database/column-encryption.mdx b/apps/docs/pages/guides/database/column-encryption.mdx index 1734d88a6af..0f6d7e599ee 100644 --- a/apps/docs/pages/guides/database/column-encryption.mdx +++ b/apps/docs/pages/guides/database/column-encryption.mdx @@ -11,7 +11,7 @@ export const meta = { Supabase provides a secure method for encrypting data using [Vault](/docs/guides/database/vault), our Postgres secrets manager. Vault is a Postgres extension with an [integrated UI](https://app.supabase.com/project/_/settings/vault/secrets) intended to act as a secure global secrets management for your project. -In addition to the Vault secret storage table, Supabase also enables an advanced feature called Transparent Column Encryption (TCE) which provides a safe way to encrypt columns in your own tables so that they doesn't leak into logs and backups. It can also provide row-level authenticated encryption. +In addition to the Vault secret storage table, Supabase also enables an advanced feature called Transparent Column Encryption (TCE) which provides a safe way to encrypt columns in your own tables so that they don't leak into logs and backups. It can also provide row-level authenticated encryption. Column Encryption comes with tradeoffs that need to be considered before using it. From dbeed694f7c9dea7f19743c3a43cd5266e4e60e6 Mon Sep 17 00:00:00 2001 From: dragarcia Date: Mon, 28 Aug 2023 16:56:24 +0800 Subject: [PATCH 012/246] chore: sort backups by inserted_at instead --- studio/stores/project/ProjectBackupsStore.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/studio/stores/project/ProjectBackupsStore.ts b/studio/stores/project/ProjectBackupsStore.ts index 45f6802ac2a..db9c184601b 100644 --- a/studio/stores/project/ProjectBackupsStore.ts +++ b/studio/stores/project/ProjectBackupsStore.ts @@ -92,9 +92,15 @@ export default class ProjectBackupsStore implements IProjectBackupsStore { list(filter?: any) { const arr = Object.values(this.data) if (!!filter) { - return arr.filter(filter).sort((a: any, b: any) => b.id - a.id) + return arr + .filter(filter) + .sort( + (a: any, b: any) => new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf() + ) } else { - return arr.sort((a: any, b: any) => b.id - a.id) + return arr.sort( + (a: any, b: any) => new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf() + ) } } From c235affcf8323ac6851c35559af4b1033e3dd5a5 Mon Sep 17 00:00:00 2001 From: dragarcia Date: Mon, 28 Aug 2023 16:57:25 +0800 Subject: [PATCH 013/246] chore: use pitr_enabled when checking for pitr sub --- studio/components/interfaces/Database/Backups/BackupsList.tsx | 2 +- studio/pages/project/[ref]/database/backups/scheduled.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/studio/components/interfaces/Database/Backups/BackupsList.tsx b/studio/components/interfaces/Database/Backups/BackupsList.tsx index dabc90d5837..6add480cb2c 100644 --- a/studio/components/interfaces/Database/Backups/BackupsList.tsx +++ b/studio/components/interfaces/Database/Backups/BackupsList.tsx @@ -21,7 +21,7 @@ const BackupsList = () => { 'queue_job.restore.prepare' ) - const isPitrEnabled = backups?.configuration?.walg_enabled + const isPitrEnabled = backups?.configuration?.pitr_enabled if (backups.isLoading) return if (backups.error) return diff --git a/studio/pages/project/[ref]/database/backups/scheduled.tsx b/studio/pages/project/[ref]/database/backups/scheduled.tsx index 2a6fe5fbd3b..d32e43d89d2 100644 --- a/studio/pages/project/[ref]/database/backups/scheduled.tsx +++ b/studio/pages/project/[ref]/database/backups/scheduled.tsx @@ -18,7 +18,7 @@ const DatabaseScheduledBackups: NextPageWithLayout = () => { const { project } = useProjectContext() const ref = project?.ref - const isPitrEnabled = backups?.configuration?.walg_enabled + const isPitrEnabled = backups?.configuration?.pitr_enabled const canReadScheduledBackups = useCheckPermissions(PermissionAction.READ, 'back_ups') From 915fdbfc23f3a47aba2adcdf8282a11344956ba2 Mon Sep 17 00:00:00 2001 From: dragarcia Date: Mon, 28 Aug 2023 16:57:44 +0800 Subject: [PATCH 014/246] chore: update scheduled backups --- .../Database/Backups/BackupItem.tsx | 32 ++----------------- .../data/database/backup-restore-mutation.ts | 2 +- 2 files changed, 4 insertions(+), 30 deletions(-) diff --git a/studio/components/interfaces/Database/Backups/BackupItem.tsx b/studio/components/interfaces/Database/Backups/BackupItem.tsx index be03cf13da2..4e447f635d3 100644 --- a/studio/components/interfaces/Database/Backups/BackupItem.tsx +++ b/studio/components/interfaces/Database/Backups/BackupItem.tsx @@ -5,7 +5,6 @@ import { useRouter } from 'next/router' import { Badge, Button, IconDownload } from 'ui' import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' -import { useBackupDownloadMutation } from 'data/database/backup-download-mutation' import { useBackupRestoreMutation } from 'data/database/backup-restore-mutation' import { setProjectStatus } from 'data/projects/projects-query' import { useCheckPermissions, useStore } from 'hooks' @@ -42,19 +41,6 @@ const BackupItem = ({ projectRef, backup, index }: BackupItemProps) => { }, }) - const { mutate: downloadBackup, isLoading: isDownloading } = useBackupDownloadMutation({ - onSuccess: (res) => { - const { fileUrl } = res - - // Trigger browser download by create,trigger and remove tempLink - const tempLink = document.createElement('a') - tempLink.href = fileUrl - document.body.appendChild(tempLink) - tempLink.click() - document.body.removeChild(tempLink) - }, - }) - const onRestoreClick = () => { confirmAlert({ title: 'Confirm to restore', @@ -69,24 +55,12 @@ const BackupItem = ({ projectRef, backup, index }: BackupItemProps) => { if (backup.status === 'COMPLETED') return (
- {backup.data.canRestore && ( - - )} -
) diff --git a/studio/data/database/backup-restore-mutation.ts b/studio/data/database/backup-restore-mutation.ts index 983f666be2c..9b4db4a4900 100644 --- a/studio/data/database/backup-restore-mutation.ts +++ b/studio/data/database/backup-restore-mutation.ts @@ -16,7 +16,7 @@ export type Backup = components['schemas']['Backup'] export async function restoreFromBackup({ ref, backup }: BackupRestoreVariables) { const { data, error } = await post('/platform/database/{ref}/backups/restore', { params: { path: { ref } }, - body: { ...backup, status: backup.status as unknown as string }, + body: { id: backup.id, isPhysicalBackup: backup.isPhysicalBackup }, }) if (error) throw error return data From cfe5eb61cb81c2eecd24e5a531ae5c3a55beecb1 Mon Sep 17 00:00:00 2001 From: dragarcia Date: Mon, 28 Aug 2023 17:06:10 +0800 Subject: [PATCH 015/246] chore: clean up unused import --- studio/components/interfaces/Database/Backups/BackupItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/components/interfaces/Database/Backups/BackupItem.tsx b/studio/components/interfaces/Database/Backups/BackupItem.tsx index 4e447f635d3..45041016a34 100644 --- a/studio/components/interfaces/Database/Backups/BackupItem.tsx +++ b/studio/components/interfaces/Database/Backups/BackupItem.tsx @@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' import { useRouter } from 'next/router' -import { Badge, Button, IconDownload } from 'ui' +import { Badge, Button } from 'ui' import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' import { useBackupRestoreMutation } from 'data/database/backup-restore-mutation' From 7f6d1fb56a95182067f3013adacc2fba325b9838 Mon Sep 17 00:00:00 2001 From: dragarcia Date: Mon, 28 Aug 2023 17:57:53 +0800 Subject: [PATCH 016/246] chore: update api.d.ts --- studio/data/api.d.ts | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/studio/data/api.d.ts b/studio/data/api.d.ts index 30c7c2551c9..1af1d2a2fb6 100644 --- a/studio/data/api.d.ts +++ b/studio/data/api.d.ts @@ -214,9 +214,13 @@ export interface paths { "/platform/organizations/{slug}/billing/subscription": { /** Gets the current subscription */ get: operations["SubscriptionController_getSubscription"]; - /** Updates subscription */ + /** Previews subscription change */ put: operations["SubscriptionController_updateSubscription"]; }; + "/platform/organizations/{slug}/billing/subscription/preview": { + /** Updates subscription */ + post: operations["SubscriptionController_previewSubscriptionChange"]; + }; "/platform/organizations/{slug}/billing/plans": { /** Gets subscription plans */ get: operations["OrgPlansController_getAvailablePlans"]; @@ -1818,6 +1822,8 @@ export interface components { SMS_PROVIDER?: string; SMS_MESSAGEBIRD_ACCESS_KEY?: string; SMS_MESSAGEBIRD_ORIGINATOR?: string; + SMS_TEST_OTP?: string; + SMS_TEST_OTP_VALID_UNTIL?: string; SMS_TEXTLOCAL_API_KEY?: string; SMS_TEXTLOCAL_SENDER?: string; SMS_TWILIO_ACCOUNT_SID?: string; @@ -2058,11 +2064,9 @@ export interface components { }; Backup: { id: number; - data: Record; + isPhysicalBackup: boolean; project_id: number; status: Record; - s3_path: string; - s3_bucket: string; inserted_at: string; }; BackupsResponse: { @@ -2070,6 +2074,7 @@ export interface components { tierKey: string; region: string; walg_enabled: boolean; + pitr_enabled: boolean; backups: (components["schemas"]["Backup"])[]; physicalBackupData: { earliestPhysicalBackupDateUnix?: number; @@ -2090,12 +2095,7 @@ export interface components { }; RestoreBackupBody: { id: number; - data: Record; - inserted_at: string; - project_id: number; - s3_bucket: string; - s3_path: string; - status: string; + isPhysicalBackup: boolean; }; PointInTimeRestoreBody: { recovery_time_target_unix: number; @@ -5463,6 +5463,26 @@ export interface operations { 500: never; }; }; + /** Updates subscription */ + SubscriptionController_previewSubscriptionChange: { + parameters: { + path: { + /** @description Organization slug */ + slug: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateSubscriptionBody"]; + }; + }; + responses: { + 201: never; + 403: never; + /** @description Failed to update subscription */ + 500: never; + }; + }; /** Gets subscription plans */ OrgPlansController_getAvailablePlans: { parameters: { From ecf244bc8aa2ecd5158eddb620a521bace1396ee Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 29 Aug 2023 13:40:41 +0800 Subject: [PATCH 017/246] Delete unused backup download mutation --- .../data/database/backup-download-mutation.ts | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 studio/data/database/backup-download-mutation.ts diff --git a/studio/data/database/backup-download-mutation.ts b/studio/data/database/backup-download-mutation.ts deleted file mode 100644 index 565fbb72de2..00000000000 --- a/studio/data/database/backup-download-mutation.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useMutation, UseMutationOptions } from '@tanstack/react-query' -import { toast } from 'react-hot-toast' - -import { post } from 'data/fetchers' -import { ResponseError } from 'types' -import { Backup } from './backup-restore-mutation' - -export type BackupDownloadVariables = { - ref: string - backup: Backup -} - -export async function downloadBackup({ ref, backup }: BackupDownloadVariables) { - const { data, error } = await post('/platform/database/{ref}/backups/download', { - params: { path: { ref } }, - body: { ...backup, status: backup.status as unknown as string }, - }) - if (error) throw error - return data -} - -type BackupDownloadData = Awaited> - -export const useBackupDownloadMutation = ({ - onSuccess, - onError, - ...options -}: Omit< - UseMutationOptions, - 'mutationFn' -> = {}) => { - return useMutation( - (vars) => downloadBackup(vars), - { - async onSuccess(data, variables, context) { - await onSuccess?.(data, variables, context) - }, - async onError(data, variables, context) { - if (onError === undefined) { - toast.error(`Failed to download backup: ${data.message}`) - } else { - onError(data, variables, context) - } - }, - ...options, - } - ) -} From 777c5b407a6ee1fbd8dee15a8219d63a81248e45 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 25 Aug 2023 14:24:14 +0200 Subject: [PATCH 018/246] Add a modal for adding new factor. --- .../interfaces/Account/AddNewFactorModal.tsx | 201 ++++++++++++++++++ studio/components/interfaces/Account/index.ts | 5 +- studio/components/ui/ConfirmationModal.tsx | 4 +- studio/data/profile/keys.ts | 1 + .../mfa-challenge-and-verify-mutation.ts | 32 +++ studio/data/profile/mfa-enroll-mutation.ts | 22 ++ studio/data/profile/mfa-list-factors-query.ts | 31 +++ studio/data/profile/mfa-unenroll-mutation.ts | 33 +++ 8 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 studio/components/interfaces/Account/AddNewFactorModal.tsx create mode 100644 studio/data/profile/mfa-challenge-and-verify-mutation.ts create mode 100644 studio/data/profile/mfa-enroll-mutation.ts create mode 100644 studio/data/profile/mfa-list-factors-query.ts create mode 100644 studio/data/profile/mfa-unenroll-mutation.ts diff --git a/studio/components/interfaces/Account/AddNewFactorModal.tsx b/studio/components/interfaces/Account/AddNewFactorModal.tsx new file mode 100644 index 00000000000..822b59b3b11 --- /dev/null +++ b/studio/components/interfaces/Account/AddNewFactorModal.tsx @@ -0,0 +1,201 @@ +import ConfirmationModal from 'components/ui/ConfirmationModal' +import InformationBox from 'components/ui/InformationBox' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import { useMfaChallengeAndVerifyMutation } from 'data/profile/mfa-challenge-and-verify-mutation' +import { useMfaEnrollMutation } from 'data/profile/mfa-enroll-mutation' +import { useMfaUnenrollMutation } from 'data/profile/mfa-unenroll-mutation' +import { useStore } from 'hooks' +import Image from 'next/image' +import { Dispatch, SetStateAction, useEffect, useState } from 'react' +import { Input, Modal } from 'ui' + +const AddNewFactorModal = ({ onClose }: { onClose: () => void }) => { + const [verificationComplete, setVerificationComplete] = useState(false) + // Generate a name with a number between 0 and 1000 + const [name, setName] = useState(`App ${Math.floor(Math.random() * 1000)}`) + + const { + mutate: enroll, + data, + reset: resetEnrollment, + isLoading: isEnrolling, + } = useMfaEnrollMutation() + const { mutate: unenroll } = useMfaUnenrollMutation() + + useEffect(() => { + return () => { + // when the modal is closed, if there's a factor id which hasn't been verified, unenroll it + if (data?.id && !verificationComplete) { + // delete the internal state of enroll mutation so that the next time enroll is called, + // it makes an API call + resetEnrollment() + unenroll({ factorId: data.id }) + } + } + }, []) + + return !data ? ( + + ) : ( + { + setVerificationComplete(true) + onClose() + }} + onClose={onClose} + /> + ) +} + +const FirstStep = ({ + name, + setName, + enroll, + onClose, +}: { + name: string + setName: Dispatch> + enroll: (params: { factorType: 'totp'; friendlyName?: string }) => void + onClose: () => void +}) => { + return ( + { + enroll({ + factorType: 'totp', + friendlyName: name, + }) + }} + > + + <> +
+ setName(e.target.value)} + /> +
+ +
+
+ ) +} + +const SecondStep = ({ + factorName, + factor, + isLoading, + onSuccess, + onClose, +}: { + factorName: string + factor: { + id: string + type: 'totp' + totp: { + qr_code: string + secret: string + uri: string + } + } + isLoading: boolean + onSuccess: () => void + onClose: () => void +}) => { + const [code, setCode] = useState('') + + const { ui } = useStore() + + const { + mutate: challengeAndVerify, + isLoading: isVerifying, + isSuccess: verificationComplete, + } = useMfaChallengeAndVerifyMutation({ + onError: (error) => { + ui.setNotification({ + category: 'error', + message: `Failed to add a second factor authentication: ${error?.message}`, + }) + }, + onSuccess: () => { + ui.setNotification({ + category: 'success', + message: `Successfully added a second factor authentication`, + }) + onSuccess() + }, + }) + + return ( + challengeAndVerify({ factorId: factor.id, code })} + > + + <> +
+ + Use an authenticator app to scan the following QR code, and provide the code from the + app to complete the enrolment. + +
+ {isLoading ? ( +
+ +
+ ) : ( + <> +
+
+ {factor.totp.uri} +
+
+
+ + } + /> +
+
+ setCode(e.target.value)} + /> +
+ + )} + +
+
+ ) +} + +export default AddNewFactorModal diff --git a/studio/components/interfaces/Account/index.ts b/studio/components/interfaces/Account/index.ts index 4ce89542872..38c906b34f7 100644 --- a/studio/components/interfaces/Account/index.ts +++ b/studio/components/interfaces/Account/index.ts @@ -1,5 +1,6 @@ import AccessTokenList from './AccessTokenList' -import NewTokenBanner from './NewTokenBanner' +import AddNewFactorModal from './AddNewFactorModal' import NewAccessTokenButton from './NewAccessTokenButton' +import NewTokenBanner from './NewTokenBanner' -export { AccessTokenList, NewTokenBanner, NewAccessTokenButton } +export { AccessTokenList, NewTokenBanner, NewAccessTokenButton, AddNewFactorModal } diff --git a/studio/components/ui/ConfirmationModal.tsx b/studio/components/ui/ConfirmationModal.tsx index 842572933c0..940fdafd810 100644 --- a/studio/components/ui/ConfirmationModal.tsx +++ b/studio/components/ui/ConfirmationModal.tsx @@ -10,6 +10,7 @@ export interface ConfirmationModalProps { size?: 'small' | 'tiny' | 'medium' | 'large' buttonLabel: string buttonLoadingLabel?: string + buttonDisabled?: boolean onSelectCancel: () => void onSelectConfirm: () => void } @@ -23,6 +24,7 @@ const ConfirmationModal = ({ size = 'small', buttonLabel = '', buttonLoadingLabel = '', + buttonDisabled = false, onSelectCancel = () => {}, onSelectConfirm = () => {}, children, @@ -62,7 +64,7 @@ const ConfirmationModal = ({
+ ) +} + +Security.getLayout = (page) => ( + + {page} + +) + +export default Security + +const ProfileCard = () => { + const [isAddNewFactorOpen, setIsAddNewFactorOpen] = useState(false) + const [factorToBeDeleted, setFactorToBeDeleted] = useState(null) + const { data, isLoading, isError, isSuccess, error } = useMfaListFactorsQuery() + + return ( + <> +
+
+ + Generate one-time passwords via authenticator apps like 1Password, Authy, etc. as a + second factor to verify your identity when prompted during sign-in. + +
+ {isLoading && ( +
+ {[1, 2].map((number) => { + return + })} +
+ )} + {isError && ( +
+ +
+ )} + {isSuccess && ( + <> +
+ {data.totp.map((factor) => { + return ( +
+
+ Factor ID: {factor.id} + + Name: {factor.friendly_name ?? 'No name provided'} + +
+
+ + Created at {new Date(factor.updated_at).toLocaleString()} + +
+
+ ) + })} +
+ {data.totp.length < 2 ? ( + <> +
+ +
+ + ) : null} + + )} +
+ {isAddNewFactorOpen && setIsAddNewFactorOpen(false)} />} + {factorToBeDeleted && ( + setFactorToBeDeleted(null)} + /> + )} + + ) +} + +const DeleteFactorModal = ({ + factorId, + lastFactorToBeDeleted, + onClose, +}: { + factorId: string + lastFactorToBeDeleted: boolean + onClose: () => void +}) => { + const { ui } = useStore() + + const { mutate: unenroll, isLoading } = useMfaUnenrollMutation({ + onError: (error) => { + ui.setNotification({ + category: 'error', + message: `Failed to delete a second factor authentication: ${error?.message}`, + }) + }, + onSuccess: () => { + ui.setNotification({ + category: 'success', + message: `Successfully deleted a second factor authentication.`, + }) + onClose() + }, + }) + + return ( + { + unenroll({ factorId }) + }} + > + + + + + {lastFactorToBeDeleted + ? 'Multi-factor authentication will be disabled' + : 'This action cannot be undone'} + + + {lastFactorToBeDeleted + ? 'There are no other factors that are set up once you delete this factor, as such your account will no longer be guarded by multi-factor authentication' + : 'You will no longer be able to use this authenticator app for multi-factor authentication when signing in to the dashboard'} + + +
+ Before deleting this factor, consider: +
    + {lastFactorToBeDeleted ? ( + <> +
  • Adding another authenticator app as a factor prior to deleting
  • +
  • Ensure that your account does not need multi-factor authentication
  • + + ) : ( + <> +
  • Your backup authenticator app is still available to use
  • +
  • Adding another authenticator app thereafter as a backup
  • + + )} +
+
+
+
+ ) +} From 5aac02b441af446f01dd575fc8e39b9a39e2ddb9 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 25 Aug 2023 14:26:02 +0200 Subject: [PATCH 020/246] Simplify the sign-in page. --- studio/pages/sign-in.tsx | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/studio/pages/sign-in.tsx b/studio/pages/sign-in.tsx index 39270ebdf65..802b60eed2d 100644 --- a/studio/pages/sign-in.tsx +++ b/studio/pages/sign-in.tsx @@ -1,37 +1,19 @@ import Link from 'next/link' -import { NextPageWithLayout } from 'types' import SignInForm from 'components/interfaces/SignIn/SignInForm' import SignInWithGitHub from 'components/interfaces/SignIn/SignInWithGitHub' import SignInWithSSO from 'components/interfaces/SignIn/SignInWithSSO' import { SignInLayout } from 'components/layouts' -import { useEffect } from 'react' import { IS_PLATFORM } from 'lib/constants' import { useRouter } from 'next/router' -import { auth } from 'lib/gotrue' +import { NextPageWithLayout } from 'types' const SignInPage: NextPageWithLayout = () => { const router = useRouter() - useEffect(() => { - // if the dashboard is running locally, redirect straight to the projects page - if (!IS_PLATFORM) { - router.replace('/project/default') - return - } - - const { - data: { subscription }, - } = auth.onAuthStateChange((event) => { - if (event === 'SIGNED_IN') { - // if user has signed in on another tab, take them to the home page - router.replace('/projects') - } - }) - - return () => { - subscription.unsubscribe() - } - }, []) + if (!IS_PLATFORM) { + // on selfhosted instance just redirect to projects page + router.replace('/project/default') + } return ( <> From 7b5183016ea902371689db0444dd90e51969e52d Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 25 Aug 2023 14:42:05 +0200 Subject: [PATCH 021/246] Add a page for signing in via MFA. --- .../interfaces/SignIn/SignInMfaForm.tsx | 124 ++++++++++++++++++ studio/data/profile/keys.ts | 1 + ...mfa-authenticator-assurance-level-query.ts | 41 ++++++ .../mfa-challenge-and-verify-mutation.ts | 7 +- studio/data/profile/mfa-unenroll-mutation.ts | 7 +- studio/pages/sign-in-mfa.tsx | 25 ++++ 6 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 studio/components/interfaces/SignIn/SignInMfaForm.tsx create mode 100644 studio/data/profile/mfa-authenticator-assurance-level-query.ts create mode 100644 studio/pages/sign-in-mfa.tsx diff --git a/studio/components/interfaces/SignIn/SignInMfaForm.tsx b/studio/components/interfaces/SignIn/SignInMfaForm.tsx new file mode 100644 index 00000000000..7647c14b266 --- /dev/null +++ b/studio/components/interfaces/SignIn/SignInMfaForm.tsx @@ -0,0 +1,124 @@ +import { AuthError } from '@supabase/gotrue-js' +import { Factor } from '@supabase/supabase-js' +import { useQueryClient } from '@tanstack/react-query' +import { mfaChallengeAndVerify } from 'data/profile/mfa-challenge-and-verify-mutation' +import { useMfaListFactorsQuery } from 'data/profile/mfa-list-factors-query' +import { useStore } from 'hooks' +import { usePushNext } from 'hooks/misc/useAutoAuthRedirect' +import { useEffect, useState } from 'react' +import { Button, Form, IconLock, Input } from 'ui' +import { object, string } from 'yup' + +const signInSchema = object({ + code: string().required('MFA Code is required'), +}) + +const SignInMfaForm = () => { + const { ui } = useStore() + const pushNext = usePushNext() + const queryClient = useQueryClient() + const { data: factors, isSuccess: factorsSuccess } = useMfaListFactorsQuery() + const [selectedFactor, setSelectedFactor] = useState(null) + + // if the user wanders into this page and he has no MFA setup, send the user to the next screen + if (factorsSuccess && factors.totp.length === 0) { + queryClient.resetQueries().then(() => pushNext()) + } + + useEffect(() => { + if (factorsSuccess) { + setSelectedFactor(factors.totp[0]) + } + }, [factors?.totp, factorsSuccess]) + + const onSignIn = async ({ code }: { code: string }) => { + const toastId = ui.setNotification({ + category: 'loading', + message: `Signing in...`, + }) + if (selectedFactor) { + try { + const result = await mfaChallengeAndVerify({ factorId: selectedFactor.id, code }) + + ui.setNotification({ + id: toastId, + category: 'success', + message: `Signed in successfully!`, + }) + + await queryClient.resetQueries() + + await pushNext() + } catch (error: unknown) { + ui.setNotification({ + id: toastId, + category: 'error', + message: (error as AuthError).message, + }) + } + } + } + + return ( +
+ {({ isSubmitting }: { isSubmitting: boolean }) => { + return ( +
+ } + placeholder="XXXXXX" + disabled={isSubmitting} + autoComplete="off" + spellCheck="false" + autoCapitalize="none" + autoCorrect="off" + label={ + selectedFactor && factors?.totp.length === 2 + ? `Code generated by ${selectedFactor.friendly_name}` + : null + } + /> + + + {factors?.totp.length === 2 && ( + + )} +
+ ) + }} +
+ ) +} + +export default SignInMfaForm diff --git a/studio/data/profile/keys.ts b/studio/data/profile/keys.ts index a43a33f7a73..2ed0d876185 100644 --- a/studio/data/profile/keys.ts +++ b/studio/data/profile/keys.ts @@ -1,4 +1,5 @@ export const profileKeys = { profile: () => ['profile'] as const, mfaFactors: () => ['mfa', 'factors'] as const, + aaLevel: () => ['mfa', 'aaLevel'] as const, } diff --git a/studio/data/profile/mfa-authenticator-assurance-level-query.ts b/studio/data/profile/mfa-authenticator-assurance-level-query.ts new file mode 100644 index 00000000000..f5b2ae77113 --- /dev/null +++ b/studio/data/profile/mfa-authenticator-assurance-level-query.ts @@ -0,0 +1,41 @@ +import { AuthMFAGetAuthenticatorAssuranceLevelResponse } from '@supabase/supabase-js' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { auth } from 'lib/gotrue' +import { profileKeys } from './keys' +import { Profile } from './types' + +export type ProfileResponse = Profile + +export async function getMfaAuthenticatorAssuranceLevel() { + const { error, data } = await auth.mfa.getAuthenticatorAssuranceLevel() + + if (error) throw error + return data +} + +type CustomAuthMFAGetAuthenticatorAssuranceLevelData = NonNullable< + AuthMFAGetAuthenticatorAssuranceLevelResponse['data'] +> +type CustomAuthMFAGetAuthenticatorAssuranceLevelError = NonNullable< + AuthMFAGetAuthenticatorAssuranceLevelResponse['error'] +> + +export const useAuthenticatorAssuranceLevelQuery = < + TData = CustomAuthMFAGetAuthenticatorAssuranceLevelData +>({ + enabled = true, + ...options +}: UseQueryOptions< + CustomAuthMFAGetAuthenticatorAssuranceLevelData, + CustomAuthMFAGetAuthenticatorAssuranceLevelError, + TData +> = {}) => { + return useQuery< + CustomAuthMFAGetAuthenticatorAssuranceLevelData, + CustomAuthMFAGetAuthenticatorAssuranceLevelError, + TData + >(profileKeys.aaLevel(), () => getMfaAuthenticatorAssuranceLevel(), { + staleTime: 1000 * 60 * 30, // default good for 30 mins + ...options, + }) +} diff --git a/studio/data/profile/mfa-challenge-and-verify-mutation.ts b/studio/data/profile/mfa-challenge-and-verify-mutation.ts index 59d3c7ad6c1..21d8c721f8a 100644 --- a/studio/data/profile/mfa-challenge-and-verify-mutation.ts +++ b/studio/data/profile/mfa-challenge-and-verify-mutation.ts @@ -24,7 +24,12 @@ export const useMfaChallengeAndVerifyMutation = ({ return useMutation((vars) => mfaChallengeAndVerify(vars), { async onSuccess(data, variables, context) { - await queryClient.invalidateQueries(profileKeys.mfaFactors()) + // when a MFA is added, the aaLevel is bumped up + await Promise.all([ + queryClient.invalidateQueries(profileKeys.mfaFactors()), + queryClient.invalidateQueries(profileKeys.aaLevel()), + ]) + await onSuccess?.(data, variables, context) }, ...options, diff --git a/studio/data/profile/mfa-unenroll-mutation.ts b/studio/data/profile/mfa-unenroll-mutation.ts index 0d8bf7ff41e..903efb9f98b 100644 --- a/studio/data/profile/mfa-unenroll-mutation.ts +++ b/studio/data/profile/mfa-unenroll-mutation.ts @@ -25,7 +25,12 @@ export const useMfaUnenrollMutation = ({ return useMutation((vars) => mfaUnenroll(vars), { async onSuccess(data, variables, context) { - await queryClient.invalidateQueries(profileKeys.mfaFactors()) + // when a factor is unenrolled, the aaLevel is bumped down if it's the last factor + await Promise.all([ + queryClient.invalidateQueries(profileKeys.mfaFactors()), + queryClient.invalidateQueries(profileKeys.aaLevel()), + ]) + await onSuccess?.(data, variables, context) }, ...options, diff --git a/studio/pages/sign-in-mfa.tsx b/studio/pages/sign-in-mfa.tsx new file mode 100644 index 00000000000..aaafe13a6a7 --- /dev/null +++ b/studio/pages/sign-in-mfa.tsx @@ -0,0 +1,25 @@ +import SignInMfaForm from 'components/interfaces/SignIn/SignInMfaForm' +import { SignInLayout } from 'components/layouts' +import { NextPageWithLayout } from 'types' + +const SignInMfaPage: NextPageWithLayout = () => { + return ( + <> +
+ +
+ + ) +} + +SignInMfaPage.getLayout = (page) => ( + + {page} + +) + +export default SignInMfaPage From 7ac94601041b9f0fc718bb7068bfb5162ed00fac Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 25 Aug 2023 14:42:44 +0200 Subject: [PATCH 022/246] Redirect to MFA if the user is logging in via email and has MFA setup. --- .../interfaces/SignIn/SignInForm.tsx | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/studio/components/interfaces/SignIn/SignInForm.tsx b/studio/components/interfaces/SignIn/SignInForm.tsx index 934b401e21e..c58d2351751 100644 --- a/studio/components/interfaces/SignIn/SignInForm.tsx +++ b/studio/components/interfaces/SignIn/SignInForm.tsx @@ -1,14 +1,17 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' +import * as Sentry from '@sentry/nextjs' +import { AuthError } from '@supabase/supabase-js' import { useQueryClient } from '@tanstack/react-query' +import { getMfaAuthenticatorAssuranceLevel } from 'data/profile/mfa-authenticator-assurance-level-query' import { useStore } from 'hooks' import { usePushNext } from 'hooks/misc/useAutoAuthRedirect' import { auth } from 'lib/gotrue' import { incrementSignInClicks } from 'lib/local-storage' import Link from 'next/link' +import { useRouter } from 'next/router' import { useRef, useState } from 'react' import { Button, Form, Input } from 'ui' import { object, string } from 'yup' -import * as Sentry from '@sentry/nextjs' const signInSchema = object({ email: string().email('Must be a valid email').required('Email is required'), @@ -17,6 +20,7 @@ const signInSchema = object({ const SignInForm = () => { const { ui } = useStore() + const router = useRouter() const pushNext = usePushNext() const queryClient = useQueryClient() @@ -47,15 +51,36 @@ const SignInForm = () => { Sentry.captureMessage('Sign in without previous sign out detected') } - ui.setNotification({ - id: toastId, - category: 'success', - message: `Signed in successfully!`, - }) + try { + const data = await getMfaAuthenticatorAssuranceLevel() + if (data) { + if (data.currentLevel !== data.nextLevel) { + ui.setNotification({ + id: toastId, + category: 'success', + message: `You need to provide your second factor authentication.`, + }) + router.replace('/sign-in-mfa') + return + } + } - await queryClient.resetQueries() + ui.setNotification({ + id: toastId, + category: 'success', + message: `Signed in successfully!`, + }) - await pushNext() + await queryClient.resetQueries() + + await pushNext() + } catch (error) { + ui.setNotification({ + id: toastId, + category: 'error', + message: (error as AuthError).message, + }) + } } else { setCaptchaToken(null) captchaRef.current?.resetCaptcha() From 6fa7acd622769d7bfcf1657fb886fddaa9066cbd Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 25 Aug 2023 16:33:43 +0200 Subject: [PATCH 023/246] Change the redirects for Github and SSO to send the user back to / instead of /projects as before. It will be picked from there by the SignInLayout and WithAuth helper. --- studio/components/interfaces/SignIn/SignInSSOForm.tsx | 2 +- studio/components/interfaces/SignIn/SignInWithGitHub.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/studio/components/interfaces/SignIn/SignInSSOForm.tsx b/studio/components/interfaces/SignIn/SignInSSOForm.tsx index 3d1585c0e57..5da7eaefa02 100644 --- a/studio/components/interfaces/SignIn/SignInSSOForm.tsx +++ b/studio/components/interfaces/SignIn/SignInSSOForm.tsx @@ -38,7 +38,7 @@ const SignInSSOForm = () => { process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' ? location.origin : process.env.NEXT_PUBLIC_SITE_URL - }${BASE_PATH}${getReturnToPath()}`, + }${BASE_PATH}${getReturnToPath('/')}`, }, }) diff --git a/studio/components/interfaces/SignIn/SignInWithGitHub.tsx b/studio/components/interfaces/SignIn/SignInWithGitHub.tsx index a34d53a49b1..38edc76b4df 100644 --- a/studio/components/interfaces/SignIn/SignInWithGitHub.tsx +++ b/studio/components/interfaces/SignIn/SignInWithGitHub.tsx @@ -1,9 +1,9 @@ -import { useState } from 'react' +import * as Sentry from '@sentry/nextjs' import { BASE_PATH } from 'lib/constants' import { auth, getReturnToPath } from 'lib/gotrue' import { incrementSignInClicks } from 'lib/local-storage' +import { useState } from 'react' import { Button, IconGitHub } from 'ui' -import * as Sentry from '@sentry/nextjs' const SignInWithGitHub = () => { const [loading, setLoading] = useState(false) @@ -24,7 +24,7 @@ const SignInWithGitHub = () => { process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' ? location.origin : process.env.NEXT_PUBLIC_SITE_URL - }${BASE_PATH}${getReturnToPath()}`, + }${BASE_PATH}${getReturnToPath('/')}`, }, }) if (error) throw error From 959b38479f0ef451c0be388d7db8a6ad68520d70 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 25 Aug 2023 16:34:18 +0200 Subject: [PATCH 024/246] Redirect the user to /sign-in page if he doesn't have the appropriate AA level. --- studio/hooks/misc/withAuth.tsx | 38 ++++++++++++++++------------------ 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/studio/hooks/misc/withAuth.tsx b/studio/hooks/misc/withAuth.tsx index 21812c5c3f1..4ba0a37276b 100644 --- a/studio/hooks/misc/withAuth.tsx +++ b/studio/hooks/misc/withAuth.tsx @@ -4,11 +4,12 @@ import { ComponentType, useEffect } from 'react' import { useParams } from 'common/hooks' import { usePermissionsQuery } from 'data/permissions/permissions-query' +import { useAuthenticatorAssuranceLevelQuery } from 'data/profile/mfa-authenticator-assurance-level-query' import { useSelectedProject, useStore } from 'hooks' import { useAuth } from 'lib/auth' import { IS_PLATFORM } from 'lib/constants' -import { STORAGE_KEY, getReturnToPath } from 'lib/gotrue' -import { NextPageWithLayout, isNextPageWithLayout } from 'types' +import { getReturnToPath, STORAGE_KEY } from 'lib/gotrue' +import { isNextPageWithLayout, NextPageWithLayout } from 'types' import Error500 from '../../pages/500' const PLATFORM_ONLY_PAGES = [ @@ -19,26 +20,19 @@ const PLATFORM_ONLY_PAGES = [ 'auth/url-configuration', ] -export function withAuth( - WrappedComponent: ComponentType | NextPageWithLayout, - options?: { - redirectTo: string - /* run the redirect if the user is logged in */ - redirectIfFound?: boolean - } -) { +export function withAuth(WrappedComponent: ComponentType | NextPageWithLayout) { const WithAuthHOC: ComponentType = (props: any) => { const router = useRouter() const { basePath } = router const { ref } = useParams() const rootStore = useStore() const { isLoading, session } = useAuth() + const { isLoading: isAALLoading, data: aalData } = useAuthenticatorAssuranceLevelQuery() const { ui } = rootStore const page = router.pathname.split('/').slice(3).join('/') - const redirectTo = options?.redirectTo ?? defaultRedirectTo(ref) - const redirectIfFound = options?.redirectIfFound + const redirectTo = defaultRedirectTo(ref) usePermissionsQuery({ onError(error: any) { @@ -51,13 +45,20 @@ export function withAuth( }) const isLoggedIn = Boolean(session) + const isCorrectLevel = aalData?.currentLevel === aalData?.nextLevel const isAccessingBlockedPage = !IS_PLATFORM && PLATFORM_ONLY_PAGES.some((platformOnlyPage) => page.startsWith(platformOnlyPage)) const isRedirecting = isAccessingBlockedPage || - checkRedirectTo(isLoading, router.pathname, isLoggedIn, redirectTo, redirectIfFound) + checkRedirectTo( + isLoading || isAALLoading, + router.pathname, + isLoggedIn, + isCorrectLevel, + redirectTo + ) useEffect(() => { // This should run after setting store data @@ -114,17 +115,14 @@ function checkRedirectTo( loading: boolean, pathname: string, isLoggedIn: boolean, - redirectTo: string, - redirectIfFound?: boolean + isCorrectLevel: boolean, + redirectTo: string ) { if (loading) return false if (pathname === redirectTo) return false - // If redirectTo is set, redirect if the user is not logged in. - if (redirectTo && !redirectIfFound && !isLoggedIn) return true - - // If redirectIfFound is also set, redirect if the user was found - if (redirectIfFound && isLoggedIn) return true + // If redirectTo is set, redirect if the user is not logged in or logged in with MFA. + if (redirectTo && (!isLoggedIn || !isCorrectLevel)) return true return false } From f2f6b9681c4587f0c9b6ef03ff0944578e8bb8f4 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 25 Aug 2023 16:35:18 +0200 Subject: [PATCH 025/246] Remove quick redirect from the SignInLayout since now it doesn't mean the user is ready to work, he may have to sign in into additional factor. --- .../layouts/SignInLayout/SignInLayout.tsx | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/studio/components/layouts/SignInLayout/SignInLayout.tsx b/studio/components/layouts/SignInLayout/SignInLayout.tsx index 387c0f15167..7a25b11d9d0 100644 --- a/studio/components/layouts/SignInLayout/SignInLayout.tsx +++ b/studio/components/layouts/SignInLayout/SignInLayout.tsx @@ -1,13 +1,13 @@ import { useQueryClient } from '@tanstack/react-query' import { useTheme } from 'common' -import { useFlag, useStore } from 'hooks' +import { useFlag } from 'hooks' import { usePushNext } from 'hooks/misc/useAutoAuthRedirect' -import { BASE_PATH, IS_PLATFORM } from 'lib/constants' -import { auth, getReturnToPath, STORAGE_KEY } from 'lib/gotrue' +import { BASE_PATH } from 'lib/constants' +import { auth } from 'lib/gotrue' import { observer } from 'mobx-react-lite' -import Head from 'next/head' import Image from 'next/image' import Link from 'next/link' +import { useRouter } from 'next/router' import { PropsWithChildren, useEffect, useState } from 'react' import { tweets } from 'shared-data' import { Button, IconFileText } from 'ui' @@ -26,6 +26,7 @@ const SignInLayout = ({ logoLinkToMarketingSite = false, children, }: PropsWithChildren) => { + const router = useRouter() const pushNext = usePushNext() const queryClient = useQueryClient() const { isDarkMode } = useTheme() @@ -42,24 +43,34 @@ const SignInLayout = ({ return } - ;(async () => { - const { error } = await auth.initialize() - + auth.initialize().then(({ error }) => { if (error) { // if there was a problem signing in via the url, don't redirect return } - const { - data: { session }, - } = await auth.getSession() + auth.getSession().then(async ({ data: { session } }) => { + try { + const { data, error } = await auth.mfa.getAuthenticatorAssuranceLevel() + if (error) { + // if there was a problem signing in via the url, don't redirect + return + } - if (session) { - await queryClient.resetQueries() + if (data) { + if (data.currentLevel !== data.nextLevel) { + router.replace('/sign-in-mfa') + return + } + } - await pushNext() - } - })() + if (session) { + await queryClient.resetQueries() + await pushNext() + } + } catch {} + }) + }) }, []) const [quote, setQuote] = useState<{ @@ -77,20 +88,12 @@ const SignInLayout = ({ return ( <> - {IS_PLATFORM && ( - -