diff --git a/.github/workflows/deploy_vercel_prod.yml b/.github/workflows/deploy_vercel_prod.yml deleted file mode 100644 index dceb61fbcde..00000000000 --- a/.github/workflows/deploy_vercel_prod.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Deploy studio app to production on vercel -on: - push: - branches: - - studio - paths: - - 'studio/**' -jobs: - deploy: - runs-on: ubuntu-18.04 - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v2 - # remove ./studio/pages/api folder before deploying to vercel - - uses: JesseTG/rm@v1.0.2 - with: - path: ./studio/pages/api - - uses: amondnet/vercel-action@v19 - with: - vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required - github-comment: false - vercel-args: '--prod -A .vercel/production.json' - vercel-org-id: team_E6KJ1W561hMTjon1QSwOh0WO #Required - vercel-project-id: Qmd7Mw8yFW8SbtvTWFV9uyRzw9P1rJLmgtHjSezywhtYec #Required - working-directory: ./studio #Your Working Directory, Optional - vercel-project-name: app diff --git a/.github/workflows/deploy_vercel_staging.yml b/.github/workflows/deploy_vercel_staging.yml deleted file mode 100644 index d6bb3760187..00000000000 --- a/.github/workflows/deploy_vercel_staging.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Deploy studio app to staging on vercel -on: - push: - branches: - - master - paths: - - 'studio/**' -jobs: - deploy: - runs-on: ubuntu-18.04 - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v2 - # remove ./studio/pages/api folder before deploying to vercel - - uses: JesseTG/rm@v1.0.2 - with: - path: ./studio/pages/api - - uses: amondnet/vercel-action@v19 - with: - vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required - github-comment: false - vercel-args: '--prod -A .vercel/staging.json' - vercel-org-id: team_E6KJ1W561hMTjon1QSwOh0WO #Required - vercel-project-id: QmcmhbiAtCMFTAHCuGgQscNbke4TzgWULECctNcKmxWCoT #Required - working-directory: ./studio #Your Working Directory, Optional - vercel-project-name: staging-app diff --git a/.github/workflows/studio-build.yml b/.github/workflows/studio-build.yml index b51253bbf04..570baa1aa61 100644 --- a/.github/workflows/studio-build.yml +++ b/.github/workflows/studio-build.yml @@ -9,7 +9,6 @@ on: - 'studio/**' jobs: build: - runs-on: ubuntu-latest strategy: @@ -18,15 +17,15 @@ jobs: # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - name: Install deps - run: npm i - working-directory: ./studio - - name: Run build - run: npm run build - working-directory: ./studio + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - name: Install deps + run: npm ci + working-directory: ./ + - name: Run build + run: npx turbo run build --filter=studio + working-directory: ./ diff --git a/apps/reference/_cli/intro.mdx b/apps/reference/_cli/intro.mdx index 1615aa7ae28..8ac2186ca6e 100644 --- a/apps/reference/_cli/intro.mdx +++ b/apps/reference/_cli/intro.mdx @@ -16,6 +16,9 @@ The CLI is still under development, but it contains all the functionality for wo - CI/CD for releasing to production: [`supabase db push`](https://supabase.com/docs/reference/cli/usage#supabase-db-push) - Manage your Supabase projects: [`supabase projects`](https://supabase.com/docs/reference/cli/usage#supabase-projects) - Generate types directly from your database schema: [`supabase gen types`](https://supabase.com/docs/reference/cli/usage#supabase-gen) + - A [community-supported GitHub Action](https://github.com/lyqht/generate-supabase-db-types-github-action) to generate TypeScript types +- Shell autocomplete: [`supabase completion`](https://supabase.com/docs/reference/cli/usage#supabase-completion) + - A [community-supported Fig autocomplete spec](https://fig.io/manual/supabase) for macOS terminal ## Additional Links diff --git a/apps/reference/docs/guides/auth/auth-helpers/nextjs.mdx b/apps/reference/docs/guides/auth/auth-helpers/nextjs.mdx index f17a9fe926e..700a1a75765 100644 --- a/apps/reference/docs/guides/auth/auth-helpers/nextjs.mdx +++ b/apps/reference/docs/guides/auth/auth-helpers/nextjs.mdx @@ -31,7 +31,8 @@ This library supports the following tooling versions: - Node.js: `^10.13.0 || >=12.0.0` - Next.js: `>=10` -Optionally, install **React Auth Helpers** for components and hooks that can be used across all React-based frameworks. +Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks. + ```sh npm install @supabase/auth-helpers-react ``` @@ -48,7 +49,7 @@ This library supports the following tooling versions: - Node.js: `^10.13.0 || >=12.0.0` - Next.js: `>=10` -Optionally, install **React Auth Helpers** for components and hooks that can be used across all React-based frameworks. +Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks. ```sh yarn add @supabase/auth-helpers-react @@ -61,23 +62,30 @@ yarn add @supabase/auth-helpers-react Retrieve your project URL and anon key in your project's [API settings](https://app.supabase.com/project/_/settings/api) in the Dashboard to set up the following environment variables. For local development you can set them in a `.env.local` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/nextjs/.env.local.example). -```bash -NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key +```bash title=".env.local" +NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL +NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY ``` ## Basic Setup + + + Wrap your `pages/_app.js` component with the `SessionContextProvider` component: ```jsx title="pages/_app.js" -import React from 'react' -import { useRouter } from 'next/router' import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' import { SessionContextProvider } from '@supabase/auth-helpers-react' -function MyApp({ Component, pageProps }: AppProps) { +function MyApp({ Component, pageProps }) { const router = useRouter() + // Create a new supabase browser client on every first render. const [supabaseClient] = useState(() => createBrowserSupabaseClient()) return ( @@ -85,58 +93,105 @@ function MyApp({ Component, pageProps }: AppProps) { supabaseClient={supabaseClient} initialSession={pageProps.initialSession} > - - ) } ``` + + + +Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component: + +```jsx title="pages/_app.tsx" +import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' +// highlight-next-line +import { SessionContextProvider, Session } from '@supabase/auth-helpers-react' + +function MyApp({ + Component, + pageProps, +}: AppProps<{ + // highlight-next-line + initialSession: Session, +}>) { + // Create a new supabase browser client on every first render. + const [supabaseClient] = useState(() => createBrowserSupabaseClient()) + + return ( + + + + ) +} +``` + + + + You can now determine if a user is authenticated by checking that the `user` object returned by the `useUser()` hook is defined. ## Usage with TypeScript You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion: -```js -// Creating a new supabase client object: -import { Database } from '../db_types'; +### Browser client -const [supabaseClient] = useState(() => - createBrowserSupabaseClient() - ); +```ts +// Creating a new supabase client object: +import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' +import { Database } from '../database.types' + +const supabaseClient = createBrowserSupabaseClient() ``` -```js +```ts // Retrieving a supabase client object from the SessionContext: -import { useSupabaseClient } from '@supabase/auth-helpers-react'; -import { Database } from '../db_types'; +import { useSupabaseClient } from '@supabase/auth-helpers-react' +import { Database } from '../database.types' -const supabaseClient = useSupabaseClient(); +const supabaseClient = useSupabaseClient() +``` + +### Server client + +```ts +// Creating a new supabase server client object (e.g. in API route): +import type { NextApiRequest, NextApiResponse } from 'next' +import type { Database } from 'types_db' + +export default async (req: NextApiRequest, res: NextApiResponse) => { + const supabaseServerClient = createServerSupabaseClient({ + req, + res, + }) + const { + data: { user }, + } = await supabaseServerClient.auth.getUser() + + res.status(200).json({ name: user?.name ?? '' }) +} ``` ## Client-side data fetching with RLS -For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to use the `supabaseClient` from the `useSessionContext` hook and only run your query once the user is defined client-side in the `useUser()` hook: +For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to use the `supabaseClient` from the `useSupabaseClient` hook and only run your query once the user is defined client-side in the `useUser()` hook: ```js import { Auth, ThemeSupa } from '@supabase/auth-ui-react' -import { useUser, useSessionContext } from '@supabase/auth-helpers-react' +import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react' import { useEffect, useState } from 'react' const LoginPage = () => { - const { isLoading, session, error, supabaseClient } = useSessionContext() + const supabaseClient = useSupabaseClient() const user = useUser() const [data, setData] = useState() + // highlight-start useEffect(() => { async function loadData() { const { data } = await supabaseClient.from('test').select('*') @@ -145,19 +200,17 @@ const LoginPage = () => { // Only run query once user is logged in. if (user) loadData() }, [user]) + // highlight-end if (!user) return ( - <> - {error &&

{error.message}

} - - + ) return ( @@ -174,7 +227,7 @@ const LoginPage = () => { export default LoginPage ``` -### Server-side rendering (SSR) - withPageAuth +## Server-side rendering (SSR) - withPageAuth If you wrap your `getServerSideProps` with `withPageAuth` your props object will be augmented with the user object. @@ -191,7 +244,7 @@ export const getServerSideProps = withPageAuth({ redirectTo: '/login' }) If there is no authenticated user, they will be redirect to your home page, unless you specify the `redirectTo` option. You can pass in your own `getServerSideProps` method, the props returned from this will be merged with the -user props. You can also access the user session data by calling `getUser` inside of this method, eg: +user props. You can also access the user session data by calling `supabase.auth.getUser()` inside of this method, eg: ```js title="pages/protected-page.js" import { withPageAuth } from '@supabase/auth-helpers-nextjs' @@ -212,7 +265,7 @@ export const getServerSideProps = withPageAuth({ }) ``` -### Server-side data fetching with RLS +## Server-side data fetching with RLS Both `withApiAuth` and `withPageAuth` return a supabase client that you can use to run [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) authenticated queries server-side: @@ -245,44 +298,62 @@ export const getServerSideProps = withPageAuth({ }) ``` -### Server-side data fetching to OAuth APIs using `provider_token` +## Server-side data fetching to OAuth APIs using `provider_token` When using third-party auth providers, sessions are initiated with an additional `provider_token` field which is persisted as an HTTPOnly cookie upon logging in to enabled usage on the server side. The `provider_token` can be used to make API requests to the OAuth provider's API endpoints on behalf of the logged-in user. In the following example, we fetch the user's full profile from the third-party API during SSR using their id and auth token: ```js -import { User, withPageAuth, getUser } from '@supabase/auth-helpers-nextjs' - -interface Profile { - /* ... */ -} +import { User, withPageAuth } from '@supabase/auth-helpers-nextjs' export default function ProtectedPage({ user, - data, + allRepos, }: { user: User, - profile: Profile, + allRepos: any, }) { - return
Protected content
+ return ( + <> +
Protected content for {user.email}
+

Data fetched with provider token:

+
{JSON.stringify(allRepos, null, 2)}
+

user:

+
{JSON.stringify(user, null, 2)}
+ + ) } export const getServerSideProps = withPageAuth({ redirectTo: '/', - async getServerSideProps(ctx) { - // Retrieve provider_token from cookies - const provider_token = ctx.req.cookies['sb-provider-token'] - // Get logged in user's third-party id from metadata - const { user } = await getUser(ctx) - const userId = user?.user_metadata.provider_id - const profile: Profile = await ( - await fetch(`https://api.example.com/users/${userId}`, { - method: 'GET', - headers: { - Authorization: `Bearer ${provider_token}`, - }, - }) + async getServerSideProps(ctx, supabase) { + const { + data: { session }, + error, + } = await supabase.auth.getSession() + if (error) { + throw error + } + if (!session) { + return { props: {} } + } + + // Retrieve provider_token & logged in user's third-party id from metadata + const { provider_token, user } = session + const userId = user.user_metadata.user_name + + const allRepos = await ( + await fetch( + `https://api.github.com/search/repositories?q=user:${userId}`, + { + method: 'GET', + headers: { + Authorization: `token ${provider_token}`, + }, + } + ) ).json() - return { props: { profile } } + + return { props: { allRepos, user } } }, }) ``` @@ -355,6 +426,7 @@ export const config = { ``` - The `supabaseClient` and `supabaseServerClient` have been removed in favor of the `createBrowserSupabaseClient` and `createServerSupabaseClient` methods. This allows you to provide the CLI-generated types to the client: + ```js // client-side import type { Database } from 'types_db'; @@ -373,12 +445,14 @@ export const config = { res.status(200).json({ name: user?.name ?? '' }) } ``` + - The `UserProvider` has been replaced by the `SessionContextProvider`. Make sure to wrap your `pages/_app.js` componenent with the `SessionContextProvider`. Then, throughout your application you can use the `useSessionContext` hook to get the `session` and the `useSupabaseClient` hook to get an authenticated `supabaseClient`. - The `useUser` hook now returns the `user` object or `null`. - Usage with TypeScript: You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion: + ```js // Creating a new supabase client object: - import { Database } from '../db_types'; + import { Database } from '../database.types'; const [supabaseClient] = useState(() => createBrowserSupabaseClient() @@ -388,7 +462,7 @@ export const config = { ```js // Retrieving a supabase client object from the SessionContext: import { useSupabaseClient } from '@supabase/auth-helpers-react'; - import { Database } from '../db_types'; + import { Database } from '../database.types'; const supabaseClient = useSupabaseClient(); ``` diff --git a/apps/reference/docs/guides/auth/auth-workos.mdx b/apps/reference/docs/guides/auth/auth-workos.mdx index e7e9c932b8d..422666106ad 100644 --- a/apps/reference/docs/guides/auth/auth-workos.mdx +++ b/apps/reference/docs/guides/auth/auth-workos.mdx @@ -65,12 +65,14 @@ async function signInWithWorkOS() { const { data, error } = await supabase.auth.signInWithOAuth( { provider: 'workos', + options: { + queryParams: { + connection: '', + organization: '', + } + } }, - { - connection: '', - organization: '', - } ) } ``` diff --git a/apps/reference/docs/guides/cli/cicd-workflow.mdx b/apps/reference/docs/guides/cli/cicd-workflow.mdx index 875d240c300..07afed16df8 100644 --- a/apps/reference/docs/guides/cli/cicd-workflow.mdx +++ b/apps/reference/docs/guides/cli/cicd-workflow.mdx @@ -348,6 +348,24 @@ Once pushed, check that the migration version is up to date for both local and r supabase migration list ``` +### Permission denied on db remote commit + +If you have been using Supabase hosted projects for a long time, you might encounter the following permission error when executing `db remote commit`. + +```bash +Error: Error running pg_dump on remote database: pg_dump: error: query failed: ERROR: permission denied for table _type + +pg_dump: error: query was: LOCK TABLE "graphql"."_type" IN ACCESS SHARE MODE +``` + +To resolve this error, you need to grant `postgres` role permissions to `graphql` schema. You can do that by running the following query from Supabase dashboard's SQL Editor. + +```sql +grant all on all tables in schema graphql to postgres, anon, authenticated, service_role; +grant all on all functions in schema graphql to postgres, anon, authenticated, service_role; +grant all on all sequences in schema graphql to postgres, anon, authenticated, service_role; +``` + ### Permission denied on db push If you created a table through Supabase dashboard, and your new migration script contains `ALTER TABLE` statements, you might run into permission error when applying them on staging or production databases. @@ -358,12 +376,14 @@ ERROR: must be owner of table employees (SQLSTATE 42501); while executing migrat This is because tables created through Supabase dashboard are owned by `supabase_admin` role while the migration scripts executed through CLI are under `postgres` role. -One way to solve this is to grant `postgres` role additional privileges through the SQL Editor available on Supabase dashboard. For example, the following command grants postgres permissions to alter any table in the public schema. +One way to solve this is to reassign the owner of those tables to `postgres` role. For example, if your table is named `users` in the public schema, you can run the following command to reassign owner. ```sql -GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO postgres; +ALTER TABLE users OWNER TO postgres; ``` +Apart from tables, you also need to reassign owner of other entities using their respective commands, including [types](https://www.postgresql.org/docs/current/sql-alterschema.html), [functions](https://www.postgresql.org/docs/current/sql-alterroutine.html), and [schemas](https://www.postgresql.org/docs/current/sql-altertype.html). + ### Rebasing new migrations Sometimes your teammate may merge a new migration file to git main branch, and now you need to rebase your local schema changes on top. diff --git a/apps/reference/docs/guides/integrations/dhiwise.mdx b/apps/reference/docs/guides/integrations/dhiwise.mdx new file mode 100644 index 00000000000..15b4110ed1e --- /dev/null +++ b/apps/reference/docs/guides/integrations/dhiwise.mdx @@ -0,0 +1,175 @@ +--- +id: dhiwise +title: 'DhiWise' +description: 'Get started with Supabase and DhiWise. Convert your Figma designs into Flutter apps, store data, and authenticate your users' +--- +import select from "/img/guides/integrations/dhiwise/select.png" +import create from "/img/guides/integrations/dhiwise/create.png" + +This guide explains how to connect Supabase backend to DhiWise Flutter application quickly. + +[DhiWise](https://www.dhiwise.com/) is a Developer tool to convert Figma designs into React and Flutter applications. It lets you quickly integrate Databases and APIs into your React and Flutter Apps. + +If you don't have a DhiWise account, create one [here](https://app.dhiwise.com). + +DhiWise supports easy Supabase Integration in just five steps. + +Let's get started! + +## Step 1: SignIn to Supabase + +Go to [Supabase](https://supabase.com/), Click `Sign In`, and create a new account by authenticating with **GitHub**. If you already have an account, you will be logged in. + +## Step 2: Create a new project in Supabase + +Click on `New project` from the Dashboard and select an organization. If you don't have an organization, create one using `+ New organization.` + +- Give your Supabase project a `name.` +- Enter a secure `Database Password.` +- Choose the `region` where your app's backend is hosted. +- Click `Create new project.` + +![New Project](/img/guides/integrations/dhiwise/newProj.png) + +## Step 3: Find the API key and URL + +Once your project is created, you can access the API Key and URL string, Or if you already have an account go to your `organization-> app-> settings-> API`. + +![auth keys](/img/guides/integrations/dhiwise/authKeys.png) + +## Step 4: Integrations + +There are two ways you can integrate Supabase into your DhiWise Flutter applications. + +### Authentication + +You can integrate `Supabase Email/Password SignUp` or `Supabase Email/Password SignIn` on your components. + +- Open the screen of your flutter application +- Go to the component on which you want to add authentication +- on the `onClick` method - select `authentication` +- From the list, If you want SignUp - select `SignUp with Email/Password`; otherwise, select `SignIn with Email/Password` from Supabase Auth section + +![Auth](/img/guides/integrations/dhiwise/auth.gif) + +And that's it. Supabase authentication will be added to the selected component. + +After downloading the application source code, +1. Add Supabase URL and Supabase public key inside ***lib/core/utils/initial_bindings*** file. +2. For additional details, refer ***https://supabase.com/docs/guides/with-flutter*** + +### Working with Data + +When you first integrate Supabase in your DhiWise Flutter application, You will be asked to add [Supabase auth key and URL](##step-3-find-the-api-key-and-url). When you add them, all the tables available in your Supabase project will be synced in DhiWise. You can integrate Select and Create queries on your Flutter screen for a particular table in DhiWise. + +

Select records

+ +#### **Step 1:** +Select the screen from the screen list where you want to integrate Supabase. + +#### **Step 2:** +Next, go to the view where you want to add Integration, and from the suggestion box for the `onClick` property, choose `Supabase integration,` which will take you to the Integration screen. Where you will be asked to `Enter function name.` Enter the name of your function and click `Submit.` + +#### **Step 3:** +After submitting the function name, you will be asked to select a type of Supabase integration. To retrieve data from Supabase, choose `select.` + +#### **Step 4:** +Next, select the table from which you want to fetch records from the listed Tables. + +#### **Step 5:** +Select the type of integration + +| Type | Description | +| --- | --- | +| **Single** | Used to fetch a single record from the database. | +| **Multiple** | Used to fetch multiple records from the database. | + +:::info +For Multiple types, you need to set `data limit,` `order by, and `order.` +::: + +#### **Step 6:** +You will be redirected to the API Integration screen, where you can set request and response. + +For request binding, the below types are supported. Also, Select the operator for comparison before moving forward. + +| Type | Description | +| --- | --- | +| **View** | Select any component from your screen. | +| **Constant** | Select a constant you've created in your app. | +| **Get from preference** | Select the key you want to fetch from preference. | +| **Navigation argument** | Select data that's been passed from one screen to another. | + +For response binding, the below types are supported. + +| Type | Description | +| --- | --- | +| **View** | Select any component from the screen. | +| **Save to preference** | Storing the data to preference. | + +#### **Select 7:** +`Handle action` - Select the action you wish to take once the Supabase call has either been accepted successfully or refused due to an error. + +Available action for On success and On error are, + +1. [Show Alert](https://docs.dhiwise.com/docs/flutter/show-alert) +2. [Navigation](https://docs.dhiwise.com/docs/flutter/navigation) + +#### **Step 8:** +Finally, you have added Supabase to your application to fetch records on your screen! + +:::tip Example +Suppose you want to fetch records from Supabase and populate the item list on your screen. You can integrate Supabase as discussed above and bind the response with your list view. +::: + +

Create records

+ +#### **Step 1:** +Choose the screen you wish to integrate Supabase for from the list of screens. + +#### **Step 2:** +Next, switch to the component you want to add Integration, and on the `onClick` property, choose `Supabase integration,` which will take you to its integration screen, where you will be asked to **Enter function name**, which will be used in generated code. Enter the name for it and click `Submit` + +#### **Step 3:** +After submitting the function name, you will be asked to select a type of Supabase integration. For example, to create a record in Supabase, choose `Create.` + +#### **Step 4:** +Next, select the table where you want to create a record from the listed Tables. + +#### **Step 5:** +If you want to create a Single record, Select **Select**. Otherwise, **Multiple**. + +#### **Step 6:** +Now, you will be redirected to the API Integration screen, where you can set request and response. + +For request binding, the below types are supported. + +| Type | Description | +| --- | --- | +| **View** | Select any component from the screen | +| **Constant** | Select a constant you've created in your app.| +| **Get from preference** | Select the key you want to fetch from preference. | +| **Navigation argument** | Select data that's been passed from one screen to another. | + +For response binding, the below types are supported. + +| Type | Description | +| --- | --- | +| **View** | Select any component from the screen | +| **Save to preference** | Storing the data to preference. | + +#### **Select 7:** +`Handle action` - Select the action you wish to take once the Supabase call has either been accepted successfully or refused due to an error. + +Available action for On success and On error are, + +1. [Show Alert](https://docs.dhiwise.com/docs/flutter/show-alert) +2. [Navigation](https://docs.dhiwise.com/docs/flutter/navigation) + +#### **Step 9:** +Finally, you have added Supabase to your application to create records from your screen data! + +## Resources + +- [DhiWise Official Website](https://dhiwise.com) +- [DhiWise Documentation](https://docs.dhiwise.com) \ No newline at end of file diff --git a/apps/reference/docs/guides/integrations/integrations.mdx b/apps/reference/docs/guides/integrations/integrations.mdx new file mode 100644 index 00000000000..9fdf5c6fa2d --- /dev/null +++ b/apps/reference/docs/guides/integrations/integrations.mdx @@ -0,0 +1,34 @@ +--- +id: integrations +title: 'Supabase Marketplace' +description: 'Integrations and Partners' +--- + +The Supabase Marketplace brings together all the tools you need to extend your Supabase project. This includes: + +- [Experts](https://supabase.com/partners/experts) - partners to help you build and support your Supabase project. +- [Integrations](https://supabase.com/partners/integrations) - extend your projects with external Auth, Caching, Hosting, and Low-code tools. + +## Build an integration + +Supabase provides several integration points: + +- The [Postgres connection](/docs/guides/database/connecting-to-postgres). Anything that works with Postgres also works with Supabase projects. +- The [Project REST API](/docs/guides/api#rest-api) & client libraries. +- The [Project GraphQL API](/docs/guides/api#graphql-api). +- The [Platform API](/docs/reference/api). + +## List your integration + +[Apply to the Partners program](https://supabase.com/partners/integrations#become-a-partner) to list your integration in the Partners marketplace and in the Supabase docs. + +Integrations are assessed on the following criteria: + +- **Business viability** + While we welcome everyone to built an integration, we only list companies that are deemed to be long-term viable. This includes an official business registration and bank account, meaningful revenue, or Venture Capital backing. We require this criteria to ensure the health of the marketplace. +- **Compliance** + Integrations should not infringe on the Supabase brand/trademark. In short, you cannot use "Supabase" in the name. As the listing appears on the Supabase domain, we don't want to mislead developers into thinking that an integration is an official product. +- **Service Level Agreements** + All listings are required to have their own Terms and Conditions, Privacy Policy, and Acceptable Use Policy, and the company must have resources to meet their SLAs. +- **Maintainability** + All integrations are required to be maintained and functional with Supabase, and the company may be assessed on your ability to remain functional over a long time horizon. diff --git a/apps/reference/docs/guides/integrations/prisma.mdx b/apps/reference/docs/guides/integrations/prisma.mdx index 1915b3a8b9f..e7f8d517ccc 100644 --- a/apps/reference/docs/guides/integrations/prisma.mdx +++ b/apps/reference/docs/guides/integrations/prisma.mdx @@ -62,7 +62,7 @@ This project comes with TypeScript configured and has the following structure. ### Configuring the project to use PostgreSQL Go ahead and delete the `prisma/dev.db` file because we will be switching to PostgreSQL. -Next, inside the `prisma/.env` file, update the value of the `DATABASE_URL` variable to the connection string you got in **step 3**. The URL might look as follows: +Next, inside the `prisma/.env` file, update the value of the `DATABASE_URL` variable to the connection string you got in **step 1**. The URL might look as follows: ```env # prisma/.env @@ -122,7 +122,7 @@ This will create a `prisma/migrations` folder inside your `prisma` directory and If you’re working in a serverless environment (for example Node.js functions hosted on AWS Lambda, Vercel or Netlify Functions), you need to set up [connection pooling](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#serverless-environments-faas) using a tool like [PgBouncer](https://www.pgbouncer.org/). That’s because every function invocation may result in a [new connection to the database](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#the-serverless-challenge). Supabase [supports connection management using PgBouncer](https://supabase.io/blog/2021/04/02/supabase-pgbouncer#what-is-connection-pooling) and are enabled by default. Go to the **Database** page from the sidebar in the Supabase dashboard and navigate to **connection pool** settings ![Connection pool settings](/img/guides/integrations/prisma/w0oowg8vq435ob5c3gf0.png) -When running migrations you need to use the non pooled connection URL (like the one we used in **step 4**). However, when deploying your app, you’ll use the pooled connection URL and add the `?pgbouncer=true` flag to the PostgreSQL connection URL. To minimize the number of concurrent connections, setting the `connection_limit` to `1` is also recommended. So the URL might look as follows: +When running migrations you need to use the non pooled connection URL (like the one we used in **step 1**). However, when deploying your app, you’ll use the pooled connection URL and add the `?pgbouncer=true` flag to the PostgreSQL connection URL. To minimize the number of concurrent connections, setting the `connection_limit` to `1` is also recommended. So the URL might look as follows: ```env # prisma/.env diff --git a/apps/reference/docs/guides/realtime/quickstart.mdx b/apps/reference/docs/guides/realtime/quickstart.mdx index f1918fe9ab6..47a77610d94 100644 --- a/apps/reference/docs/guides/realtime/quickstart.mdx +++ b/apps/reference/docs/guides/realtime/quickstart.mdx @@ -103,7 +103,7 @@ const channel = supabase.channel('calc-latency', { }) channel.subscribe(async (status) => { - if (status === 'SUBSCRIBED) { + if (status === 'SUBSCRIBED') { const begin = performance.now() await channel.send({ diff --git a/apps/reference/docs/guides/with-angular.mdx b/apps/reference/docs/guides/with-angular.mdx index 93389bfc2df..2d4dc88df31 100644 --- a/apps/reference/docs/guides/with-angular.mdx +++ b/apps/reference/docs/guides/with-angular.mdx @@ -33,7 +33,7 @@ If you want to do it yourself, let's get started! ### GitHub -Whenever you get stuck at any point, take a look at [this repo](https://github.com/angular-supa/supabase-angular-user-management). +Whenever you get stuck at any point, take a look at [this repo](https://github.com/supabase/examples/tree/main/supabase-js-v2/user-management/angular-user-management). ## Project set up @@ -180,281 +180,365 @@ export const environment = { Now that we have the API credentials in place, let's create a **SupabaseService** with `ng g s supabase` to initialize the Supabase client and implement functions to communicate with the Supabase API. ```ts title="src/app/supabase.service.ts" -import { Injectable } from '@angular/core' +import { Injectable } from '@angular/core'; import { AuthChangeEvent, + AuthSession, createClient, Session, SupabaseClient, -} from '@supabase/supabase-js' -import { environment } from '../environments/environment' + User, +} from '@supabase/supabase-js'; +import { environment } from 'src/environments/environment'; +import { Database } from 'src/schema'; export interface Profile { - username: string - website: string - avatar_url: string + id?: string; + username: string; + website: string; + avatar_url: string; } @Injectable({ providedIn: 'root', }) export class SupabaseService { - private supabase: SupabaseClient + private supabase: SupabaseClient; + _session: AuthSession | null = null; constructor() { this.supabase = createClient( environment.supabaseUrl, environment.supabaseKey - ) - } - - get user() { - return this.supabase.auth.user() + ); } get session() { - return this.supabase.auth.session() + this.supabase.auth.getSession().then(({ data }) => { + this._session = data.session; + }) + return this._session; } - get profile() { + profile(user: User) { return this.supabase .from('profiles') .select(`username, website, avatar_url`) - .eq('id', this.user?.id) - .single() + .eq('id', user.id) + .single(); } authChanges( callback: (event: AuthChangeEvent, session: Session | null) => void ) { - return this.supabase.auth.onAuthStateChange(callback) + return this.supabase.auth.onAuthStateChange(callback); } signIn(email: string) { - return this.supabase.auth.signIn({ email }) + return this.supabase.auth.signInWithOtp({ email }); } signOut() { - return this.supabase.auth.signOut() + return this.supabase.auth.signOut(); } updateProfile(profile: Profile) { const update = { ...profile, - id: this.user?.id, updated_at: new Date(), - } + }; - return this.supabase.from('profiles').upsert(update, { - returning: 'minimal', // Don't return the value after inserting - }) + return this.supabase.from('profiles').upsert(update); } downLoadImage(path: string) { - return this.supabase.storage.from('avatars').download(path) + return this.supabase.storage.from('avatars').download(path); } uploadAvatar(filePath: string, file: File) { - return this.supabase.storage.from('avatars').upload(filePath, file) + return this.supabase.storage.from('avatars').upload(filePath, file); } } ``` -And one optional step is to update the CSS file `src/index.css` to make the app look nice. -You can find the full contents of this file [here](https://raw.githubusercontent.com/angular-supa/supabase-angular-user-management/main/src/styles.css). +Optionally, update [src/styles.css](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/angular-user-management/src/styles.css) to style the app. ### Set up a Login component Let's set up an Angular component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. Create an **AuthComponent** with `ng g c auth` Angular CLI command. -```ts title="src/app/auth.component.ts" -import { Component } from '@angular/core' -import { SupabaseService } from './supabase.service' +```ts title="src/app/auth/auth.component.ts" +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { SupabaseService } from '../supabase.service'; @Component({ selector: 'app-auth', - template: ` -
-
-

Supabase + Angular

-

Sign in via magic link with your email below

-
- -
-
- -
-
-
- `, + templateUrl: './auth.component.html', + styleUrls: ['./auth.component.css'], }) -export class AuthComponent { - loading = false +export class AuthComponent implements OnInit { + loading = false; - constructor(private readonly supabase: SupabaseService) {} + signInForm = this.formBuilder.group({ + email: '', + }); - async handleLogin(input: string) { + constructor( + private readonly supabase: SupabaseService, + private readonly formBuilder: FormBuilder + ) {} + + ngOnInit(): void {} + + async onSubmit(): Promise { try { - this.loading = true - await this.supabase.signIn(input) - alert('Check your email for the login link!') + this.loading = true; + const email = this.signInForm.value.email as string; + const { error } = await this.supabase.signIn(email); + if (error) throw error; + alert('Check your email for the login link!'); } catch (error) { - alert(error.error_description || error.message) + if (error instanceof Error) { + alert(error.message); + } } finally { - this.loading = false + this.signInForm.reset(); + this.loading = false; } } } ``` +```html title="src/app/auth/auth.component.html" +
+
+

Supabase + Angular

+

Sign in via magic link with your email below

+
+
+ + +
+
+ +
+
+
+
-
- - -
-
- - -
-
- - -
- -
- -
- -
- -
- - `, + templateUrl: './account.component.html', + styleUrls: ['./account.component.css'], }) export class AccountComponent implements OnInit { - loading = false - profile: Profile | undefined + loading = false; + profile!: Profile; - @Input() session: Session | undefined + @Input() + session!: AuthSession; - constructor(private readonly supabase: SupabaseService) {} + updateProfileForm = this.formBuilder.group({ + username: '', + website: '', + avatar_url: '', + }); - ngOnInit() { - this.getProfile() + constructor( + private readonly supabase: SupabaseService, + private formBuilder: FormBuilder + ) {} + + async ngOnInit(): Promise { + await this.getProfile(); + + const { username, website, avatar_url } = this.profile; + this.updateProfileForm.patchValue({ + username, + website, + avatar_url, + }); } async getProfile() { try { - this.loading = true - let { data: profile, error, status } = await this.supabase.profile + this.loading = true; + const { user } = this.session; + let { data: profile, error, status } = await this.supabase.profile(user); if (error && status !== 406) { - throw error + throw error; } if (profile) { - this.profile = profile + this.profile = profile; } } catch (error) { - alert(error.message) + if (error instanceof Error) { + alert(error.message); + } } finally { - this.loading = false + this.loading = false; } } - async updateProfile( - username: string, - website: string, - avatar_url: string = '' - ) { + async updateProfile(): Promise { try { - this.loading = true - await this.supabase.updateProfile({ username, website, avatar_url }) + this.loading = true; + const { user } = this.session; + + const username = this.updateProfileForm.value.username as string; + const website = this.updateProfileForm.value.website as string; + const avatar_url = this.updateProfileForm.value.avatar_url as string; + + const { error } = await this.supabase.updateProfile({ + id: user.id, + username, + website, + avatar_url, + }); + if (error) throw error; } catch (error) { - alert(error.message) + if (error instanceof Error) { + alert(error.message); + } } finally { - this.loading = false + this.loading = false; } } async signOut() { - await this.supabase.signOut() + await this.supabase.signOut(); } } ``` +```html title="src/app/account/account.component.html" +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ +
+ +
+
+``` + ### Launch! Now that we have all the components in place, let's update **AppComponent**: ```ts title="src/app/app.component.ts" -import { Component, OnInit } from '@angular/core' -import { SupabaseService } from './supabase.service' +import { Component, OnInit } from '@angular/core'; +import { SupabaseService } from './supabase.service'; @Component({ selector: 'app-root', - template: ` -
- - - - -
- `, + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'], }) export class AppComponent implements OnInit { - session = this.supabase.session + title = 'angular-user-management'; + + session = this.supabase.session; constructor(private readonly supabase: SupabaseService) {} ngOnInit() { - this.supabase.authChanges((_, session) => (this.session = session)) + this.supabase.authChanges((_, session) => (this.session = session)); } } ``` +```html title="src/app/app.component.html" +
+ + + + +
+``` + +`app.module.ts` also needs to be modified to include the `ReactiveFormsModule` from the `@angular/forms` package. + +```ts title="src/app/app.module.ts" +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppComponent } from './app.component'; +import { AuthComponent } from './auth/auth.component'; +import { AccountComponent } from './account/account.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { AvatarComponent } from './avatar/avatar.component'; + +@NgModule({ + declarations: [ + AppComponent, + AuthComponent, + AccountComponent, + AvatarComponent, + ], + imports: [BrowserModule, ReactiveFormsModule], + providers: [], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + Once that's done, run this in a terminal window: ```bash @@ -474,110 +558,141 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma Let's create an avatar for the user so that they can upload a profile photo. Create an **AvatarComponent** with `ng g c avatar` Angular CLI command. -```ts title="src/app/avatar.component.ts" -import { Component, EventEmitter, Input, Output } from '@angular/core' -import { SupabaseService } from './supabase.service' -import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' +```ts title="src/app/avatar/avatar.component.ts" +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser'; +import { SupabaseService } from '../supabase.service'; @Component({ selector: 'app-avatar', - template: ` -
- Avatar -
-
-
- - -
- `, + templateUrl: './avatar.component.html', + styleUrls: ['./avatar.component.css'], }) -export class AvatarComponent { - _avatarUrl: SafeResourceUrl | undefined - uploading = false +export class AvatarComponent implements OnInit { + _avatarUrl: SafeResourceUrl | undefined; + uploading = false; @Input() - set avatarUrl(url: string | undefined) { + set avatarUrl(url: string | null) { if (url) { - this.downloadImage(url) + this.downloadImage(url); } } - @Output() upload = new EventEmitter() + @Output() upload = new EventEmitter(); constructor( private readonly supabase: SupabaseService, private readonly dom: DomSanitizer ) {} + ngOnInit(): void {} + async downloadImage(path: string) { try { - const { data } = await this.supabase.downLoadImage(path) + const { data } = await this.supabase.downLoadImage(path); if (data instanceof Blob) { this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl( URL.createObjectURL(data) - ) + ); } } catch (error) { - console.error('Error downloading image: ', error.message) + if (error instanceof Error) { + console.error('Error downloading image: ', error.message); + } } } async uploadAvatar(event: any) { try { - this.uploading = true + this.uploading = true; if (!event.target.files || event.target.files.length === 0) { - throw new Error('You must select an image to upload.') + throw new Error('You must select an image to upload.'); } - const file = event.target.files[0] - const fileExt = file.name.split('.').pop() - const fileName = `${Math.random()}.${fileExt}` - const filePath = `${fileName}` + const file = event.target.files[0]; + const fileExt = file.name.split('.').pop(); + const filePath = `${Math.random()}.${fileExt}`; - await this.supabase.uploadAvatar(filePath, file) - this.upload.emit(filePath) + await this.supabase.uploadAvatar(filePath, file); + this.upload.emit(filePath); } catch (error) { - alert(error.message) + if (error instanceof Error) { + alert(error.message); + } } finally { - this.uploading = false + this.uploading = false; } } } ``` +```html title="src/app/avatar/avatar.component.html" +
+ Avatar +
+
+
+ + +
+``` + ### Add the new widget And then we can add the widget on top of the **AccountComponent** html template: -```ts title="src/app/account.component.ts" -template: ` - - +```html title="src/app/account.component.html" +
+ + + +
+``` - -` +And add an `updateAvatar` function along with an `avatarUrl` getter to the **AccountComponent** typescript file: + +```ts title="src/app/account.component.ts" +@Component({ + selector: 'app-account', + templateUrl: './account.component.html', + styleUrls: ['./account.component.css'], +}) +export class AccountComponent implements OnInit { + // ... + get avatarUrl() { + return this.updateProfileForm.value.avatar_url as string; + } + + async updateAvatar(event: string): Promise { + this.updateProfileForm.patchValue({ + avatar_url: event, + }); + await this.updateProfile(); + } + // ... +} ``` ## Next steps diff --git a/apps/reference/docs/guides/with-nextjs.mdx b/apps/reference/docs/guides/with-nextjs.mdx index ebcc253325c..3d40b368b08 100644 --- a/apps/reference/docs/guides/with-nextjs.mdx +++ b/apps/reference/docs/guides/with-nextjs.mdx @@ -61,6 +61,7 @@ Now we are going to set up the database schema. We can use the "User Management or you can just copy/paste the SQL from below and run it yourself. ```sql --- Create a table for public "profiles" +-- Create a table for public profiles create table profiles ( - id uuid references auth.users not null, + id uuid references auth.users not null primary key, updated_at timestamp with time zone, username text unique, + full_name text, avatar_url text, website text, - primary key (id), - unique(username), constraint username_length check (char_length(username) >= 3) ); +-- Set up Row Level Security (RLS) +-- See https://supabase.com/docs/guides/auth/row-level-security for more details. +alter table profiles + enable row level security; -alter table profiles enable row level security; +create policy "Public profiles are viewable by everyone." on profiles + for select using (true); -create policy "Public profiles are viewable by everyone." - on profiles for select - using ( true ); +create policy "Users can insert their own profile." on profiles + for insert with check (auth.uid() = id); -create policy "Users can insert their own profile." - on profiles for insert - with check ( auth.uid() = id ); +create policy "Users can update own profile." on profiles + for update using (auth.uid() = id); -create policy "Users can update own profile." - on profiles for update - using ( auth.uid() = id ); - --- Set up Realtime! -begin; - drop publication if exists supabase_realtime; - create publication supabase_realtime; -commit; -alter publication supabase_realtime add table profiles; +-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth. +-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details. +create function public.handle_new_user() +returns trigger as $$ +begin + insert into public.profiles (id, full_name, avatar_url) + values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url'); + return new; +end; +$$ language plpgsql security definer; +create trigger on_auth_user_created + after insert on auth.users + for each row execute procedure public.handle_new_user(); -- Set up Storage! insert into storage.buckets (id, name) -values ('avatars', 'avatars'); + values ('avatars', 'avatars'); -create policy "Avatar images are publicly accessible." - on storage.objects for select - using ( bucket_id = 'avatars' ); +-- Set up access controls for storage. +-- See https://supabase.com/docs/guides/storage#policy-examples for more details. +create policy "Avatar images are publicly accessible." on storage.objects + for select using (bucket_id = 'avatars'); -create policy "Anyone can upload an avatar." - on storage.objects for insert - with check ( bucket_id = 'avatars' ); +create policy "Anyone can upload an avatar." on storage.objects + for insert with check (bucket_id = 'avatars'); + +create policy "Anyone can update their own avatar." on storage.objects + for update using ( auth.uid() = owner ) with check (bucket_id = 'avatars'); ``` @@ -163,12 +172,32 @@ Let's start building the Next.js app from scratch. We can use [`create-next-app`](https://nextjs.org/docs/getting-started) to initialize an app called `supabase-nextjs`: + + + ```bash -npx create-next-app supabase-nextjs --use-npm +npx create-next-app@latest --use-npm supabase-nextjs cd supabase-nextjs ``` -Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js) + + + +```bash +npx create-next-app@latest --ts --use-npm supabase-nextjs +cd supabase-nextjs +``` + + + + +Then install the Supabase client library: [supabase-js](https://github.com/supabase/supabase-js) ```bash npm install @supabase/supabase-js @@ -182,93 +211,148 @@ NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY ``` -Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client. -These variables will be exposed on the browser, and that's completely fine since we have -[Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. - -```js title="utils/supabaseClient.js" -import { createClient } from '@supabase/supabase-js' - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY - -export const supabase = createClient(supabaseUrl, supabaseAnonKey) -``` - And one optional step is to update the CSS file `styles/globals.css` to make the app look nice. -You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/nextjs-ts-user-management/styles/globals.css). +You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-ts-user-management/styles/globals.css). ### Set up a Login component -Let's set up a React component to manage logins and sign ups. -We'll use Magic Links, so users can sign in with their email without using passwords. +#### Supabase Auth Helpers -```jsx title="components/Auth.js" -import { useState } from 'react' -import { supabase } from '../utils/supabaseClient' +Next.js is a highly versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and middleware edge-functions. -export default function Auth() { - const [loading, setLoading] = useState(false) - const [email, setEmail] = useState('') +It can be challenging to authenticate your users in all these different environments, that's why we've created the [Supabase Auth Helpers](https://supabase.com/docs/guides/auth/auth-helpers/nextjs) to make user management and data fetching within Next.js as easy as possible. - const handleLogin = async (email) => { - try { - setLoading(true) - const { error } = await supabase.auth.signInWithOtp({ email }) - if (error) throw error - alert('Check your email for the login link!') - } catch (error) { - alert(error.error_description || error.message) - } finally { - setLoading(false) - } - } +Install the auth helpers for React and Next.js + +```bash +npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs +``` + + + + +Wrap your `pages/_app.js` component with the `SessionContextProvider` component: + +```jsx title="pages/_app.js" +import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' +import { SessionContextProvider } from '@supabase/auth-helpers-react' + +function MyApp({ Component, pageProps }) { + const router = useRouter() + const [supabaseClient] = useState(() => createBrowserSupabaseClient()) return ( -
-
-

Supabase + Next.js

-

- Sign in via magic link with your email below -

-
- setEmail(e.target.value)} - /> -
-
- -
-
+ + + ) +} +``` + + + + +Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component: + +```jsx title="pages/_app.tsx" +import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' +// highlight-next-line +import { SessionContextProvider, Session } from '@supabase/auth-helpers-react' + +function MyApp({ + Component, + pageProps, +}: AppProps<{ + // highlight-next-line + initialSession: Session, +}>) { + const [supabaseClient] = useState(() => createBrowserSupabaseClient()) + + return ( + + + + ) +} +``` + +See the [Auth Helpers docs](/docs/guides/auth/auth-helpers/nextjs#usage-with-typescript) for more details on usage with TypeScript. + + + + +#### Supabase Auth UI + +We can use the [Supabase Auth UI](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui) a pre-built React component for authenticating users via OAuth, email, and magic links. + +Install the Supabase Auth UI for React + +```bash +npm install @supabase/auth-ui-react +``` + +Add the `Auth` component to your home page + +```jsx title="pages/index.js" +import type { NextPage } from 'next' +import { Auth, ThemeSupa } from '@supabase/auth-ui-react' +import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react' + +const Home: NextPage = () => { + const session = useSession() + const supabase = useSupabaseClient() + + return ( +
+ {!session ? ( + + ) : ( +

Account page will go here.

+ )}
) } + +export default Home ``` ### Account page After a user is signed in we can allow them to edit their profile details and manage their account. -Let's create a new component for that called `Account.js`. + + -```jsx title="components/Account.js" +Let's create a new component for that called `Account.js` within a `components` folder. + +```tsx title="components/Account.js" import { useState, useEffect } from 'react' -import { supabase } from '../utils/supabaseClient' +import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react' export default function Account({ session }) { + const supabase = useSupabaseClient() + const user = useUser() const [loading, setLoading] = useState(true) const [username, setUsername] = useState(null) const [website, setWebsite] = useState(null) @@ -278,27 +362,9 @@ export default function Account({ session }) { getProfile() }, [session]) - async function getCurrentUser() { - const { - data: { session }, - error, - } = await supabase.auth.getSession() - - if (error) { - throw error - } - - if (!session?.user) { - throw new Error('User not logged in') - } - - return session.user - } - async function getProfile() { try { setLoading(true) - const user = await getCurrentUser() let { data, error, status } = await supabase .from('profiles') @@ -316,7 +382,8 @@ export default function Account({ session }) { setAvatarUrl(data.avatar_url) } } catch (error) { - alert(error.message) + alert('Error loading user data!') + console.log(error) } finally { setLoading(false) } @@ -325,23 +392,21 @@ export default function Account({ session }) { async function updateProfile({ username, website, avatar_url }) { try { setLoading(true) - const user = await getCurrentUser() const updates = { id: user.id, username, website, avatar_url, - updated_at: new Date(), + updated_at: new Date().toISOString(), } let { error } = await supabase.from('profiles').upsert(updates) - - if (error) { - throw error - } + if (error) throw error + alert('Profile updated!') } catch (error) { - alert(error.message) + alert('Error updating the data!') + console.log(error) } finally { setLoading(false) } @@ -354,7 +419,7 @@ export default function Account({ session }) {
- + + + +Let's create a new component for that called `Account.tsx` within a `components` folder. + +```tsx title="components/Account.tsx" +import { useState, useEffect } from 'react' +import { + useUser, + useSupabaseClient, + Session, +} from '@supabase/auth-helpers-react' +import { Database } from '../utils/database.types' +type Profiles = Database['public']['Tables']['profiles']['Row'] + +export default function Account({ session }: { session: Session }) { + const supabase = useSupabaseClient() + const user = useUser() + const [loading, setLoading] = useState(true) + const [username, setUsername] = useState(null) + const [website, setWebsite] = useState(null) + const [avatar_url, setAvatarUrl] = useState(null) + + useEffect(() => { + getProfile() + }, [session]) + + async function getProfile() { + try { + setLoading(true) + if (!user) throw new Error('No user') + + let { data, error, status } = await supabase + .from('profiles') + .select(`username, website, avatar_url`) + .eq('id', user.id) + .single() + + if (error && status !== 406) { + throw error + } + + if (data) { + setUsername(data.username) + setWebsite(data.website) + setAvatarUrl(data.avatar_url) + } + } catch (error) { + alert('Error loading user data!') + console.log(error) + } finally { + setLoading(false) + } + } + + async function updateProfile({ + username, + website, + avatar_url, + }: { + username: Profiles['username'] + website: Profiles['website'] + avatar_url: Profiles['avatar_url'] + }) { + try { + setLoading(true) + if (!user) throw new Error('No user') + + const updates = { + id: user.id, + username, + website, + avatar_url, + updated_at: new Date().toISOString(), + } + + let { error } = await supabase.from('profiles').upsert(updates) + if (error) throw error + alert('Profile updated!') + } catch (error) { + alert('Error updating the data!') + console.log(error) + } finally { + setLoading(false) + } + } + + return ( +
+
+ + +
+
+ + setUsername(e.target.value)} + /> +
+
+ + setWebsite(e.target.value)} + /> +
+ +
+ +
+ +
+ +
+
+ ) +} +``` + +
+ + ### Launch! Now that we have all the components in place, let's update `pages/index.js`: ```jsx title="pages/index.js" -import { useState, useEffect } from 'react' -import { supabase } from '../utils/supabaseClient' -import Auth from '../components/Auth' +import type { NextPage } from 'next' +import { Auth, ThemeSupa } from '@supabase/auth-ui-react' +import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react' +// highlight-next-line import Account from '../components/Account' -export default function Home() { - const [isLoading, setIsLoading] = useState(true) - const [session, setSession] = useState(null) - - useEffect(() => { - let mounted = true - - async function getInitialSession() { - const { - data: { session }, - } = await supabase.auth.getSession() - - // only update the react state if the component is still mounted - if (mounted) { - if (session) { - setSession(session) - } - - setIsLoading(false) - } - } - - getInitialSession() - - const { subscription } = supabase.auth.onAuthStateChange( - (_event, session) => { - setSession(session) - } - ) - - return () => { - mounted = false - - subscription?.unsubscribe() - } - }, []) +const Home: NextPage = () => { + const session = useSession() + const supabase = useSupabaseClient() return (
{!session ? ( - + ) : ( - + // highlight-next-line + )}
) } + +export default Home ``` Once that's done, run this in a terminal window: @@ -462,8 +640,6 @@ npm run dev And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app. -![Supabase User demo](/img/supabase-react-demo.png) - ## Bonus: Profile photos Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like @@ -471,14 +647,23 @@ photos and videos. ### Create an upload widget -Let's create an avatar for the user so that they can upload a profile photo. -We can start by creating a new component: +Let's create an avatar widget for the user so that they can upload a profile photo. We can start by creating a new component: + + + ```jsx title="components/Avatar.js" -import { useEffect, useState } from 'react' -import { supabase } from '../utils/supabaseClient' +import React, { useEffect, useState } from 'react' +import { useSupabaseClient } from '@supabase/auth-helpers-react' -export default function Avatar({ url, size, onUpload }) { +export default function Avatar({ uid, url, size, onUpload }) { + const supabase = useSupabaseClient() const [avatarUrl, setAvatarUrl] = useState(null) const [uploading, setUploading] = useState(false) @@ -497,11 +682,11 @@ export default function Avatar({ url, size, onUpload }) { const url = URL.createObjectURL(data) setAvatarUrl(url) } catch (error) { - console.log('Error downloading image: ', error.message) + console.log('Error downloading image: ', error) } } - async function uploadAvatar(event) { + const uploadAvatar = async (event) => { try { setUploading(true) @@ -511,12 +696,12 @@ export default function Avatar({ url, size, onUpload }) { const file = event.target.files[0] const fileExt = file.name.split('.').pop() - const fileName = `${Math.random()}.${fileExt}` + const fileName = `${uid}.${fileExt}` const filePath = `${fileName}` let { error: uploadError } = await supabase.storage .from('avatars') - .upload(filePath, file) + .upload(filePath, file, { upsert: true }) if (uploadError) { throw uploadError @@ -524,7 +709,8 @@ export default function Avatar({ url, size, onUpload }) { onUpload(filePath) } catch (error) { - alert(error.message) + alert('Error uploading avatar!') + console.log(error) } finally { setUploading(false) } @@ -566,6 +752,120 @@ export default function Avatar({ url, size, onUpload }) { } ``` + + + +```tsx title="components/Avatar.tsx" +import React, { useEffect, useState } from 'react' +import { useSupabaseClient } from '@supabase/auth-helpers-react' +import { Database } from '../utils/database.types' +type Profiles = Database['public']['Tables']['profiles']['Row'] + +export default function Avatar({ + uid, + url, + size, + onUpload, +}: { + uid: string + url: Profiles['avatar_url'] + size: number + onUpload: (url: string) => void +}) { + const supabase = useSupabaseClient() + const [avatarUrl, setAvatarUrl] = useState(null) + const [uploading, setUploading] = useState(false) + + useEffect(() => { + if (url) downloadImage(url) + }, [url]) + + async function downloadImage(path: string) { + try { + const { data, error } = await supabase.storage + .from('avatars') + .download(path) + if (error) { + throw error + } + const url = URL.createObjectURL(data) + setAvatarUrl(url) + } catch (error) { + console.log('Error downloading image: ', error) + } + } + + const uploadAvatar: React.ChangeEventHandler = async ( + event + ) => { + try { + setUploading(true) + + if (!event.target.files || event.target.files.length === 0) { + throw new Error('You must select an image to upload.') + } + + const file = event.target.files[0] + const fileExt = file.name.split('.').pop() + const fileName = `${uid}.${fileExt}` + const filePath = `${fileName}` + + let { error: uploadError } = await supabase.storage + .from('avatars') + .upload(filePath, file, { upsert: true }) + + if (uploadError) { + throw uploadError + } + + onUpload(filePath) + } catch (error) { + alert('Error uploading avatar!') + console.log(error) + } finally { + setUploading(false) + } + } + + return ( +
+ {avatarUrl ? ( + Avatar + ) : ( +
+ )} +
+ + +
+
+ ) +} +``` + + + + ### Add the new widget And then we can add the widget to the Account page: @@ -580,6 +880,7 @@ return (
{/* Add to the body */} { @@ -596,5 +897,8 @@ return ( At this stage you have a fully functional application! +- See the complete [example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management) and deploy it to Vercel. +- Explore the [Auth Helpers for Next.js](https://supabase.com/docs/guides/auth/auth-helpers/nextjs). +- Explore the [pre-built Auth UI for React](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui). - Got a question? [Ask here](https://github.com/supabase/supabase/discussions). - Sign in: [app.supabase.com](https://app.supabase.com) diff --git a/apps/reference/docs/guides/with-solidjs.mdx b/apps/reference/docs/guides/with-solidjs.mdx index da23c294236..e601c98aee8 100644 --- a/apps/reference/docs/guides/with-solidjs.mdx +++ b/apps/reference/docs/guides/with-solidjs.mdx @@ -271,9 +271,9 @@ interface Props { const Account: Component = ({ session }) => { const [loading, setLoading] = createSignal(true) - const [username, setUsername] = (createSignal < string) | (null > null) - const [website, setWebsite] = (createSignal < string) | (null > null) - const [avatarUrl, setAvatarUrl] = (createSignal < string) | (null > null) + const [username, setUsername] = createSignal(null) + const [website, setWebsite] = createSignal(null) + const [avatarUrl, setAvatarUrl] = createSignal(null) createEffect(() => { getProfile() diff --git a/apps/reference/docs/guides/with-vue-3.mdx b/apps/reference/docs/guides/with-vue-3.mdx index 06b7f9e0617..49ce6b3739b 100644 --- a/apps/reference/docs/guides/with-vue-3.mdx +++ b/apps/reference/docs/guides/with-vue-3.mdx @@ -24,7 +24,7 @@ By the end of this guide you'll have an app which allows users to login and upda ### GitHub -Whenever you get stuck at any point, take a look at [this repo](https://github.com/zernonia/supabase-vue-3). +Whenever you get stuck at any point, take a look at [this repo](https://github.com/supabase/examples/tree/main/supabase-js-v2/user-management/vue3-user-management). ## Project set up @@ -182,102 +182,141 @@ const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY export const supabase = createClient(supabaseUrl, supabaseAnonKey) ``` -And one optional step is to update the CSS file `src/assets/main.css` to make the app look nice. -You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/react-user-management/src/index.css). - -```javascript title="src/main.js" -import { createApp } from 'vue' -import App from './App.vue' -import './assets/main.css' - -createApp(App).mount('#app') -``` +Optionally, update [src/style.css](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/vue3-user-management/src/style.css) to style the app. ### Set up a Login component Let's set up a Vue component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. ```html title="/src/components/Auth.vue" + + - - -``` - -### User store - -To access the user information in other places, we use a reactive store. Create a new file called `store.js` and utilize Vue 3 `reactive` functionalities. - -```javascript title="src/store.js" -import { reactive } from 'vue' - -export const store = reactive({ - user: {}, -}) ``` ### Account page After a user is signed in we can allow them to edit their profile details and manage their account. -Let's create a new component for that called `Profile.vue`. +Let's create a new component for that called `Account.vue`. + +```html title="src/components/Account.vue" + -```html title="src/components/Profile.vue" - - ``` ### Launch! @@ -403,37 +346,31 @@ Let's create a new component for that called `Profile.vue`. Now that we have all the components in place, let's update `App.vue`: ```html title="src/App.vue" + + - - ``` Once that's done, run this in a terminal window: @@ -442,7 +379,7 @@ Once that's done, run this in a terminal window: npm run dev ``` -And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app. +And then open the browser to [localhost:5173](http://localhost:5173) and you should see the completed app. ![Supabase Vue 3](/img/supabase-vue-3-demo.png) @@ -455,132 +392,96 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component: ```html title="src/components/Avatar.vue" + + - - ``` ### Add the new widget And then we can add the widget to the Account page: -```html title="src/Profile.vue" +```html title="src/components/Account.vue" + + - - ``` ## Next steps diff --git a/apps/reference/docusaurus.config.js b/apps/reference/docusaurus.config.js index ba1093b6fb0..56e2df43582 100644 --- a/apps/reference/docusaurus.config.js +++ b/apps/reference/docusaurus.config.js @@ -14,7 +14,7 @@ const config = { url: 'https://supabase.com', baseUrl: baseUrl, onBrokenLinks: 'throw', - onBrokenMarkdownLinks: 'warn', + onBrokenMarkdownLinks: 'throw', favicon: '/favicon.ico', themes: ['docusaurus-theme-search-typesense'], diff --git a/apps/reference/nav/_referenceSidebars.js b/apps/reference/nav/_referenceSidebars.js index 60ad0dcf290..ea72197d6f6 100644 --- a/apps/reference/nav/_referenceSidebars.js +++ b/apps/reference/nav/_referenceSidebars.js @@ -215,23 +215,51 @@ const sidebars = { label: 'Integrations', collapsed: true, items: [ - 'guides/integrations/appsmith', - 'guides/integrations/auth0', - 'guides/integrations/authsignal', - 'guides/integrations/clerk', - 'guides/integrations/dashibase', - 'guides/integrations/directus', - 'guides/integrations/draftbit', - 'guides/integrations/keyri', - 'guides/integrations/pgmustard', - 'guides/integrations/plasmic', - 'guides/integrations/polyscale', - 'guides/integrations/prisma', - 'guides/integrations/sequin', - 'guides/integrations/snaplet', - 'guides/integrations/stytch', - 'guides/integrations/supertokens', - 'guides/integrations/vercel', + 'guides/integrations/integrations', + { + type: 'category', + label: 'Auth', + collapsed: true, + items: [ + 'guides/integrations/auth0', + 'guides/integrations/authsignal', + 'guides/integrations/clerk', + 'guides/integrations/keyri', + 'guides/integrations/stytch', + 'guides/integrations/supertokens', + ], + }, + { + type: 'category', + label: 'Caching / Offline-first', + collapsed: true, + items: ['guides/integrations/polyscale'], + }, + { + type: 'category', + label: 'Developer Tools', + collapsed: true, + items: [ + 'guides/integrations/pgmustard', + 'guides/integrations/prisma', + 'guides/integrations/sequin', + 'guides/integrations/snaplet', + 'guides/integrations/vercel', + ], + }, + { + type: 'category', + label: 'Low-code', + collapsed: true, + items: [ + 'guides/integrations/appsmith', + 'guides/integrations/dashibase', + 'guides/integrations/dhiwise', + 'guides/integrations/directus', + 'guides/integrations/draftbit', + 'guides/integrations/plasmic', + ], + }, ], }, ], diff --git a/apps/reference/static/img/guides/integrations/dhiwise/auth.gif b/apps/reference/static/img/guides/integrations/dhiwise/auth.gif new file mode 100644 index 00000000000..db05bb42fca Binary files /dev/null and b/apps/reference/static/img/guides/integrations/dhiwise/auth.gif differ diff --git a/apps/reference/static/img/guides/integrations/dhiwise/authKeys.png b/apps/reference/static/img/guides/integrations/dhiwise/authKeys.png new file mode 100644 index 00000000000..1ac402f5d9b Binary files /dev/null and b/apps/reference/static/img/guides/integrations/dhiwise/authKeys.png differ diff --git a/apps/reference/static/img/guides/integrations/dhiwise/create.png b/apps/reference/static/img/guides/integrations/dhiwise/create.png new file mode 100644 index 00000000000..bcb06e5e5ee Binary files /dev/null and b/apps/reference/static/img/guides/integrations/dhiwise/create.png differ diff --git a/apps/reference/static/img/guides/integrations/dhiwise/newProj.png b/apps/reference/static/img/guides/integrations/dhiwise/newProj.png new file mode 100644 index 00000000000..e3f5d8db355 Binary files /dev/null and b/apps/reference/static/img/guides/integrations/dhiwise/newProj.png differ diff --git a/apps/reference/static/img/guides/integrations/dhiwise/select.png b/apps/reference/static/img/guides/integrations/dhiwise/select.png new file mode 100644 index 00000000000..3de535da47e Binary files /dev/null and b/apps/reference/static/img/guides/integrations/dhiwise/select.png differ diff --git a/apps/www/README.md b/apps/www/README.md index ab4de38784c..5505fe5a8a0 100644 --- a/apps/www/README.md +++ b/apps/www/README.md @@ -16,7 +16,6 @@ cd supabase npm install npm run dev - # visit website http://localhost:3000 ``` diff --git a/apps/www/_blog/2022-10-14-postgres-full-text-search-vs-the-rest.mdx b/apps/www/_blog/2022-10-14-postgres-full-text-search-vs-the-rest.mdx index d14a3b671cb..f96545c783a 100644 --- a/apps/www/_blog/2022-10-14-postgres-full-text-search-vs-the-rest.mdx +++ b/apps/www/_blog/2022-10-14-postgres-full-text-search-vs-the-rest.mdx @@ -206,7 +206,7 @@ We'll focus on the end-user-centric meaning of performance here (query speed). Postgres is capable of loading CSVs, but since we'll be using other search engines as well, let's convert to format that's much easier to use and process - [Newline Delimited JSON](https://dataprotocols.org/ndjson/). -After a few lines of code and one `[csv2ndjson.mjs` script](https://github.com/VADOSWARE/fts-benchmark/blob/main/src/util/csv2ndjson.mjs) later, we have a `movies.ndjson.json` full of JSON documents that we can easily ingest into any search engine (or other database for that matter!). +After a few lines of code and one [`csv2ndjson.mjs` script](https://github.com/VADOSWARE/fts-benchmark/blob/main/src/util/csv2ndjson.mjs) later, we have a `movies.ndjson.json` full of JSON documents that we can easily ingest into any search engine (or other database for that matter!). ### Inserting the data diff --git a/apps/www/components/CaseStudies/benchmark.tsx b/apps/www/components/CaseStudies/benchmark.tsx index 630e7b6e7cf..1e2a218bf1a 100644 --- a/apps/www/components/CaseStudies/benchmark.tsx +++ b/apps/www/components/CaseStudies/benchmark.tsx @@ -31,11 +31,6 @@ const Benchmark = () => {

-
) diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index 97e6e3bfc04..bed6e889a50 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -270,7 +270,7 @@ module.exports = [ { permanent: false, source: '/docs/client/auth-session', - destination: '/docs/reference/javascript/auth-session', + destination: '/docs/reference/javascript/v1/auth-session', }, { permanent: false, @@ -325,12 +325,12 @@ module.exports = [ { permanent: false, source: '/docs/client/removesubscription', - destination: '/docs/reference/javascript/removesubscription', + destination: '/docs/reference/javascript/v1/removesubscription', }, { permanent: false, source: '/docs/client/getsubscriptions', - destination: '/docs/reference/javascript/getsubscriptions', + destination: '/docs/reference/javascript/v1/getsubscriptions', }, { permanent: false, @@ -1359,60 +1359,120 @@ module.exports = [ destination: '/docs/guides/auth/auth-helpers/sveltekit', }, - // V2 redirects - // { - // permanent: true, - // source: '/docs/reference/javascript/auth-update', - // destination: '/docs/reference/javascript/auth-updateuser', - // }, - // { - // permanent: true, - // source: '/docs/reference/javascript/auth-api-getuser', - // destination: '/docs/reference/javascript/auth-getuser', - // }, - // { - // permanent: true, - // source: '/docs/reference/javascript/auth-api-resetpasswordforemail', - // destination: '/docs/reference/javascript/auth-resetpasswordforemail', - // }, - // { - // permanent: true, - // source: '/docs/reference/javascript/auth-api-verifyotp', - // destination: '/docs/reference/javascript/auth-verifyotp', - // }, - // { - // permanent: true, - // source: '/docs/reference/javascript/auth-api-listusers', - // destination: '/docs/reference/javascript/auth-admin-listusers', - // }, - // { - // permanent: true, - // source: '/docs/reference/javascript/auth-api-createuser', - // destination: '/docs/reference/javascript/auth-admin-createuser', - // }, - // { - // permanent: true, - // source: '/docs/reference/javascript/auth-api-deleteuser', - // destination: '/docs/reference/javascript/auth-admin-deleteuser', - // }, - // { - // permanent: true, - // source: '/docs/reference/javascript/auth-api-generatelink', - // destination: '/docs/reference/javascript/auth-admin-generatelink', - // }, - // { - // permanent: true, - // source: '/docs/reference/javascript/auth-api-inviteuserbyemail', - // destination: '/docs/reference/javascript/auth-admin-inviteuserbyemail', - // }, - // { - // permanent: true, - // source: '/docs/reference/javascript/auth-api-getuserbyid', - // destination: '/docs/reference/javascript/auth-admin-getuserbyid', - // }, - // { - // permanent: true, - // source: '/docs/reference/javascript/auth-api-updateuserbyid', - // destination: '/docs/reference/javascript/auth-admin-updateuserbyid', - // }, + // supabase-js v1 to v2 redirects + // v1: /auth-update + // v2: /auth-updateuser + { + permanent: true, + source: '/docs/reference/javascript/auth-update', + destination: '/docs/reference/javascript/v1/auth-update', + }, + // v1: /auth-api-get-user + // v2: /auth-getuser + { + permanent: true, + source: '/docs/reference/javascript/auth-api-getuser', + destination: '/docs/reference/javascript/v1/auth-api-getuser', + }, + // v1: /auth-api-resetpasswordforemail + // v2: /auth-resetpasswordforemail + { + permanent: true, + source: '/docs/reference/javascript/auth-api-resetpasswordforemail', + destination: '/docs/reference/javascript/v1/auth-api-resetpasswordforemail', + }, + // v1: /auth-api-verifyotp + // v2: /auth-verifyotp + { + permanent: true, + source: '/docs/reference/javascript/auth-api-verifyotp', + destination: '/docs/reference/javascript/v1/auth-api-verifyotp', + }, + // v1: /auth-api-listusers + // v2: /auth-admin-listusers + { + permanent: true, + source: '/docs/reference/javascript/auth-api-listusers', + destination: '/docs/reference/javascript/v1/auth-api-listusers', + }, + // v1: /auth-api-createuser + // v2: /auth-admin-createuser + { + permanent: true, + source: '/docs/reference/javascript/auth-api-createuser', + destination: '/docs/reference/javascript/v1/auth-api-createuser', + }, + // v1: /auth-api-deleteuser + // v2: /auth-admin-deleteuser + { + permanent: true, + source: '/docs/reference/javascript/auth-api-deleteuser', + destination: '/docs/reference/javascript/v1/auth-api-deleteuser', + }, + // v1: /auth-api-generatelink + // v2: /auth-admin-generatelink + { + permanent: true, + source: '/docs/reference/javascript/auth-api-generatelink', + destination: '/docs/reference/javascript/v1/auth-api-generatelink', + }, + // v1: /auth-api-inviteuserbyemail + // v2: /auth-admin-inviteuserbyemail + { + permanent: true, + source: '/docs/reference/javascript/auth-api-inviteuserbyemail', + destination: '/docs/reference/javascript/v1/auth-api-inviteuserbyemail', + }, + // v1: /auth-api-getuserbyid + // v2: /auth-admin-getuserbyid + { + permanent: true, + source: '/docs/reference/javascript/auth-api-getuserbyid', + destination: '/docs/reference/javascript/v1/auth-api-getuserbyid', + }, + // v1: /auth-api-updateuserbyid + // v2: /auth-admin-updateuserbyid + { + permanent: true, + source: '/docs/reference/javascript/auth-api-updateuserbyid', + destination: '/docs/reference/javascript/v1/auth-api-updateuserbyid', + }, + // signIn method is now split into signInWithPassword ,signInWithPasswordless ,signInWithOAuth + // send traffic to v1 docs instead + { + permanent: true, + source: '/docs/reference/javascript/auth-signin', + destination: '/docs/reference/javascript/v1/auth-signin', + }, + // v1: /auth-session + // v2: /auth-getsession + { + permanent: true, + source: '/docs/reference/javascript/auth-session', + destination: '/docs/reference/javascript/v1/auth-session', + }, + // v1: /auth-api-sendmobileotp + // v2: /auth-signinwithotp + { + permanent: true, + source: '/docs/reference/javascript/auth-api-sendmobileotp', + destination: '/docs/reference/javascript/v1/auth-api-sendmobileotp', + }, + + // realtime methods been replaced with new names + { + permanent: true, + source: '/docs/reference/javascript/removesubscription', + destination: '/docs/reference/javascript/v1/removesubscription', + }, + { + permanent: true, + source: '/docs/reference/javascript/removeallsubscriptions', + destination: '/docs/reference/javascript/v1/removeallsubscriptions', + }, + { + permanent: true, + source: '/docs/reference/javascript/getsubscriptions', + destination: '/docs/reference/javascript/v1/getsubscriptions', + }, ] diff --git a/apps/www/pages/legal/dpa.tsx b/apps/www/pages/legal/dpa.tsx index ad9ecd2b3d4..0cb1cc54d2d 100644 --- a/apps/www/pages/legal/dpa.tsx +++ b/apps/www/pages/legal/dpa.tsx @@ -49,11 +49,7 @@ const DPA = () => { ("DPA").

-

- You can download the latest DPA document through our{' '} - security portal, or by - submitting your email here. -

+

You can download our latest DPA document by submitting your email here.

{formSubmitted ? (

diff --git a/apps/www/pages/legal/soc2.tsx b/apps/www/pages/legal/soc2.tsx index c58ebe4dfef..f5fc2a80fac 100644 --- a/apps/www/pages/legal/soc2.tsx +++ b/apps/www/pages/legal/soc2.tsx @@ -18,11 +18,7 @@ const SOC2 = () => { getting certified for SOC2 Type 2 and HIPAA next.

-

- You can request for our latest SOC 2 document either through our{' '} - security portal, or by submitting - your request here. -

+

You can request for our latest SOC 2 document by submitting your request here.

diff --git a/apps/www/pages/security.mdx b/apps/www/pages/security.mdx index 43867d08290..19fc0f6fdeb 100644 --- a/apps/www/pages/security.mdx +++ b/apps/www/pages/security.mdx @@ -52,7 +52,7 @@ export const Section = ({ children, icon, img }) => ( ### SOC 2 -Supabase is SOC2 Type 1 compliant. Enterprise customers can request a copy of our SOC2 through our [Security Portal](https://security.supabase.com). +Supabase is SOC2 Type 1 compliant. Enterprise customers can request a copy of our SOC2 [here](https://supabase.com/legal/soc2).
@@ -126,8 +126,7 @@ In addition to internal security reviews, we use various tools to scan our code

Security Portal

-Access Supabase’s security and compliance documents on our Security Portal, such as -penetration testing and audit reports. +Access more of Supabase’s security information on our Security Portal, such as penetration testing and audit reports. @@ -120,7 +120,7 @@ const NewProjectPanel: FC = ({}) => { - +
- + diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.constants.tsx b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.constants.tsx deleted file mode 100644 index c705721fab6..00000000000 --- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.constants.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { IconArchive, IconDatabase, IconKey } from 'ui' - -export const usageBasedItems = [ - { - title: 'Database', - icon: , - features: [ - { key: 'db_size', title: 'Database space', units: 'bytes' }, - { key: 'db_egress', title: 'Database egress', units: 'bytes' }, - ], - }, - { - title: 'Auth', - icon: , - features: [{ key: 'monthly_active_users', title: 'Monthly Active Users', units: 'absolute' }], - }, - { - title: 'Storage', - icon: , - features: [ - { key: 'storage_size', title: 'Storage space', units: 'bytes' }, - { key: 'storage_egress', title: 'Storage egress', units: 'bytes' }, - ], - }, -] diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx index 6970d676909..531f40a9a40 100644 --- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx +++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx @@ -6,8 +6,8 @@ import { formatBytes } from 'lib/helpers' import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants' import SparkBar from 'components/ui/SparkBar' import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { usageBasedItems } from './ProjectUsageBars.constants' import InformationBox from 'components/ui/InformationBox' +import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants' interface Props { projectRef?: string @@ -48,7 +48,7 @@ const ProjectUsage: FC = ({ projectRef }) => { {usage && (
- {usageBasedItems.map((product) => { + {USAGE_BASED_PRODUCTS.map((product) => { const isExceededUsage = showUsageExceedMessage && product.features diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.types.ts b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.types.ts index 7b419d8f0da..542264ebca6 100644 --- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.types.ts +++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.types.ts @@ -1,6 +1,7 @@ export interface ResourceUsage { usage: number limit: number + cost: number } export interface UsageStats { diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageMinimal.tsx b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageMinimal.tsx index 94c38d4acc6..88859e5206b 100644 --- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageMinimal.tsx +++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageMinimal.tsx @@ -5,7 +5,7 @@ import { useProjectSubscription, useProjectUsage } from 'hooks' import { formatBytes } from 'lib/helpers' import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants' import SparkBar from 'components/ui/SparkBar' -import { usageBasedItems } from './ProjectUsageBars.constants' +import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants' interface ProjectUsageMinimalProps { projectRef?: string @@ -23,7 +23,7 @@ const ProjectUsageMinimal: FC = ({ projectRef, filter return <> } - const product = usageBasedItems.find((item) => item.title === filter) + const product = USAGE_BASED_PRODUCTS.find((item) => item.title === filter) if (!product) return <> if (usageError || subscriptionError) { diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx index 98897ffe6d0..68273a549ee 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx @@ -212,7 +212,7 @@ const ColumnEditor: FC = ({ disabled={!isUndefined(columnFields?.foreignKey)} description={ - + )} - + - + +
+ {screenshot !== undefined ? ( +
{ + const blob = convertB64toBlob(screenshot) + const blobUrl = URL.createObjectURL(blob) + window.open(blobUrl, '_blank') + }} + className="cursor-pointer rounded h-[26px] w-[30px] border border-scale-600 relative bg-cover bg-center bg-no-repeat" + > +
{ + event.stopPropagation() + setScreenshot(undefined) + }} + > + +
+
+ ) : ( + + + +

Have a technical issue? Contact{' '} diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationActions.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationActions.tsx index c7ee95701b4..c62892bc2e4 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationActions.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationActions.tsx @@ -90,7 +90,7 @@ const NotificationActions: FC = ({ })} {changelogLink && ( - + diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.tsx index 5acfa19eaa5..be32b086b81 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/NotificationRows.utils.tsx @@ -129,7 +129,7 @@ export const formatNotificationText = ( {upgrade.changelog_link && (

diff --git a/studio/hooks/queries/index.ts b/studio/hooks/queries/index.ts index 53c6ca96435..b93e9618493 100644 --- a/studio/hooks/queries/index.ts +++ b/studio/hooks/queries/index.ts @@ -5,7 +5,6 @@ export * from './useFreeProjectLimitCheck' export * from './usePermissions' export * from './useProfile' export * from './useProjectAuthConfig' -export * from './useProjectPaygStatistics' export * from './useProjectSettings' export * from './useProjectSubscription' export * from './useSubscriptionStats' diff --git a/studio/hooks/queries/useProjectPaygStatistics.ts b/studio/hooks/queries/useProjectPaygStatistics.ts deleted file mode 100644 index 3af268448ac..00000000000 --- a/studio/hooks/queries/useProjectPaygStatistics.ts +++ /dev/null @@ -1,82 +0,0 @@ -import dayjs from 'dayjs' -import useSWR from 'swr' -import { get as _get, maxBy } from 'lodash' -import { get } from 'lib/common/fetch' -import { API_URL, DATE_FORMAT, PRICING_TIER_PRODUCT_IDS } from 'lib/constants' -import { chargeableProducts } from 'components/interfaces/Billing' -import { PaygStats } from 'components/interfaces/Billing/PAYGUsage/PAYGUsage.types' - -/** - * Get project payg statistics - * - * @param ref project reference - * @param tierProdId tier product supabase_prod_id - */ -export function useProjectPaygStatistics(ref?: string, tierProdId?: string) { - const startDate = dayjs().utc().startOf('month').format(DATE_FORMAT) - const endDate = dayjs().utc().endOf('month').format(DATE_FORMAT) - const attributes = - 'total_egress,total_db_size_bytes,total_db_egress_bytes,total_storage_size_bytes,total_storage_egress' - const url = `${API_URL}/projects/${ref}/daily-stats?attribute=${attributes}&startDate=${encodeURIComponent( - startDate - )}&endDate=${encodeURIComponent(endDate)}&interval='1d'` - /** - * data is a dictionary - * { - * data: array, - * totalAverage: number, - * total: number, - * maximum: number, - * format: string, - * } - */ - const { data, error } = useSWR( - ref && tierProdId === PRICING_TIER_PRODUCT_IDS.PAYG ? url : null, - get - ) - const anyError = data?.error || error - - return { - paygStats: anyError ? undefined : getPaygStats(data?.data), - error: anyError, - isLoading: !anyError && !data, - isError: !!anyError, - } -} - -function getPaygStats(data: any): PaygStats { - const paygStats: any = {} - if (!data) return paygStats - - let dataModified: any = [] - data.map((x: any) => { - dataModified.push({ - ...x, - total_egress_modified: - (x.total_egress == undefined ? 0 : x.total_egress) - - (x.total_storage_egress == undefined ? 0 : x.total_storage_egress), - }) - }) - - chargeableProducts.forEach((product) => { - product.features.forEach((feature) => { - // calculate max - const max = _get(maxBy(dataModified, feature.attribute), feature.attribute) ?? 0 - - // calculate sum - let sum = 0 - dataModified.map((x: any) => { - const featureAttribute = x[feature.attribute] == undefined ? 0 : x[feature.attribute] - sum = sum + featureAttribute - }) - - // set sum and max under attribute name - paygStats[feature.attribute] = { - sum, - max, - } - }) - }) - - return paygStats -} diff --git a/studio/lib/api/apiWrapper.ts b/studio/lib/api/apiWrapper.ts index 26935f89c23..a3bd3484998 100644 --- a/studio/lib/api/apiWrapper.ts +++ b/studio/lib/api/apiWrapper.ts @@ -29,7 +29,7 @@ export default async function apiWrapper( } } - const func = withSentry(handler) + const func = withSentry(handler as any) // @ts-ignore return await func(req, res) } catch (error) { diff --git a/studio/package.json b/studio/package.json index 4c8457f638a..1b9790d0711 100644 --- a/studio/package.json +++ b/studio/package.json @@ -32,13 +32,14 @@ "analytics-node": "^3.5.0", "awesome-debounce-promise": "^2.1.0", "clipboard": "^2.0.8", - "config": "*", "common": "*", + "config": "*", "configcat-js": "^5.6.0", "crypto-js": "^4.1.1", "dayjs": "^1.11.0", "file-saver": "^2.0.5", "generate-password": "^1.7.0", + "html-to-image": "^1.10.8", "immutability-helper": "^3.1.1", "json-logic-js": "^2.0.2", "lodash": "^4.17.21", diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index 64b3e5375c8..deefe6cd8ee 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -10,6 +10,7 @@ import 'styles/contextMenu.scss' import 'styles/react-data-grid-logs.scss' import 'styles/date-picker.scss' import 'styles/grid.scss' +import 'styles/users-table.scss' import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' diff --git a/studio/pages/index.tsx b/studio/pages/index.tsx index c85615379e9..4cb6f9d2660 100644 --- a/studio/pages/index.tsx +++ b/studio/pages/index.tsx @@ -61,7 +61,11 @@ const UnauthorizedLanding = () => { auth.signInWithOAuth({ provider: 'github', options: { - redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}?${params.toString()}`, + redirectTo: `${ + process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' + ? process.env.NEXT_PUBLIC_VERCEL_URL + : process.env.NEXT_PUBLIC_SITE_URL + }?${params.toString()}`, }, }) } diff --git a/studio/pages/project/[ref]/settings/billing/index.tsx b/studio/pages/project/[ref]/settings/billing/index.tsx index f1db9798a2a..116e2dedbab 100644 --- a/studio/pages/project/[ref]/settings/billing/index.tsx +++ b/studio/pages/project/[ref]/settings/billing/index.tsx @@ -4,14 +4,13 @@ import { observer } from 'mobx-react-lite' import { Loading, IconArrowRight } from 'ui' import { Project, NextPageWithLayout } from 'types' -import { useProjectPaygStatistics, useProjectSubscription, useStore } from 'hooks' +import { useProjectSubscription, useStore } from 'hooks' import { STRIPE_PRODUCT_IDS, TIME_PERIODS_REPORTS, TIME_PERIODS_BILLING } from 'lib/constants' import { SettingsLayout } from 'components/layouts' import LoadingUI from 'components/ui/Loading' import OveragesBanner from 'components/ui/OveragesBanner/OveragesBanner' import DateRangePicker from 'components/to-be-cleaned/DateRangePicker' import { PAYGUsage, Subscription, Invoices } from 'components/interfaces/Billing' -import { PaygStats } from 'components/interfaces/Billing/PAYGUsage/PAYGUsage.types' import ProjectUsage from 'components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars' const ProjectBilling: NextPageWithLayout = () => { @@ -39,9 +38,6 @@ interface SettingsProps { const Settings: FC = ({ project }) => { const { ui } = useStore() - const projectTier = ui.selectedProject?.subscription_tier - - const [dateRange, setDateRange] = useState() const { subscription, @@ -49,10 +45,9 @@ const Settings: FC = ({ project }) => { error, } = useProjectSubscription(ui.selectedProject?.ref) - const { paygStats }: { paygStats: PaygStats | undefined } = useProjectPaygStatistics( - ui.selectedProject?.ref, - subscription?.tier?.supabase_prod_id - ) + const [dateRange, setDateRange] = useState() + const projectTier = ui.selectedProject?.subscription_tier + const isPayg = subscription?.tier?.prod_id === STRIPE_PRODUCT_IDS.PAYG useEffect(() => { if (error) { @@ -74,7 +69,6 @@ const Settings: FC = ({ project }) => { loading={loading} project={project} subscription={subscription} - paygStats={paygStats} currentPeriodStart={subscription?.billing.current_period_start} currentPeriodEnd={subscription?.billing.current_period_end} /> @@ -86,7 +80,7 @@ const Settings: FC = ({ project }) => {
- ) : subscription?.tier?.prod_id === STRIPE_PRODUCT_IDS.PAYG ? ( + ) : isPayg ? (
= ({ project }) => {
)}
- {paygStats && dateRange && } + {dateRange && } ) : ( diff --git a/studio/styles/monaco.scss b/studio/styles/monaco.scss index ef48f841bce..edb6be20dd6 100644 --- a/studio/styles/monaco.scss +++ b/studio/styles/monaco.scss @@ -32,12 +32,17 @@ span.th.tr.td.textarea { } .grid-monaco-editor, -.monaco-editor .suggest-widget .main { - max-height: none; - height: inherit; - width: 100%; - margin: inherit; - padding: inherit; - overflow: inherit; - background-color: inherit; +.monaco-editor .suggest-widget { + .main { + max-height: none; + height: inherit; + width: 100%; + margin: inherit; + padding: inherit; + overflow: inherit; + } + + .monaco-list-row.focused .main { + background: var(--vscode-quickInput-background); + } } diff --git a/studio/styles/users-table.scss b/studio/styles/users-table.scss new file mode 100644 index 00000000000..5bd24e6d6e5 --- /dev/null +++ b/studio/styles/users-table.scss @@ -0,0 +1,39 @@ +.users-table-container { + scrollbar-width: thin; + scrollbar-color: var(--colors-slate9) var(--colors-slate1); + + .dark & { + scrollbar-color: var(--colors-slate10) var(--colors-slate3o); + } + + ::-webkit-scrollbar { + background: var(--colors-slate2); + height: 3px; + width: 0px; + border-radius: 0; + + .dark & { + background: var(--colors-slate6); + } + } + + ::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px var(--colors-slate6); + -webkit-border-radius: 5px; + border-radius: 5px; + + .dark & { + -webkit-box-shadow: inset 0 0 6px var(--colors-slate8); + } + } + + ::-webkit-scrollbar-thumb { + -webkit-border-radius: 5px; + border-radius: 5px; + background: var(--colors-slate9); + + .dark & { + -webkit-box-shadow: inset 0 0 6px var(--colors-slate12); + } + } +}