diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 9762c179622..bf98d6d34fd 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -48,7 +48,12 @@ const nextConfig = { basePath: '/docs', images: { dangerouslyAllowSVG: true, - domains: ['avatars.githubusercontent.com', 'github.com', 'user-images.githubusercontent.com'], + domains: [ + 'avatars.githubusercontent.com', + 'github.com', + 'user-images.githubusercontent.com', + 'raw.githubusercontent.com', + ], }, experimental: { mdxRs: true, diff --git a/apps/docs/pages/faq.mdx b/apps/docs/pages/faq.mdx index eced1e89829..66c0c120969 100644 --- a/apps/docs/pages/faq.mdx +++ b/apps/docs/pages/faq.mdx @@ -16,7 +16,7 @@ Self-hosting Supabase is free. If you wish to use our cloud-platform, we provide ### How do I host Supabase? -You can use the docker-compose script [here](https://github.com/supabase/supabase/tree/master/docker), and find detailed instructions [here](/docs/guides/hosting/overview). +You can use the docker compose script [here](https://github.com/supabase/supabase/tree/master/docker), and find detailed instructions [here](/docs/guides/hosting/overview). Supabase is an amalgamation of open source tools. Some of these tools are made by Supabase (like our [Realtime Server](https://github.com/supabase/realtime)), some we support directly (like [PostgREST](http://postgrest.org/en/v7.0.0/)), and some are third-party tools (like [KonSupabase is an amalgamation open sourceg](https://github.com/Kong/kong)). diff --git a/apps/docs/pages/guides/integrations/authsignal.mdx b/apps/docs/pages/guides/integrations/authsignal.mdx index 250b822b483..65d83b22d01 100644 --- a/apps/docs/pages/guides/integrations/authsignal.mdx +++ b/apps/docs/pages/guides/integrations/authsignal.mdx @@ -61,7 +61,7 @@ cd supabase-authsignal-example Create a `.env.local` file and enter the following values: -``` +```sh NEXT_PUBLIC_SUPABASE_URL=get-from-supabase-dashboard NEXT_PUBLIC_SUPABASE_ANON_KEY=get-from-supabase-dashboard AUTHSIGNAL_SECRET=get-from-authsignal-dashboard @@ -84,19 +84,19 @@ npm run dev Install the Supabase client and Auth helpers for Next.js: -``` +```bash npm install @supabase/supabase-js @supabase/auth-helpers-nextjs ``` Install the Authsignal Node.js client: -``` +```bash npm install @authsignal/node ``` Finally install 2 packages to help encrypt and serialize session data in cookies: -``` +```bash npm install @hapi/iron cookie npm install --save-dev @types/cookie ``` @@ -105,18 +105,18 @@ npm install --save-dev @types/cookie Add the following code to `/lib/authsignal.ts`: -``` -import { Authsignal } from "@authsignal/node"; +```ts +import { Authsignal } from '@authsignal/node' -const secret = process.env.AUTHSIGNAL_SECRET; +const secret = process.env.AUTHSIGNAL_SECRET if (!secret) { - throw new Error("AUTHSIGNAL_SECRET is undefined"); + throw new Error('AUTHSIGNAL_SECRET is undefined') } -const redirectUrl = "http://localhost:3000/api/callback"; +const redirectUrl = 'http://localhost:3000/api/callback' -export const authsignal = new Authsignal({ secret, redirectUrl }); +export const authsignal = new Authsignal({ secret, redirectUrl }) ``` The `redirectUrl` here is a Next.js API route which Authsignal will redirect back to after an MFA challenge. We'll implement this below. @@ -131,83 +131,72 @@ Next we will add some helper functions for managing cookies: Add the following code to `/lib/cookies.ts`: -``` -import Iron from "@hapi/iron"; -import { Session } from "@supabase/supabase-js"; -import { parse, serialize } from "cookie"; -import { NextApiRequest, NextApiResponse } from "next"; +```ts +import Iron from '@hapi/iron' +import { Session } from '@supabase/supabase-js' +import { parse, serialize } from 'cookie' +import { NextApiRequest, NextApiResponse } from 'next' export async function setTempCookie(session: Session, res: NextApiResponse) { - const token = await Iron.seal(session, TEMP_TOKEN_SECRET, Iron.defaults); + const token = await Iron.seal(session, TEMP_TOKEN_SECRET, Iron.defaults) const cookie = serialize(TEMP_COOKIE, token, { maxAge: session.expires_in, httpOnly: true, - secure: process.env.NODE_ENV === "production", - path: "/", - sameSite: "lax", - }); + secure: process.env.NODE_ENV === 'production', + path: '/', + sameSite: 'lax', + }) - res.setHeader("Set-Cookie", cookie); + res.setHeader('Set-Cookie', cookie) } -export async function getSessionFromTempCookie( - req: NextApiRequest -): Promise { - const cookie = req.headers.cookie as string; +export async function getSessionFromTempCookie(req: NextApiRequest): Promise { + const cookie = req.headers.cookie as string - const cookies = parse(cookie ?? ""); + const cookies = parse(cookie ?? '') - const tempCookie = cookies[TEMP_COOKIE]; + const tempCookie = cookies[TEMP_COOKIE] if (!tempCookie) { - return undefined; + return undefined } - const session = await Iron.unseal( - tempCookie, - TEMP_TOKEN_SECRET, - Iron.defaults - ); + const session = await Iron.unseal(tempCookie, TEMP_TOKEN_SECRET, Iron.defaults) - return session; + return session } export function setAuthCookie(session: Session, res: NextApiResponse) { - const { access_token, refresh_token, expires_in } = session; + const { access_token, refresh_token, expires_in } = session const authCookies = [ { name: ACCESS_TOKEN_COOKIE, value: access_token }, - refresh_token - ? { name: REFRESH_TOKEN_COOKIE, value: refresh_token } - : undefined, + refresh_token ? { name: REFRESH_TOKEN_COOKIE, value: refresh_token } : undefined, ] .filter(isDefined) .map(({ name, value }) => serialize(name, value, { maxAge: expires_in, httpOnly: true, - secure: process.env.NODE_ENV === "production", - path: "/", - sameSite: "lax", + secure: process.env.NODE_ENV === 'production', + path: '/', + sameSite: 'lax', }) - ); + ) // Also clear the temp cookie - const updatedCookies = [ - ...authCookies, - serialize(TEMP_COOKIE, "", { maxAge: -1, path: "/" }), - ]; + const updatedCookies = [...authCookies, serialize(TEMP_COOKIE, '', { maxAge: -1, path: '/' })] - res.setHeader("Set-Cookie", updatedCookies); + res.setHeader('Set-Cookie', updatedCookies) } -const isDefined = (value: T | undefined): value is T => !!value; +const isDefined = (value: T | undefined): value is T => !!value -const TEMP_TOKEN_SECRET = process.env.TEMP_TOKEN_SECRET!; -const TEMP_COOKIE = "as-mfa-cookie"; -const ACCESS_TOKEN_COOKIE = "sb-access-token"; -const REFRESH_TOKEN_COOKIE = "sb-refresh-token"; +const TEMP_TOKEN_SECRET = process.env.TEMP_TOKEN_SECRET! +const TEMP_COOKIE = 'as-mfa-cookie' +const ACCESS_TOKEN_COOKIE = 'sb-access-token' +const REFRESH_TOKEN_COOKIE = 'sb-refresh-token' ``` ## Step 7: Building the UI @@ -216,34 +205,34 @@ We will add some form components for signing in and signing up as well as a basi Add the following code to `/pages/sign-up.tsx`: -``` -import Link from "next/link"; -import { useRouter } from "next/router"; +```ts +import Link from 'next/link' +import { useRouter } from 'next/router' export default function SignUpPage() { - const router = useRouter(); + const router = useRouter() return (
{ - e.preventDefault(); + e.preventDefault() const target = e.target as typeof e.target & { - email: { value: string }; - password: { value: string }; - }; + email: { value: string } + password: { value: string } + } - const email = target.email.value; - const password = target.password.value; + const email = target.email.value + const password = target.password.value - await fetch("/api/sign-up", { - method: "POST", - headers: { "Content-Type": "application/json" }, + await fetch('/api/sign-up', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), - }).then((res) => res.json()); + }).then((res) => res.json()) - router.push("/"); + router.push('/') }} > @@ -253,49 +242,49 @@ export default function SignUpPage() {
- {"Already have an account? "} + {'Already have an account? '} Sign in
- ); + ) } ``` Then add the following code to `/pages/sign-in.tsx`: -``` -import Link from "next/link"; -import { useRouter } from "next/router"; +```ts +import Link from 'next/link' +import { useRouter } from 'next/router' export default function SignInPage() { - const router = useRouter(); + const router = useRouter() return (
{ - e.preventDefault(); + e.preventDefault() const target = e.target as typeof e.target & { - email: { value: string }; - password: { value: string }; - }; + email: { value: string } + password: { value: string } + } - const email = target.email.value; - const password = target.password.value; + const email = target.email.value + const password = target.password.value - const { state, mfaUrl } = await fetch("/api/sign-in", { - method: "POST", - headers: { "Content-Type": "application/json" }, + const { state, mfaUrl } = await fetch('/api/sign-in', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), - }).then((res) => res.json()); + }).then((res) => res.json()) - if (state === "CHALLENGE_REQUIRED") { - window.location.href = mfaUrl; + if (state === 'CHALLENGE_REQUIRED') { + window.location.href = mfaUrl } else { - router.push("/"); + router.push('/') } }} > @@ -312,38 +301,38 @@ export default function SignInPage() {
- ); + ) } ``` Now we will use Supabase's `withPageAuth` wrapper around `getServerSideProps` to make the home page require authentication via SSR. Replace the existing code in `/pages/index.tsx` with the following: -``` -import { getUser, User, withPageAuth } from "@supabase/auth-helpers-nextjs"; -import { GetServerSideProps } from "next"; -import { useRouter } from "next/router"; -import { authsignal } from "../lib/authsignal"; +```ts +import { getUser, User, withPageAuth } from '@supabase/auth-helpers-nextjs' +import { GetServerSideProps } from 'next' +import { useRouter } from 'next/router' +import { authsignal } from '../lib/authsignal' interface Props { - user: User; - isEnrolled: boolean; + user: User + isEnrolled: boolean } export const getServerSideProps: GetServerSideProps = withPageAuth({ - redirectTo: "/sign-in", + redirectTo: '/sign-in', async getServerSideProps(ctx) { - const { user } = await getUser(ctx); + const { user } = await getUser(ctx) - const { isEnrolled } = await authsignal.getUser({userId: user.id}); + const { isEnrolled } = await authsignal.getUser({ userId: user.id }) return { props: { user, isEnrolled }, - }; + } }, -}); +}) export default function HomePage({ user, isEnrolled }: Props) { - const router = useRouter(); + const router = useRouter() return (
@@ -351,29 +340,29 @@ export default function HomePage({ user, isEnrolled }: Props) {
Signed in as: {user?.email}
- +
- ); + ) } ``` Optional: To make things look a bit nicer, you can add the following to `/styles/globals.css`: -``` +```css main { min-height: 100vh; display: flex; @@ -435,113 +424,95 @@ Now we'll replace the existing api routes in `/pages/api/` with 5 new routes: Add the following code to `/pages/api/sign-in.ts`: -``` -import { supabaseClient } from "@supabase/auth-helpers-nextjs"; -import { NextApiRequest, NextApiResponse } from "next"; -import { authsignal } from "../../lib/authsignal"; -import { setAuthCookie, setTempCookie } from "../../lib/cookies"; +```ts +import { supabaseClient } from '@supabase/auth-helpers-nextjs' +import { NextApiRequest, NextApiResponse } from 'next' +import { authsignal } from '../../lib/authsignal' +import { setAuthCookie, setTempCookie } from '../../lib/cookies' -export default async function signIn( - req: NextApiRequest, - res: NextApiResponse -) { - const { email, password } = req.body; +export default async function signIn(req: NextApiRequest, res: NextApiResponse) { + const { email, password } = req.body - const { data, error } = await supabaseClient.auth.api.signInWithEmail( - email, - password - ); + const { data, error } = await supabaseClient.auth.api.signInWithEmail(email, password) if (error || !data?.user) { - return res.send({ error }); + return res.send({ error }) } const { state, url: mfaUrl } = await authsignal.track({ - action: "signIn", + action: 'signIn', userId: data.user.id, - }); + }) - if (state === "CHALLENGE_REQUIRED") { - await setTempCookie(data, res); + if (state === 'CHALLENGE_REQUIRED') { + await setTempCookie(data, res) } else { - setAuthCookie(data, res); + setAuthCookie(data, res) } - res.send({ state, mfaUrl }); + res.send({ state, mfaUrl }) } ``` Then to handle new sign-ups add the following to `/pages/api/sign-up.ts`: -``` -import { supabaseClient } from "@supabase/auth-helpers-nextjs"; -import { Session } from "@supabase/supabase-js"; -import { NextApiRequest, NextApiResponse } from "next"; -import { setAuthCookie } from "../../lib/cookies"; +```ts +import { supabaseClient } from '@supabase/auth-helpers-nextjs' +import { Session } from '@supabase/supabase-js' +import { NextApiRequest, NextApiResponse } from 'next' +import { setAuthCookie } from '../../lib/cookies' -export default async function signUp( - req: NextApiRequest, - res: NextApiResponse -) { - const { email, password } = req.body; +export default async function signUp(req: NextApiRequest, res: NextApiResponse) { + const { email, password } = req.body - const { data, error } = await supabaseClient.auth.api.signUpWithEmail( - email, - password - ); + const { data, error } = await supabaseClient.auth.api.signUpWithEmail(email, password) if (error || !isSession(data)) { - res.send({ error }); + res.send({ error }) } else { - setAuthCookie(data, res); - res.send({ data }); + setAuthCookie(data, res) + res.send({ data }) } } -const isSession = (data: any): data is Session => !!data?.access_token; +const isSession = (data: any): data is Session => !!data?.access_token ``` To clear the auth cookies on sign-out add the following to `/pages/api/sign-out.ts`: -``` -import { supabaseClient } from "@supabase/auth-helpers-nextjs"; -import { NextApiRequest, NextApiResponse } from "next"; +```ts +import { supabaseClient } from '@supabase/auth-helpers-nextjs' +import { NextApiRequest, NextApiResponse } from 'next' -export default async function signOut( - req: NextApiRequest, - res: NextApiResponse -) { - supabaseClient.auth.api.deleteAuthCookie(req, res, { redirectTo: "/sign-in" }); +export default async function signOut(req: NextApiRequest, res: NextApiResponse) { + supabaseClient.auth.api.deleteAuthCookie(req, res, { redirectTo: '/sign-in' }) } ``` To handle the user's actions to set up MFA or manage their existing MFA settings, add the following to `/pages/api/mfa.ts`: -``` -import { getUser, withApiAuth } from "@supabase/auth-helpers-nextjs"; -import { NextApiRequest, NextApiResponse } from "next"; -import { authsignal } from "../../lib/authsignal"; +```ts +import { getUser, withApiAuth } from '@supabase/auth-helpers-nextjs' +import { NextApiRequest, NextApiResponse } from 'next' +import { authsignal } from '../../lib/authsignal' -export default withApiAuth(async function mfa( - req: NextApiRequest, - res: NextApiResponse -) { - if (req.method !== "POST") { - return res.status(405).send({ message: "Only POST requests allowed" }); +export default withApiAuth(async function mfa(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') { + return res.status(405).send({ message: 'Only POST requests allowed' }) } - const { user } = await getUser({ req, res }); + const { user } = await getUser({ req, res }) - const { isEnrolled } = req.body; + const { isEnrolled } = req.body const { url: mfaUrl } = await authsignal.track({ - action: isEnrolled ? "manageSettings" : "enroll", + action: isEnrolled ? 'manageSettings' : 'enroll', userId: user.id, redirectToSettings: isEnrolled, - }); + }) - res.send({ mfaUrl }); -}); + res.send({ mfaUrl }) +}) ``` Because the user should be authenticated with Supabase to set up or manage MFA, we can use Supabase's `withApiAuth` wrapper to protect this route. @@ -550,28 +521,25 @@ The `redirectToSettings` param specifies whether the user should be redirected t Finally we need a route to handle the redirect back from Authsignal after an MFA challenge. Add the following to `/pages/api/callback.ts`: -``` -import { NextApiRequest, NextApiResponse } from "next"; -import { authsignal } from "../../lib/authsignal"; -import { getSessionFromTempCookie, setAuthCookie } from "../../lib/cookies"; +```ts +import { NextApiRequest, NextApiResponse } from 'next' +import { authsignal } from '../../lib/authsignal' +import { getSessionFromTempCookie, setAuthCookie } from '../../lib/cookies' -export default async function callback( - req: NextApiRequest, - res: NextApiResponse -) { - const token = req.query.token as string; +export default async function callback(req: NextApiRequest, res: NextApiResponse) { + const token = req.query.token as string - const { success } = await authsignal.validateChallenge({ token }); + const { success } = await authsignal.validateChallenge({ token }) if (success) { - const session = await getSessionFromTempCookie(req); + const session = await getSessionFromTempCookie(req) if (session) { - setAuthCookie(session, res); + setAuthCookie(session, res) } } - res.redirect("/"); + res.redirect('/') } ``` diff --git a/apps/docs/pages/guides/integrations/weweb.mdx b/apps/docs/pages/guides/integrations/weweb.mdx index 93ddbd5f2bc..c65a695400e 100644 --- a/apps/docs/pages/guides/integrations/weweb.mdx +++ b/apps/docs/pages/guides/integrations/weweb.mdx @@ -39,12 +39,7 @@ Once you have added both to WeWeb, you will have the option to enable realtime t **🚨 Warning 🚨** -> Realtime is disabled by default in Supabase for better database performance and security. Learn more about [realtime functionalities](https://supabase.com/docs/guides/database#realtime) in our documentation. -> Member -> @awalias awalias 16 days ago -> might just wanna change these ones to be relative but otherwise looks great 👍 - -@EWMAs Reply... +> Realtime is disabled by default in Supabase for better database performance and security. Learn more about [realtime functionalities](/docs/guides/realtime). ## Step 2: GET Data from Supabase diff --git a/apps/docs/pages/guides/self-hosting.mdx b/apps/docs/pages/guides/self-hosting.mdx index 1c99b9765d9..3092dd9cf45 100644 --- a/apps/docs/pages/guides/self-hosting.mdx +++ b/apps/docs/pages/guides/self-hosting.mdx @@ -149,7 +149,7 @@ We realize that database migrations are difficult, and this is one of the proble While Supabase officially supports Docker, we have several other deployment strategies managed by the community: -- [supabase-docker](../../guides/hosting/docker) (Official) +- [supabase-docker](/docs/guides/self-hosting/docker) (Official) - [supabase-kubernetes](https://github.com/supabase-community/supabase-kubernetes) (Unofficial) - [supabase-terraform](https://github.com/supabase-community/supabase-terraform) (Unofficial) - [supabase-traefik](https://github.com/supabase-community/supabase-traefik) (Unofficial) diff --git a/apps/docs/pages/guides/self-hosting/docker.mdx b/apps/docs/pages/guides/self-hosting/docker.mdx index 08da99ae31c..0a32049889b 100644 --- a/apps/docs/pages/guides/self-hosting/docker.mdx +++ b/apps/docs/pages/guides/self-hosting/docker.mdx @@ -11,7 +11,7 @@ Docker is the easiest way to get started with self-hosted Supabase. You need the following installed in your system: -- [Docker](https://docs.docker.com/engine/install/) and [docker-compose](https://docs.docker.com/compose/install/) +- [Docker](https://docs.docker.com/engine/install/) and [docker compose](https://docs.docker.com/compose/install/) - [Git](https://git-scm.com/downloads) ## Quick Start @@ -31,7 +31,7 @@ cd supabase/docker cp .env.example .env # Start -docker-compose up +docker compose up ``` Now visit [http://localhost:3000](http://localhost:3000) to start using Supabase Studio. @@ -97,7 +97,7 @@ Once you have done this, you can safely comment out the `db` section of the `doc ### Setting database's `log_min_messages` -By default, `docker-compose` sets the database's `log_min_messages` configuration to `fatal` to prevent redundant logs generated by Realtime. +By default, `docker compose` sets the database's `log_min_messages` configuration to `fatal` to prevent redundant logs generated by Realtime. However, you might miss important log messages such as database errors. Configure `log_min_messages` based on your needs. ### File storage backend on macOS diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 95817e22f6d..2231880862b 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -34,6 +34,7 @@ Kevin Grüneberg Lakshmipathi G Laura C Leo T +Long Hoang Margarita Sandomirskaia Marijana Šimag Mark Burggraf @@ -60,7 +61,7 @@ ____________ JOBS -If you are interested in working with us check out the open roles here: https://about.supabase.com/careers +If you are interested in working with us check out the open roles here: https://supabase.com/careers ___________ diff --git a/apps/www/_blog/2020-07-10-alpha-launch-postmortem.mdx b/apps/www/_blog/2020-07-10-alpha-launch-postmortem.mdx index 61bb85d236e..f0186212be6 100644 --- a/apps/www/_blog/2020-07-10-alpha-launch-postmortem.mdx +++ b/apps/www/_blog/2020-07-10-alpha-launch-postmortem.mdx @@ -37,13 +37,13 @@ GitHub stars rocketed for our two main repos, [supabase](https://github.com/supa Here are the things that survived well. -### Middleware: docker-compose up +### Middleware: docker compose up -This was the most surprising survivor. Our middleware was served from a single Ubuntu server with 4 CPUs and 8GB of RAM. This server was running our middleware using `docker-compose up`: +This was the most surprising survivor. Our middleware was served from a single Ubuntu server with 4 CPUs and 8GB of RAM. This server was running our middleware using `docker compose up`: -![This image shows in middleware architecture. We used docker-compose to pull up 5 open source tools: Kong, Realtime, PostgREST, PG-API, and PG-BOSS.](/images/blog/supabase-middleware-docker.png) +![This image shows in middleware architecture. We used docker compose to pull up 5 open source tools: Kong, Realtime, PostgREST, PG-API, and PG-BOSS.](/images/blog/supabase-middleware-docker.png) -In case you're wondering why any sane company would use that in production, it's because we weren't planning to launch - the HackerNews post was created by an early GitHub follower, while we were alpha testing, and it was too scary to migrate the middleware while it was servicing the thundering herd. All Supabase projects use the same middleware stack (sans docker-compose), so I guess this counts as as a successful load test. +In case you're wondering why any sane company would use that in production, it's because we weren't planning to launch - the HackerNews post was created by an early GitHub follower, while we were alpha testing, and it was too scary to migrate the middleware while it was servicing the thundering herd. All Supabase projects use the same middleware stack (without docker compose), so I guess this counts as a successful load test. We have since migrated our middleware to multiple ECS clusters, globally load-balanced using AWS's [Global Accelerator](https://aws.amazon.com/global-accelerator/). diff --git a/apps/www/_blog/2021-03-31-supabase-cli.mdx b/apps/www/_blog/2021-03-31-supabase-cli.mdx index b24682700de..ea8d4c94ed8 100644 --- a/apps/www/_blog/2021-03-31-supabase-cli.mdx +++ b/apps/www/_blog/2021-03-31-supabase-cli.mdx @@ -217,7 +217,7 @@ Also check `kong.yml` inside the `kong` directory where you'll see how all the s Once you're all set, you can start the stack by running: ```bash -docker-compose up +docker compose up ``` Head over to the [Self Hosting Docs](/docs/guides/self-hosting) for a more complete walk through, it also includes several [one-click deploys](/docs/guides/self-hosting#one-click-deploys), so you can easily deploy into your own cloud hosting provider. diff --git a/apps/www/data/Developers.json b/apps/www/data/Developers.json index a29460f0c5b..df449e784ab 100644 --- a/apps/www/data/Developers.json +++ b/apps/www/data/Developers.json @@ -1,26 +1,20 @@ [ { "text": "Documentation", - "description": "Everything you need that makes it simple to get started.", + "description": "Everything you need to start building with Supabase.", "url": "/docs", "icon": "M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" }, { - "text": "API reference", - "description": "Examples and references for using API libraries.", - "url": "/docs/client/supabase-client", - "icon": "M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" - }, - { - "text": "Guides", - "description": "Examples and references for using Supabase.", - "url": "/docs/", + "text": "Changelog", + "description": "See the latest updates and product improvements.", + "url": "/changelog", "icon": "M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" }, { "text": "Careers", "description": "Join the Supabase team and get involved.", - "url": "https://supabase.com/careers", + "url": "/careers", "icon": "M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" } ] diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index 09317c8a31f..ff18f604298 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -1787,16 +1787,6 @@ module.exports = [ source: '/docs/guides/with-ionic-angular', destination: '/docs/guides/getting-started/tutorials/with-ionic-angular', }, - { - permanent: true, - source: '/docs/guides/examples', - destination: '/docs/guides/resources/examples', - }, - { - permanent: true, - source: '/docs/guides/examples', - destination: '/docs/guides/resources/examples', - }, { permanent: true, source: '/docs/guides/hosting/overview', diff --git a/apps/www/pages/launch-week/index.tsx b/apps/www/pages/launch-week/index.tsx index 225de69fcb0..7888948b8c7 100644 --- a/apps/www/pages/launch-week/index.tsx +++ b/apps/www/pages/launch-week/index.tsx @@ -38,7 +38,7 @@ export default function launchweek() { const { isDarkMode } = useTheme() const title = 'Launch Week 6' const description = 'Supabase Launch Week 6 | 12-18 Dec 2022' - const liveDay = 'Friday' + const liveDay = null const [supabase] = useState(() => createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!) @@ -219,11 +219,6 @@ export default function launchweek() { )} > - -
- Jump to current day -
-
{process.env.NEXT_PUBLIC_LW_STARTED && ( <> diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 70da7c553bd..659c501cde3 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,8 +1,8 @@ # Usage -# Start: docker-compose up -# With helpers: docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up -# Stop: docker-compose down -# Destroy: docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans +# Start: docker compose up +# With helpers: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up +# Stop: docker compose down +# Destroy: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans version: "3.8" diff --git a/package-lock.json b/package-lock.json index 5f6dcf98766..d002b6210bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37607,6 +37607,7 @@ "uniforms-bootstrap4": "^3.6.2", "uniforms-bridge-json-schema": "^3.6.2", "uuid": "^8.3.2", + "valtio": "^1.7.6", "yup": "^0.32.11", "yup-password": "^0.2.2", "zxcvbn": "^4.4.2" @@ -37829,6 +37830,11 @@ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.1.tgz", "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==" }, + "studio/node_modules/proxy-compare": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.3.0.tgz", + "integrity": "sha512-c3L2CcAi7f7pvlD0D7xsF+2CQIW8C3HaYx2Pfgq8eA4HAl3GAH6/dVYsyBbYF/0XJs2ziGLrzmz5fmzPm6A0pQ==" + }, "studio/node_modules/react-resize-detector": { "version": "6.7.8", "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.7.8.tgz", @@ -37889,6 +37895,46 @@ "node": ">=4.2.0" } }, + "studio/node_modules/valtio": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.7.6.tgz", + "integrity": "sha512-zsGrCCYOIpy8egQxftduFyJusF/BMu3CganhHKUOE/I6t6V6yA1MDfZZkrYoWYCGkC3rSBYcIHEEsoYQM9lV2w==", + "dependencies": { + "proxy-compare": "2.3.0", + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@babel/helper-module-imports": ">=7.12", + "@babel/types": ">=7.13", + "aslemammad-vite-plugin-macro": ">=1.0.0-alpha.1", + "babel-plugin-macros": ">=3.0", + "react": ">=16.8", + "vite": ">=2.8.6" + }, + "peerDependenciesMeta": { + "@babel/helper-module-imports": { + "optional": true + }, + "@babel/types": { + "optional": true + }, + "aslemammad-vite-plugin-macro": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + }, + "react": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "studio/node_modules/webpack-bundle-analyzer": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.3.0.tgz", @@ -63371,6 +63417,7 @@ "uniforms-bootstrap4": "^3.6.2", "uniforms-bridge-json-schema": "^3.6.2", "uuid": "^8.3.2", + "valtio": "^1.7.6", "yup": "^0.32.11", "yup-password": "^0.2.2", "zxcvbn": "^4.4.2" @@ -63521,6 +63568,11 @@ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.1.tgz", "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==" }, + "proxy-compare": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.3.0.tgz", + "integrity": "sha512-c3L2CcAi7f7pvlD0D7xsF+2CQIW8C3HaYx2Pfgq8eA4HAl3GAH6/dVYsyBbYF/0XJs2ziGLrzmz5fmzPm6A0pQ==" + }, "react-resize-detector": { "version": "6.7.8", "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.7.8.tgz", @@ -63564,6 +63616,15 @@ "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true }, + "valtio": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.7.6.tgz", + "integrity": "sha512-zsGrCCYOIpy8egQxftduFyJusF/BMu3CganhHKUOE/I6t6V6yA1MDfZZkrYoWYCGkC3rSBYcIHEEsoYQM9lV2w==", + "requires": { + "proxy-compare": "2.3.0", + "use-sync-external-store": "1.2.0" + } + }, "webpack-bundle-analyzer": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.3.0.tgz", diff --git a/package.json b/package.json index 4a605d28309..bf893aa1978 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,10 @@ "dev:www": "turbo run dev --filter=www --parallel", "lint": "turbo run lint", "format": "prettier --write \"apps/**/*.{js,jsx,ts,tsx,css,md,json}\"", - "docker:dev": "cd docker && docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up --renew-anon-volumes", - "docker:up": "cd docker && docker-compose up", - "docker:down": "cd docker && docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down --remove-orphans", - "docker:remove": "cd docker && docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml rm -vfs", + "docker:dev": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up --renew-anon-volumes", + "docker:up": "cd docker && docker compose up", + "docker:down": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down --remove-orphans", + "docker:remove": "cd docker && docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml rm -vfs", "test": "jest", "test:studio": "turbo run test --filter=studio", "perf:kong": "ab -t 5 -c 20 -T application/json http://localhost:8000/", diff --git a/studio/README.md b/studio/README.md index 4b9929f4a48..2c4fda0ba60 100644 --- a/studio/README.md +++ b/studio/README.md @@ -19,7 +19,7 @@ As such, the features exposed on Studio for existing deployments are limited to ## Managing Project Settings -Project settings are managed outside of the Dashboard. If you use docker-compose, you should manage the settings in your docker-compose file. If you're deploying Supabase to your own cloud, you should store your secrets and env vars in a vault or secrets manager. +Project settings are managed outside of the Dashboard. If you use docker compose, you should manage the settings in your docker-compose file. If you're deploying Supabase to your own cloud, you should store your secrets and env vars in a vault or secrets manager. ## How to contribute? @@ -51,7 +51,7 @@ Follow the [self-hosting guide](https://supabase.com/docs/guides/hosting/docker) ``` cd .. cd docker -docker-compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up +docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up ``` Once you've got that set up, update `.env` in the studio folder with the corresponding values. diff --git a/studio/components/interfaces/Billing/AddNewPaymentMethodModal/AddNewPaymentMethodModal.tsx b/studio/components/interfaces/Billing/AddNewPaymentMethodModal/AddNewPaymentMethodModal.tsx index 8e8e7e865b6..f8c52edd2eb 100644 --- a/studio/components/interfaces/Billing/AddNewPaymentMethodModal/AddNewPaymentMethodModal.tsx +++ b/studio/components/interfaces/Billing/AddNewPaymentMethodModal/AddNewPaymentMethodModal.tsx @@ -1,9 +1,11 @@ -import { FC, useEffect, useState } from 'react' -import { IconLoader, Modal } from 'ui' +import HCaptcha from '@hcaptcha/react-hcaptcha' import { loadStripe } from '@stripe/stripe-js' import { Elements } from '@stripe/react-stripe-js' +import { FC, useCallback, useEffect, useState } from 'react' +import { Modal } from 'ui' import { useStore } from 'hooks' +import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store' import { post } from 'lib/common/fetch' import { API_URL, STRIPE_PUBLIC_KEY } from 'lib/constants' import AddPaymentMethodForm from './AddPaymentMethodForm' @@ -20,16 +22,49 @@ const AddNewPaymentMethodModal: FC = ({ visible, returnUrl, onCancel }) = const { ui } = useStore() const [intent, setIntent] = useState() - useEffect(() => { - if (visible) { - setupIntent() - } - }, [visible]) + const captchaLoaded = useIsHCaptchaLoaded() - const setupIntent = async () => { + const [captchaToken, setCaptchaToken] = useState(null) + const [captchaRef, setCaptchaRef] = useState(null) + + const captchaRefCallback = useCallback((node) => { + setCaptchaRef(node) + }, []) + + useEffect(() => { + const loadPaymentForm = async () => { + if (visible && captchaRef && captchaLoaded) { + let token = captchaToken + + try { + if (!token) { + const captchaResponse = await captchaRef.execute({ async: true }) + token = captchaResponse?.response ?? null + } + } catch (error) { + return + } + + await setupIntent(token ?? undefined) + resetCaptcha() + } + } + + loadPaymentForm() + }, [visible, captchaRef, captchaLoaded]) + + const resetCaptcha = () => { + setCaptchaToken(null) + captchaRef?.resetCaptcha() + } + + const setupIntent = async (hcaptchaToken: string | undefined) => { setIntent(undefined) + const orgSlug = ui.selectedOrganization?.slug ?? '' - const intent = await post(`${API_URL}/organizations/${orgSlug}/payments/setup-intent`, {}) + const intent = await post(`${API_URL}/organizations/${orgSlug}/payments/setup-intent`, { + hcaptchaToken, + }) if (intent.error) { return ui.setNotification({ @@ -37,9 +72,9 @@ const AddNewPaymentMethodModal: FC = ({ visible, returnUrl, onCancel }) = message: intent.error.message, error: intent.error, }) + } else { + setIntent(intent) } - - setIntent(intent) } const options = { @@ -47,27 +82,43 @@ const AddNewPaymentMethodModal: FC = ({ visible, returnUrl, onCancel }) = appearance: { theme: 'night', labels: 'floating' }, } as any + const onLocalCancel = () => { + setIntent(undefined) + return onCancel() + } + return ( - -
- {intent !== undefined ? ( + // We cant display the hCaptcha in the modal, as the modal auto-closes when clicking the captcha + // So we only show the modal if the captcha has been executed successfully (intent loaded) + <> + { + setCaptchaToken(token) + }} + onClose={onLocalCancel} + onExpire={() => { + setCaptchaToken(null) + }} + /> + + +
- + - ) : ( -
- -
- )} -
-
+
+
+ ) } diff --git a/studio/components/interfaces/Billing/Billing.utils.ts b/studio/components/interfaces/Billing/Billing.utils.ts index 84e1e448694..67f98f93e7b 100644 --- a/studio/components/interfaces/Billing/Billing.utils.ts +++ b/studio/components/interfaces/Billing/Billing.utils.ts @@ -52,7 +52,8 @@ export const formSubscriptionUpdatePayload = ( }, nonChangeableAddons: SubscriptionAddon[], selectedPaymentMethod: string, - region: string + region: string, + hcaptchaToken: string | undefined ) => { const { computeSize, pitrDuration, customDomains } = selectedAddons @@ -78,6 +79,7 @@ export const formSubscriptionUpdatePayload = ( addons, proration_date, payment_method: selectedPaymentMethod, + hcaptchaToken } } diff --git a/studio/components/interfaces/Billing/EnterpriseUpdate.tsx b/studio/components/interfaces/Billing/EnterpriseUpdate.tsx index bf98cd7e4aa..254d83275c7 100644 --- a/studio/components/interfaces/Billing/EnterpriseUpdate.tsx +++ b/studio/components/interfaces/Billing/EnterpriseUpdate.tsx @@ -1,4 +1,4 @@ -import { FC, useState, useEffect } from 'react' +import { FC, useState, useEffect, useRef } from 'react' import { useRouter } from 'next/router' import { Transition } from '@headlessui/react' @@ -6,6 +6,7 @@ import { useStore, useFlag } from 'hooks' import { getURL } from 'lib/helpers' import { post, patch } from 'lib/common/fetch' import { API_URL, PROJECT_STATUS } from 'lib/constants' +import HCaptcha from '@hcaptcha/react-hcaptcha' import Divider from 'components/ui/Divider' import { SubscriptionAddon } from './AddOns/AddOns.types' @@ -45,6 +46,9 @@ const EnterpriseUpdate: FC = ({ const isCustomDomainsEnabled = useFlag('customDomains') const isPITRSelfServeEnabled = useFlag('pitrSelfServe') + const [captchaToken, setCaptchaToken] = useState(null) + const captchaRef = useRef(null) + const projectId = ui.selectedProject?.id ?? -1 const projectRef = ui.selectedProject?.ref ?? 'default' const projectRegion = ui.selectedProject?.region ?? '' @@ -110,7 +114,8 @@ const EnterpriseUpdate: FC = ({ selectedAddons, nonChangeableAddons, selectedPaymentMethodId, - projectRegion + projectRegion, + undefined ), tier: currentSubscription.tier.price_id, } @@ -127,8 +132,45 @@ const EnterpriseUpdate: FC = ({ setIsRefreshingPreview(false) } + const resetCaptcha = () => { + setCaptchaToken(null) + captchaRef.current?.resetCaptcha() + } + + const beforeConfirmPayment = async (): Promise => { + setIsSubmitting(true) + let token = captchaToken + + try { + if (!token) { + const captchaResponse = await captchaRef.current?.execute({ async: true }) + token = captchaResponse?.response ?? null + setCaptchaToken(token) + } + } catch (error) { + setIsSubmitting(false) + return false + } + + setIsSubmitting(false) + return true + } + // Last todo to support enterprise billing on dashboard + E2E test const onConfirmPayment = async () => { + setIsSubmitting(true) + let token = captchaToken + + try { + if (!token) { + const captchaResponse = await captchaRef.current?.execute({ async: true }) + token = captchaResponse?.response ?? null + } + } catch (error) { + setIsSubmitting(false) + return + } + const payload = { ...formSubscriptionUpdatePayload( currentSubscription, @@ -136,13 +178,14 @@ const EnterpriseUpdate: FC = ({ selectedAddons, nonChangeableAddons, selectedPaymentMethodId, - projectRegion + projectRegion, + token ?? undefined ), tier: currentSubscription.tier.price_id, } - - setIsSubmitting(true) const res = await patch(`${API_URL}/projects/${projectRef}/subscription`, payload) + resetCaptcha() + if (res?.error) { ui.setNotification({ category: 'error', @@ -272,7 +315,21 @@ const EnterpriseUpdate: FC = ({ onSelectAddNewPaymentMethod={() => { setShowAddPaymentMethodModal(true) }} + beforeConfirmPayment={beforeConfirmPayment} onConfirmPayment={onConfirmPayment} + captcha={ + { + setCaptchaToken(token) + }} + onExpire={() => { + setCaptchaToken(null) + }} + /> + } /> diff --git a/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx b/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx index c1838f7c233..439f5edc6f3 100644 --- a/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx +++ b/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx @@ -11,6 +11,7 @@ import { CANCELLATION_REASONS } from '../Billing.constants' import { UpdateSuccess } from '../' import { SubscriptionPreview } from '../Billing.types' import { StripeSubscription } from 'components/interfaces/Billing' +import HCaptcha from '@hcaptcha/react-hcaptcha' interface Props { freeTier: any @@ -24,6 +25,9 @@ const ExitSurvey: FC = ({ freeTier, subscription, onSelectBack }) => { const projectId = ui.selectedProject?.id ?? -1 const projectRef = ui.selectedProject?.ref + const captchaRef = useRef(null) + const [captchaToken, setCaptchaToken] = useState(null) + const formRef = useRef() const initialValues = { message: '' } @@ -38,7 +42,7 @@ const ExitSurvey: FC = ({ freeTier, subscription, onSelectBack }) => { const [subscriptionPreview, setSubscriptionPreview] = useState() // Anything above a micro instance size will involve a change in compute size - const willChangeComputeSize = (subscription?.addons ?? []).length > 0 + // as downgrading back to free will bring the project back to micro const currentComputeSize = subscription?.addons.find((option) => option.supabase_prod_id.includes('addon_instance') ) @@ -59,7 +63,7 @@ const ExitSurvey: FC = ({ freeTier, subscription, onSelectBack }) => { } } - const getDowngradeSuccessMessage = () => { + const getDowngradeRefundMessage = () => { return `A total of $${returnedAmount} will be refunded as credits on ${billingDate.toLocaleDateString( 'en-US', { @@ -88,15 +92,33 @@ const ExitSurvey: FC = ({ freeTier, subscription, onSelectBack }) => { }) } - if (willChangeComputeSize) { + setIsSubmitting(true) + let token = captchaToken + + try { + if (!token) { + const captchaResponse = await captchaRef.current?.execute({ async: true }) + token = captchaResponse?.response ?? null + } + } catch (error) { + setIsSubmitting(false) + return + } + + if (currentComputeSize !== undefined) { setMessage(values.message) return setShowConfirmModal(true) } else { - downgradeProject(values) + downgradeProject(values, token as string) } } - const downgradeProject = async (values?: any) => { + const resetCaptcha = () => { + setCaptchaToken(null) + captchaRef.current?.resetCaptcha() + } + + const downgradeProject = async (values?: any, hcaptchaToken?: string) => { const downgradeMessage = values?.message ?? message try { @@ -110,8 +132,11 @@ const ExitSurvey: FC = ({ freeTier, subscription, onSelectBack }) => { tier, addons, proration_date, + hcaptchaToken: captchaToken ?? hcaptchaToken, }) + resetCaptcha() + if (res?.error) { return ui.setNotification({ category: 'error', @@ -119,11 +144,11 @@ const ExitSurvey: FC = ({ freeTier, subscription, onSelectBack }) => { error: res.error, }) } else { - if (willChangeComputeSize) { + if (currentComputeSize !== undefined) { app.onProjectStatusUpdated(projectId, PROJECT_STATUS.RESTORING) ui.setNotification({ category: 'info', - message: getDowngradeSuccessMessage(), + message: getDowngradeRefundMessage(), duration: 8000, }) ui.setNotification({ @@ -139,9 +164,9 @@ const ExitSurvey: FC = ({ freeTier, subscription, onSelectBack }) => { } const feedbackRes = await post(`${API_URL}/feedback/downgrade`, { projectRef, - reasons: selectedReasons.reduce((a, b) => `${a}- ${b}\n`, ''), - additionalFeedback: downgradeMessage, - exitAction: 'downgrade', + reasons: selectedReasons.reduce((a, b) => `${a}- ${b}\n`, ''), + additionalFeedback: downgradeMessage, + exitAction: 'downgrade', }) if (feedbackRes.error) throw feedbackRes.error } catch (error: any) { @@ -159,7 +184,7 @@ const ExitSurvey: FC = ({ freeTier, subscription, onSelectBack }) => { ) } @@ -173,7 +198,7 @@ const ExitSurvey: FC = ({ freeTier, subscription, onSelectBack }) => { enterFrom="transform opacity-0 translate-x-10" enterTo="transform opacity-100 translate-x-0" > -
+
+
+ { + setCaptchaToken(token) + }} + onExpire={() => { + setCaptchaToken(null) + }} + /> +
)} diff --git a/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx b/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx index 078d2a067d2..8deab10c609 100644 --- a/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx +++ b/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react' +import { FC, useRef, useState } from 'react' import * as Tooltip from '@radix-ui/react-tooltip' import { Listbox, IconLoader, Button, IconPlus, IconAlertCircle, IconCreditCard } from 'ui' import { PermissionAction } from '@supabase/shared-types/out/constants' @@ -42,8 +42,11 @@ interface Props { isLoadingPaymentMethods: boolean onSelectPaymentMethod: (method: any) => void onSelectAddNewPaymentMethod: () => void + beforeConfirmPayment: () => Promise onConfirmPayment: () => void isSubmitting: boolean + + captcha: React.ReactNode } // Use case of this panel is actually only for upgrading from Free to Pro @@ -70,8 +73,10 @@ const PaymentSummaryPanel: FC = ({ isLoadingPaymentMethods, onSelectPaymentMethod, onSelectAddNewPaymentMethod, + beforeConfirmPayment, onConfirmPayment, isSubmitting, + captcha, }) => { const { ui } = useStore() const projectRegion = ui.selectedProject?.region @@ -120,7 +125,7 @@ const PaymentSummaryPanel: FC = ({ ).toFixed(2) } - const validateOrder = () => { + const validateOrder = async () => { const error = validateSubscriptionUpdatePayload(selectedAddons) if (error) { return ui.setNotification({ @@ -129,16 +134,23 @@ const PaymentSummaryPanel: FC = ({ message: error, }) } else { - isChangingComputeSize || (isChangingPITRDuration && selectedPITRDays === 0) - ? setShowConfirmModal(true) - : onConfirmPayment() + // [Joshen] We're validating captcha before subsequent actions as there's an issue + // with the hcaptcha overlay and the modal component, such that clicking on the hcaptcha closes + // both the hcaptcha overlay and the modal. Need to figure that out first, then we can validate + // the hcaptcha within onConfirmPayment() + const hasValidCaptcha = await beforeConfirmPayment() + if (hasValidCaptcha) { + isChangingComputeSize || (isChangingPITRDuration && selectedPITRDays === 0) + ? setShowConfirmModal(true) + : onConfirmPayment() + } } } return ( <>

Payment Summary

@@ -293,7 +305,7 @@ const PaymentSummaryPanel: FC = ({
)} -
+
= ({

Select payment method

{isLoadingPaymentMethods ? ( -
+

Retrieving payment methods

) : paymentMethods.length === 0 ? ( -
+

No saved payment methods

@@ -370,11 +382,11 @@ const PaymentSummaryPanel: FC = ({ ) })}
-

+

Add new payment method

@@ -382,6 +394,8 @@ const PaymentSummaryPanel: FC = ({ )}
+
{captcha}
+
diff --git a/studio/components/interfaces/Billing/ProUpgrade.tsx b/studio/components/interfaces/Billing/ProUpgrade.tsx index 7fe2e78c578..2f05f9a8844 100644 --- a/studio/components/interfaces/Billing/ProUpgrade.tsx +++ b/studio/components/interfaces/Billing/ProUpgrade.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useState } from 'react' +import { FC, useEffect, useRef, useState } from 'react' import { Transition } from '@headlessui/react' import { useRouter } from 'next/router' import * as Tooltip from '@radix-ui/react-tooltip' @@ -29,6 +29,7 @@ import { } from './AddOns/AddOns.utils' import BackButton from 'components/ui/BackButton' import SupportPlan from './AddOns/SupportPlan' +import HCaptcha from '@hcaptcha/react-hcaptcha' // Do not allow compute size changes for af-south-1 @@ -52,6 +53,9 @@ const ProUpgrade: FC = ({ const isCustomDomainsEnabled = useFlag('customDomains') const isPITRSelfServeEnabled = useFlag('pitrSelfServe') + const [captchaToken, setCaptchaToken] = useState(null) + const captchaRef = useRef(null) + const { addons } = products const computeSizes = formatComputeSizes(addons) const pitrDurationOptions = formatPITROptions(addons) @@ -133,7 +137,8 @@ const ProUpgrade: FC = ({ selectedAddons, nonChangeableAddons, selectedPaymentMethodId, - projectRegion + projectRegion, + undefined ) setIsRefreshingPreview(true) @@ -148,6 +153,30 @@ const ProUpgrade: FC = ({ setIsRefreshingPreview(false) } + const resetCaptcha = () => { + setCaptchaToken(null) + captchaRef.current?.resetCaptcha() + } + + const beforeConfirmPayment = async (): Promise => { + setIsSubmitting(true) + let token = captchaToken + + try { + if (!token) { + const captchaResponse = await captchaRef.current?.execute({ async: true }) + token = captchaResponse?.response ?? null + setCaptchaToken(token) + } + } catch (error) { + setIsSubmitting(false) + return false + } + + setIsSubmitting(false) + return true + } + const onConfirmPayment = async () => { const payload = formSubscriptionUpdatePayload( currentSubscription, @@ -155,11 +184,12 @@ const ProUpgrade: FC = ({ selectedAddons, nonChangeableAddons, selectedPaymentMethodId, - projectRegion + projectRegion, + captchaToken ?? undefined ) - - setIsSubmitting(true) const res = await patch(`${API_URL}/projects/${projectRef}/subscription`, payload) + resetCaptcha() + if (res?.error) { ui.setNotification({ category: 'error', @@ -199,17 +229,17 @@ const ProUpgrade: FC = ({ enter="transition ease-out duration-300" enterFrom="transform opacity-0 translate-x-10" enterTo="transform opacity-100 translate-x-0" - className="flex w-full items-start justify-between" + className="flex items-start justify-between w-full" >
-
+
onSelectBack()} />

Change your project's subscription

@@ -233,14 +263,14 @@ const ProUpgrade: FC = ({ )}
-
+

Enable spend cap

setShowSpendCapHelperModal(true)} />
@@ -318,8 +348,22 @@ const ProUpgrade: FC = ({ onSelectAddNewPaymentMethod={() => { setShowAddPaymentMethodModal(true) }} + beforeConfirmPayment={beforeConfirmPayment} onConfirmPayment={onConfirmPayment} isSubmitting={isSubmitting} + captcha={ + { + setCaptchaToken(token) + }} + onExpire={() => { + setCaptchaToken(null) + }} + /> + } />
@@ -338,7 +382,7 @@ const ProUpgrade: FC = ({ header="Enabling spend cap" onCancel={() => setShowSpendCapHelperModal(false)} > -
+

@@ -353,7 +397,7 @@ const ProUpgrade: FC = ({

{/* Maybe instead of a table, show something more interactive like a spend cap playground */} {/* Maybe ideate this in Figma first but this is good enough for now */} -
+

Item

Limit

@@ -380,7 +424,7 @@ const ProUpgrade: FC = ({ diff --git a/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts b/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts index 63ec9612ed2..de1990ca96b 100644 --- a/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts +++ b/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts @@ -812,7 +812,7 @@ create publication supabase_realtime sql: ` -- Create a table for public profiles create table profiles ( - id uuid references auth.users not null primary key, + id uuid references auth.users on delete cascade not null primary key, updated_at timestamp with time zone, username text unique, full_name text, diff --git a/studio/components/layouts/StorageLayout/StorageMenu.tsx b/studio/components/layouts/StorageLayout/StorageMenu.tsx index dc41cdacb64..2889292f42f 100644 --- a/studio/components/layouts/StorageLayout/StorageMenu.tsx +++ b/studio/components/layouts/StorageLayout/StorageMenu.tsx @@ -122,8 +122,10 @@ const BucketRow = ({ -

{bucket.name}

+
+

+ {bucket.name} +

{bucket.public && Public}
} diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.js index 139a0be29cb..ad57ec629c1 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.js +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.js @@ -34,7 +34,7 @@ const HeaderPathEdit = ({ loading, breadcrumbs, togglePathEdit }) => {
) : (
-

{breadcrumbs[breadcrumbs.length - 1] || ''}

+

{breadcrumbs[breadcrumbs.length - 1] || ''}