From 6789079aeed86f5c96566ef5972933f169ce3eb2 Mon Sep 17 00:00:00 2001 From: GeekyAdams Date: Wed, 28 Jun 2023 21:05:12 +0100 Subject: [PATCH 001/327] 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/327] 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 488ee2583067cb092a773509f8dc992af8430c7c Mon Sep 17 00:00:00 2001 From: Abdurrahman Rajab Date: Thu, 29 Jun 2023 19:45:41 +0300 Subject: [PATCH 003/327] chore: enable insert --- .../PolicyEditorModal.constants.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts b/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts index a24711e35e9..fcd6e52a190 100644 --- a/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts +++ b/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts @@ -83,5 +83,21 @@ FOR DELETE USING ( check: '', command: 'DELETE', roles: [], + }, { + id: 'policy-5', + templateName: 'Enable insert access for users based on their user ID *', + description: + 'This policy assumes that your table has a column "user_id", and allows users to insert rows which the "user_id" column matches their ID', + statement: ` +CREATE POLICY "policy_name" +ON ${schema}.${table} +FOR INSERT USING ( + auth.uid() = user_id +);`.trim(), + name: 'Enable insert for users based on user_id', + definition: 'auth.uid() = user_id', + check: '', + command: 'INSERT', + roles: [], }, ] From f30a7181efb57cbfb7ff28a07c56271289f7d599 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Fri, 30 Jun 2023 11:29:28 -0230 Subject: [PATCH 004/327] 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 99228d9a2f0f9a3913a5fbafa5f08f7a6d1dd9e6 Mon Sep 17 00:00:00 2001 From: Denis Grafov Date: Mon, 3 Jul 2023 16:22:46 +0400 Subject: [PATCH 005/327] Update storage_management.mdx --- apps/docs/components/MDX/storage_management.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/docs/components/MDX/storage_management.mdx b/apps/docs/components/MDX/storage_management.mdx index ce28a1684c8..94aaa57a999 100644 --- a/apps/docs/components/MDX/storage_management.mdx +++ b/apps/docs/components/MDX/storage_management.mdx @@ -61,13 +61,16 @@ as $$ declare status int; content text; + avatar_name text; begin if coalesce(old.avatar_url, '') <> '' and (tg_op = 'DELETE' or (old.avatar_url <> new.avatar_url)) then + -- extract avatar name + avatar_name := substring(old.avatar_url from '/([^\/]+)\?.*$'); select into status, content result.status, result.content - from public.delete_avatar(old.avatar_url) as result; + from public.delete_avatar(avatar_name) as result; if status <> 200 then raise warning 'Could not delete avatar: % %', status, content; end if; From d0d295da9ecc2f0396e70080c53ea35cc7f8216f Mon Sep 17 00:00:00 2001 From: Zach Blume Date: Wed, 5 Jul 2023 20:16:46 -0400 Subject: [PATCH 006/327] 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 007/327] 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 cd9b301220c9f44e2c361c0b85c27b7f04ddaadf Mon Sep 17 00:00:00 2001 From: Abdurrahman Rajab Date: Thu, 3 Aug 2023 22:39:09 +0300 Subject: [PATCH 008/327] feat: add check --- .../Policies/PolicyEditorModal/PolicyEditorModal.constants.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts b/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts index fcd6e52a190..0b82ea98cf8 100644 --- a/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts +++ b/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts @@ -93,10 +93,12 @@ CREATE POLICY "policy_name" ON ${schema}.${table} FOR INSERT USING ( auth.uid() = user_id +) WITH CHECK ( + auth.uid() = user_id );`.trim(), name: 'Enable insert for users based on user_id', definition: 'auth.uid() = user_id', - check: '', + check: 'true', command: 'INSERT', roles: [], }, From 1a40b38610ed4937927d97d3bbbe1607fa0e1918 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Tue, 22 Aug 2023 03:01:25 +0000 Subject: [PATCH 009/327] Add email based auth with PKCE flow --- .../NavigationMenu.constants.ts | 4 + ...mail-based-auth-with-pkce-flow-for-ssr.mdx | 240 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 37803d4823b..2a5991b0882 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -481,6 +481,10 @@ export const auth = { }, { name: 'Password Reset', url: '/guides/auth/auth-password-reset' }, { name: 'Email Templates', url: '/guides/auth/auth-email-templates' }, + { + name: 'Email Auth with PKCE flow for SSR', + url: '/guides/auth/email-based-auth-with-pkce-flow-for-ssr', + }, ], }, { diff --git a/apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx b/apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx new file mode 100644 index 00000000000..6350914c336 --- /dev/null +++ b/apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx @@ -0,0 +1,240 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + title: 'Email Auth with PKCE flow for SSR', + description: 'Learn how to configure email authentication in your server-side rendering (SSR) application to work with the PKCE flow.', +} + +- Install Supabase Auth Helpers +- Set environment variables +- Setting up the Auth Helpers +- Create API endpoint for handling `token_hash` +- Update email templates with URL for API endpoint + +### Install Supabase Auth Helpers + +The Auth Helpers will help you in getting user authentication working in your favourite server-side rendering (SSR) framework. + + + +```bash +npm install @supabase/auth-helpers-nextjs @supabase/supabase-js +``` + + +```bash +npm install @supabase/auth-helpers-sveltekit @supabase/supabase-js +``` + + + +### Set environment variables + +Create an `.env.local` file in your project root directory. You can get your `SITE_URL` and `ANON_KEY` from inside of the [dashboard](https://supabase.com/dashboard/project/_/settings/api). + + + +```bash .env.local +NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +``` + + +```bash .env.local +PUBLIC_SUPABASE_URL=your_supabase_project_url +PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +``` + + + +### Setting up the Auth Helpers + +When using the Supabase client on the server, you must perform extra steps to ensure the user's auth session remains active. Since the user's session is tracked in a cookie, we need to read this cookie and update it if necessary. + + + +Next.js Server Components allow you to read a cookie but not write back to it. Middleware on the other hand allow you to both read and write to cookies. + +Next.js [Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) runs immediately before each route is rendered. We'll use Middleware to refresh the user's session before loading Server Component routes. + +Create a new `middleware.js` file in the root of your project and populate with the following: + +```js middleware.js +import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs' +import { NextResponse } from 'next/server' + +export async function middleware(req) { + const res = NextResponse.next() + const supabase = createMiddlewareClient({ req, res }) + await supabase.auth.getSession() + return res +} +``` + + +Create a new `hooks.server.js` file in the root of your project and populate with the following: + +```ts src/hooks.server.js +import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' +import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit' +import type { Handle } from '@sveltejs/kit' + +export const handle: Handle = async ({ event, resolve }) => { + event.locals.supabase = createSupabaseServerClient({ + supabaseUrl: PUBLIC_SUPABASE_URL, + supabaseKey: PUBLIC_SUPABASE_ANON_KEY, + event, + }) + + /** + * a little helper that is written for convenience so that instead + * of calling `const { data: { session } } = await supabase.auth.getSession()` + * you just call this `await getSession()` + */ + event.locals.getSession = async () => { + const { + data: { session }, + } = await event.locals.supabase.auth.getSession() + return session + } + + return resolve(event, { + filterSerializedResponseHeaders(name) { + return name === 'content-range' + }, + }) +} +``` + + + +### Create API endpoint for handling `token_hash` + +In order to use the updated email links we will need to setup a endpoint for verifying the `token_hash` along with the `type` to exchange `token_hash` for the user's `session`, which is set as a cookie for future requests made to Supabase. + + + +Create a new file at `app/auth/confirm/route.js` and populate with the following: + +```js app/auth/confirm/route.js +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +export async function GET(req) { + const { searchParams } = new URL(req.url); + const token_hash = searchParams.get("token_hash"); + const type = searchParams.get("type"); + const next = searchParams.get("next") ?? "/"; + + if (token_hash && type) { + const supabase = createRouteHandlerClient({ cookies }); + await supabase.auth.verifyOtp({ type, token_hash }); + } + + return NextResponse.redirect(new URL(`/${next.slice(1)}`, req.url)); +} +``` + + +Create a new file at `src/routes/auth/confirm/+server.js` and populate with the following: + +```js src/routes/auth/confirm/+server.js +import { redirect } from '@sveltejs/kit'; + +export const GET = async (event) => { + const { + url, + locals: { supabase } + } = event; + const token_hash = url.searchParams.get('token') as string; + const type = url.searchParams.get('type') as string; + const next = url.searchParams.get('next') ?? '/'; + + if (token_hash && type) { + const { error } = await supabase.auth.verifyOtp({ token_hash, type }); + if (!error) { + throw redirect(303, `/${next.slice(1)}`); + } + } + + // return to the sign in screen + throw redirect(303, '/auth/signin'); +}; +``` + + + +### Update email templates with URL for API endpoint + +Let's update the URL in our email templates to point to our new confirmation endpoint for the user to get confirmed. + +**Confirm signup template** + +```html +

Confirm your signup

+ +

Follow this link to confirm your user:

+

Confirm your email

+``` + +**Invite user template** + +```html +

You have been invited

+ +

You have been invited to create a user on {{ .SiteURL }}. Follow this link to accept the invite:

+ +

Accept the invite

+``` + +**Magic Link template** + +```html +

Magic Link

+ +

Follow this link to login:

+

Log In

+``` + +**Change Email Address template** + +```html +

Confirm Change of Email

+ +

Follow this link to confirm the update of your email from {{ .Email }} to {{ .NewEmail }}:

+

Change Email

+``` + +**Reset Password template** + +```html +

Reset Password

+ +

Follow this link to reset the password for your user:

+

Reset Password

+``` + +export const Page = ({ children }) => + +export default Page From 35a13a8c7aa61aaf7c0d7ac24bb5f2a268913845 Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Wed, 23 Aug 2023 03:16:10 +0800 Subject: [PATCH 010/327] fix: update vector to set postgres logs to LOG instead of INFO --- docker/volumes/logs/vector.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/volumes/logs/vector.yml b/docker/volumes/logs/vector.yml index 52394f8fc0f..3b664deb6d2 100644 --- a/docker/volumes/logs/vector.yml +++ b/docker/volumes/logs/vector.yml @@ -135,6 +135,9 @@ transforms: .metadata.host = "db-default" .metadata.parsed.timestamp = .timestamp .metadata.parsed.error_severity = replace!(.severity, r'^err$', "warning") + if .metadata.parsed.error_severity == "info" { + .metadata.parsed.error_severity = "log" + } sinks: logflare_auth: From bf7cc8f66be4a02fe5ab3ecf1a1822cd75840c56 Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Wed, 23 Aug 2023 03:25:21 +0800 Subject: [PATCH 011/327] fix: add upcase to log level --- docker/volumes/logs/vector.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/volumes/logs/vector.yml b/docker/volumes/logs/vector.yml index 3b664deb6d2..2b925a56c05 100644 --- a/docker/volumes/logs/vector.yml +++ b/docker/volumes/logs/vector.yml @@ -138,6 +138,7 @@ transforms: if .metadata.parsed.error_severity == "info" { .metadata.parsed.error_severity = "log" } + .metadata.parsed.error_severity = upcase(.metadata.parsed.error_severity) sinks: logflare_auth: From 23b858524ef1caa68fe7158edb84847f36a8abe1 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Tue, 22 Aug 2023 23:39:42 +0000 Subject: [PATCH 012/327] Update the first sentence --- .../guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx b/apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx index 6350914c336..1c842286d9a 100644 --- a/apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx +++ b/apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx @@ -13,7 +13,7 @@ export const meta = { ### Install Supabase Auth Helpers -The Auth Helpers will help you in getting user authentication working in your favourite server-side rendering (SSR) framework. +The Auth Helpers will assist you in implementing user authentication within your server-side rendering (SSR) framework. Date: Wed, 23 Aug 2023 01:26:40 +0000 Subject: [PATCH 013/327] Add oauth based auth with PKCE flow --- .../NavigationMenu.constants.ts | 1 + .../auth/oauth-with-pkce-flow-for-ssr.mdx | 192 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 apps/docs/pages/guides/auth/oauth-with-pkce-flow-for-ssr.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 37803d4823b..8247affb1c0 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -481,6 +481,7 @@ export const auth = { }, { name: 'Password Reset', url: '/guides/auth/auth-password-reset' }, { name: 'Email Templates', url: '/guides/auth/auth-email-templates' }, + { name: 'OAuth with PKCE flow for SSR', url: '/guides/auth/oauth-with-pkce-flow-for-ssr' }, ], }, { diff --git a/apps/docs/pages/guides/auth/oauth-with-pkce-flow-for-ssr.mdx b/apps/docs/pages/guides/auth/oauth-with-pkce-flow-for-ssr.mdx new file mode 100644 index 00000000000..bef2287eaf9 --- /dev/null +++ b/apps/docs/pages/guides/auth/oauth-with-pkce-flow-for-ssr.mdx @@ -0,0 +1,192 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + title: 'OAuth with PKCE flow for SSR', + description: 'Learn how to configure OAuth authentication in your server-side rendering (SSR) application to work with the PKCE flow.', +} + +### Install Supabase Auth Helpers + +The Auth Helpers will assist you in implementing user authentication within your server-side rendering (SSR) framework. + + + +```bash +npm install @supabase/auth-helpers-nextjs @supabase/supabase-js +``` + + +```bash +npm install @supabase/auth-helpers-sveltekit @supabase/supabase-js +``` + + + +### Set environment variables + +Create an `.env.local` file in your project root directory. You can get your `SITE_URL` and `ANON_KEY` from inside of the [dashboard](https://supabase.com/dashboard/project/_/settings/api). + + + +```bash .env.local +NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +``` + + +```bash .env.local +PUBLIC_SUPABASE_URL=your_supabase_project_url +PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +``` + + + +### Setting up the Auth Helpers + +When using the Supabase client on the server, you must perform extra steps to ensure the user's auth session remains active. Since the user's session is tracked in a cookie, we need to read this cookie and update it if necessary. + + + +Next.js Server Components allow you to read a cookie but not write back to it. Middleware on the other hand allow you to both read and write to cookies. + +Next.js [Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) runs immediately before each route is rendered. We'll use Middleware to refresh the user's session before loading Server Component routes. + +Create a new `middleware.js` file in the root of your project and populate with the following: + +```js middleware.js +import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs' +import { NextResponse } from 'next/server' + +export async function middleware(req) { + const res = NextResponse.next() + const supabase = createMiddlewareClient({ req, res }) + await supabase.auth.getSession() + return res +} +``` + + +Create a new `hooks.server.js` file in the root of your project and populate with the following: + +```ts src/hooks.server.js +import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' +import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit' +import type { Handle } from '@sveltejs/kit' + +export const handle: Handle = async ({ event, resolve }) => { + event.locals.supabase = createSupabaseServerClient({ + supabaseUrl: PUBLIC_SUPABASE_URL, + supabaseKey: PUBLIC_SUPABASE_ANON_KEY, + event, + }) + + event.locals.getSession = async () => { + const { + data: { session }, + } = await event.locals.supabase.auth.getSession() + return session + } + + return resolve(event, { + filterSerializedResponseHeaders(name) { + return name === 'content-range' + }, + }) +} +``` + + + +### Create API endpoint for handling the `code` exchange + +In order to use OAuth we will need to setup a endpoint for the `code` exchange, to exchange an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase. + + + +Create a new file at `app/auth/callback/route.js` and populate with the following: + +```js app/auth/callback/route.js +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +export async function GET(req) { + const { searchParams } = new URL(req.url); + const code = searchParams.get("code"); + const next = searchParams.get("next") ?? "/"; + + if (code) { + const supabase = createRouteHandlerClient({ cookies: () => cookies() }); + const { error } = await supabase.auth.exchangeCodeForSession(code); + if (!error) { + return NextResponse.redirect(new URL(`/${next.slice(1)}`, req.url)); + } + } + + // return the user to an error page with instructions + return NextResponse.redirect(new URL('/auth/auth-code-error', req.url)); +} +``` + + +Create a new file at `src/routes/auth/callback/+server.js` and populate with the following: + +```js src/routes/auth/callback/+server.js +import { redirect } from '@sveltejs/kit'; + +export const GET = async (event) => { + const { + url, + locals: { supabase } + } = event; + const code = url.searchParams.get('code') as string; + const next = url.searchParams.get('next') ?? '/'; + + if (code) { + const { error } = await supabase.auth.exchangeCodeForSession(code) + if (!error) { + throw redirect(303, `/${next.slice(1)}`); + } + } + + // return the user to an error page with instructions + throw redirect(303, '/auth/auth-code-error'); +}; +``` + + + +Let's point our `.signInWithOAuth` method's redirect to the callback route we create above: + +```js +await supabase.auth.signInWithOAuth({ + email, + options: { + redirectTo: `http://example.com/auth/callback` + } +}) +``` + +export const Page = ({ children }) => + +export default Page From 1b56873ea903698c7a33f5fdae16e866c23c4564 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Wed, 23 Aug 2023 01:36:42 +0000 Subject: [PATCH 014/327] Remove bullet points from docs --- ...mail-based-auth-with-pkce-flow-for-ssr.mdx | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx b/apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx index 1c842286d9a..208660ed982 100644 --- a/apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx +++ b/apps/docs/pages/guides/auth/email-based-auth-with-pkce-flow-for-ssr.mdx @@ -5,12 +5,6 @@ export const meta = { description: 'Learn how to configure email authentication in your server-side rendering (SSR) application to work with the PKCE flow.', } -- Install Supabase Auth Helpers -- Set environment variables -- Setting up the Auth Helpers -- Create API endpoint for handling `token_hash` -- Update email templates with URL for API endpoint - ### Install Supabase Auth Helpers The Auth Helpers will assist you in implementing user authentication within your server-side rendering (SSR) framework. @@ -101,11 +95,6 @@ export const handle: Handle = async ({ event, resolve }) => { event, }) - /** - * a little helper that is written for convenience so that instead - * of calling `const { data: { session } } = await supabase.auth.getSession()` - * you just call this `await getSession()` - */ event.locals.getSession = async () => { const { data: { session }, @@ -149,10 +138,14 @@ export async function GET(req) { if (token_hash && type) { const supabase = createRouteHandlerClient({ cookies }); - await supabase.auth.verifyOtp({ type, token_hash }); + const { error } = await supabase.auth.verifyOtp({ type, token_hash }); + if (!error) { + return NextResponse.redirect(new URL(`/${next.slice(1)}`, req.url)); + } } - return NextResponse.redirect(new URL(`/${next.slice(1)}`, req.url)); + // return the user to an error page with some instructions + return NextResponse.redirect(new URL('/auth/auth-code-error', req.url)); } ``` @@ -178,8 +171,8 @@ export const GET = async (event) => { } } - // return to the sign in screen - throw redirect(303, '/auth/signin'); + // return the user to an error page with some instructions + throw redirect(303, '/auth/auth-code-error'); }; ``` 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 015/327] 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 016/327] 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 017/327] 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 d7548f43d29a66e8cb841efa9ca584172873a763 Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Sat, 26 Aug 2023 13:09:05 +0200 Subject: [PATCH 018/327] Update kotlin docs --- apps/docs/docs/ref/kotlin/installing.mdx | 2 +- apps/docs/docs/ref/kotlin/introduction.mdx | 41 +++++++++++++++------- spec/supabase_kt_v0.yml | 6 +++- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/apps/docs/docs/ref/kotlin/installing.mdx b/apps/docs/docs/ref/kotlin/installing.mdx index 72e10be54fd..07125a7c0ac 100644 --- a/apps/docs/docs/ref/kotlin/installing.mdx +++ b/apps/docs/docs/ref/kotlin/installing.mdx @@ -49,7 +49,7 @@ custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supab - The available modules are: **gotrue-kt**, **realtime-kt**, **storage-kt**, **functions-kt**, **postgrest-kt** and **apollo-graphql** + The available modules are: **gotrue-kt**, **realtime-kt**, **storage-kt**, **functions-kt**, **postgrest-kt**, **apollo-graphql**, [**compose-auth**](https://github.com/supabase-community/supabase-kt/tree/master/plugins/ComposeAuth) and [**compose-auth-ui**](https://github.com/supabase-community/supabase-kt/tree/master/plugins/ComposeAuthUI) When using multiple modules, you can also use the BOM dependency to ensure that all modules use the same version: diff --git a/apps/docs/docs/ref/kotlin/introduction.mdx b/apps/docs/docs/ref/kotlin/introduction.mdx index 914310ba347..5903e53382f 100644 --- a/apps/docs/docs/ref/kotlin/introduction.mdx +++ b/apps/docs/docs/ref/kotlin/introduction.mdx @@ -15,24 +15,39 @@ hideTitle: true
This reference documents every object and method available in Supabase's Kotlin Multiplatform library, [supabase-kt](https://github.com/supabase-community/supabase-kt). You can use supabase-kt to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files. +Supported targets: -Supported Kotlin targets: +| | **GoTrue** | **Realtime** | **Postgrest** | **Storage** | **Functions** | **Apollo-GraphQL** | **Compose Auth** | **Compose Auth UI** | +| ----------- | ---------- | ------------ | ------------- | ----------- | ------------- | ------------------ | ---------------- | ------------------- | +| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ☑️ | ✅ | +| **Android** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **JS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ☑️ | ✅ | +| **IOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **tvOS** | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| **watchOS** | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| **MacOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| **Windows** | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| **Linux** | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | -| | **GoTrue** | **Realtime** | **Postgrest** | **Storage** | **Functions** | **Apollo-GraphQL** | -| ------------------------------------------------------------------ | ---------- | ------------ | ------------- | ----------- | ------------- | ------------------ | -| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Android** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **JS** _(Browser, NodeJS)_ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **IOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **tvOS** _(tvosArm64, tvosX64, tvosSimulatorArm64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **watchOS** _(watchosArm64, watchosX64, watchosSimulatorArm64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **MacOS** _(macosX64 & macosArm64)_ 🚧 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Windows** _(mingwX64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | -| **Linux** _(linuxX64)_ 🚧 | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | +
+ +In-depth Kotlin targets + +**tvOS**: tvosArm64, tvosX64, tvosSimulatorArm64 + +**watchOS**: watchosArm64, watchosX64, watchosSimulatorArm64 + +**MacOS**: macosX64, macosArm64 + +**Windows**: mingwX64 + +**Linux**: linuxX64 + +
✅ = full support -☑️ = partial support: no built-in OAuth/OTP link handling. Linux also has no persistent storage. +☑️ = partial support: no built-in OAuth/OTP link handling. Linux also has no support for persistent storage. For Compose Auth, it relies on GoTrue as fallback. 🚧 = experimental/needs feedback diff --git a/spec/supabase_kt_v0.yml b/spec/supabase_kt_v0.yml index 265d6a037c9..270ca72b467 100644 --- a/spec/supabase_kt_v0.yml +++ b/spec/supabase_kt_v0.yml @@ -37,7 +37,8 @@ functions: **On JS**, it uses the website origin as the callback url. Session importing gets handled automatically. **Windows, tvOS, watchOS & Linux** currently have no default implementation. Feel free to create a PR. - You always make your own implementation and use `gotrue.parseFragmentAndImportSession(fragment)` to let [supabase-kt](https://github.com/supabase-community/supabase-kt) handle the parsing & importing of the session after receiving a callback. + You always make your own implementation and use `gotrue.parseSessionFromFragment(fragment)` or `gotrue.parseSessionFromUrl(url)` to let [supabase-kt](https://github.com/supabase-community/supabase-kt) handle the parsing after receiving a callback. + Then you can simply use `gotrue.importSession(session)`. ### Configure deeplink callbacks for Authentication @@ -53,6 +54,9 @@ functions: install(GoTrue) { host = "deeplink host" // this can be anything, eg. your package name or app/company url (not your supabase url) scheme = "deeplink scheme" + + //Android only, you can also change that OAuth/SSO logins open in a custom tab, rather than an external browser: + defaultExternalAuthAction = ExternalAuthAction.CUSTOM_TABS //defaults to EXTERNAL_BROWSER } ``` 4. **Call platform specific function on startup** \ From f73feb7d0023a0af4237176bfb8c436ddff526be Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Sat, 26 Aug 2023 13:14:51 +0200 Subject: [PATCH 019/327] Update introduction --- apps/docs/docs/ref/kotlin/introduction.mdx | 33 +++++++++++++--------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/apps/docs/docs/ref/kotlin/introduction.mdx b/apps/docs/docs/ref/kotlin/introduction.mdx index 5903e53382f..113bd3759c8 100644 --- a/apps/docs/docs/ref/kotlin/introduction.mdx +++ b/apps/docs/docs/ref/kotlin/introduction.mdx @@ -15,31 +15,36 @@ hideTitle: true
This reference documents every object and method available in Supabase's Kotlin Multiplatform library, [supabase-kt](https://github.com/supabase-community/supabase-kt). You can use supabase-kt to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files. + Supported targets: -| | **GoTrue** | **Realtime** | **Postgrest** | **Storage** | **Functions** | **Apollo-GraphQL** | **Compose Auth** | **Compose Auth UI** | -| ----------- | ---------- | ------------ | ------------- | ----------- | ------------- | ------------------ | ---------------- | ------------------- | -| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ☑️ | ✅ | -| **Android** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **JS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ☑️ | ✅ | -| **IOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **tvOS** | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | -| **watchOS** | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | -| **MacOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | -| **Windows** | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | -| **Linux** | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| | **GoTrue** | **Realtime** | **Postgrest** | **Storage** | **Functions** | **Apollo-GraphQL** | **Compose Auth 🚧** | **Compose Auth UI 🚧** | +|------------------|------------|--------------|---------------|-------------|---------------|--------------------|---------------------|------------------------| +| **JVM** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ☑️ | ✅ | +| **Android** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **JS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ☑️ | ✅ | +| **IOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **tvOS** | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| **watchOS** | ☑️ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| **MacOS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| **Windows** | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| **Linux** | ☑️ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
In-depth Kotlin targets +**iOS:** iosArm64, iosSimulatorArm64, iosX64 + +**JS**: Browser, NodeJS + **tvOS**: tvosArm64, tvosX64, tvosSimulatorArm64 -**watchOS**: watchosArm64, watchosX64, watchosSimulatorArm64 +**watchOS**: watchosArm64, watchosX64, watchosSimulatorArm64 -**MacOS**: macosX64, macosArm64 +**MacOS**: macosX64, macosArm64 -**Windows**: mingwX64 +**Windows**: mingwX64 **Linux**: linuxX64 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 020/327] 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 021/327] 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 022/327] 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 023/327] 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 024/327] 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 025/327] 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 026/327] 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 027/327] 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 028/327] 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 029/327] 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 031/327] 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 032/327] 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 033/327] 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 034/327] 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 035/327] 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 036/327] 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 && ( - -