diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 7d798d837a8..0c9a3c46ebd 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -1,15 +1,9 @@ -# This is a basic workflow to help you get started with Actions name: Tests # Controls when the workflow will run on: - # Triggers the workflow on push or pull request events but only for the main branch - push: - branches: [master] - pull_request: - branches: [master] - - # Allows you to run this workflow manually from the Actions tab + schedule: + - cron: '0 4/6 * * *' workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel @@ -41,17 +35,23 @@ jobs: - name: Install dependencies run: npm ci - - name: Run infrastructure - run: | - cp ../docker/.env.example ../docker/.env - npm run docker:up + - uses: supabase/setup-cli@v1 + - run: supabase start - name: Run Test - run: npm run test + run: npm run test:local + env: + SUPABASE_DB_PORT: 54322 + SUPABASE_DB_PASS: postgres + SUPABASE_DB_HOST: localhost + SUPABASE_GOTRUE: http://localhost:54321 + SUPABASE_URL: http://localhost:54321 + SUPABASE_KEY_ANON: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs + SUPABASE_KEY_ADMIN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU - name: Stop infrastructure if: always() - run: npm run docker:down + run: supabase stop - name: Get Allure history uses: actions/checkout@v2 @@ -73,7 +73,7 @@ jobs: keep_reports: 50 - name: Deploy report to Github Pages - if: ${{ !github.event.pull_request.head.repo.fork }} + if: always() uses: peaceiris/actions-gh-pages@v2 env: EXTERNAL_REPOSITORY: supabase/test-reports @@ -82,7 +82,7 @@ jobs: PUBLISH_DIR: allure-history - name: Post the link to the report - if: ${{ !github.event.pull_request.head.repo.fork }} + if: always() uses: Sibz/github-status-action@v1 with: authToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/reference/_api/usage.mdx b/apps/reference/_api/usage.mdx index 26e64d10479..09584d7a96b 100644 --- a/apps/reference/_api/usage.mdx +++ b/apps/reference/_api/usage.mdx @@ -5,17 +5,25 @@ title: Usage toc_max_heading_level: 3 --- -import Tabs from '@theme/Tabs' -import TabItem from '@theme/TabItem' +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + + + ## Organizations {#organizations} Organization endpoints + + ### List all organizations {#list-all-organizations} @@ -26,8 +34,15 @@ GET https://api.supabase.com/v1/organizations + + + + + + + #### Responses @@ -36,8 +51,11 @@ GET https://api.supabase.com/v1/organizations + + + ```json { "schema": { @@ -52,12 +70,16 @@ GET https://api.supabase.com/v1/organizations "type": "string" } }, - "required": ["id", "name"] + "required": [ + "id", + "name" + ] } } } ``` + @@ -68,6 +90,8 @@ Unexpected error listing organizations + + @@ -76,6 +100,7 @@ Unexpected error listing organizations
+ ### Create an organization {#create-an-organization} @@ -86,8 +111,14 @@ POST https://api.supabase.com/v1/organizations + + + + + + #### Body Parameters ```json @@ -99,11 +130,14 @@ POST https://api.supabase.com/v1/organizations "type": "string" } }, - "required": ["name"] + "required": [ + "name" + ] } } ``` + #### Responses @@ -112,8 +146,11 @@ POST https://api.supabase.com/v1/organizations + + + ```json { "schema": { @@ -126,11 +163,15 @@ POST https://api.supabase.com/v1/organizations "type": "string" } }, - "required": ["id", "name"] + "required": [ + "id", + "name" + ] } } ``` + @@ -141,6 +182,8 @@ Unexpected error creating an organization + + @@ -149,10 +192,14 @@ Unexpected error creating an organization
+ + ## Projects {#projects} Project endpoints + + ### List all projects {#list-all-projects} @@ -163,8 +210,15 @@ GET https://api.supabase.com/v1/projects + + + + + + + #### Responses @@ -173,8 +227,11 @@ GET https://api.supabase.com/v1/projects + + + ```json { "schema": { @@ -198,12 +255,19 @@ GET https://api.supabase.com/v1/projects "type": "string" } }, - "required": ["id", "organization_id", "name", "region", "created_at"] + "required": [ + "id", + "organization_id", + "name", + "region", + "created_at" + ] } } } ``` + @@ -212,6 +276,7 @@ GET https://api.supabase.com/v1/projects
+ ### Create a project {#create-a-project} @@ -222,8 +287,14 @@ POST https://api.supabase.com/v1/projects + + + + + + #### Body Parameters ```json @@ -242,7 +313,10 @@ POST https://api.supabase.com/v1/projects }, "plan": { "type": "string", - "enum": ["free", "pro"] + "enum": [ + "free", + "pro" + ] }, "region": { "type": "string", @@ -265,11 +339,18 @@ POST https://api.supabase.com/v1/projects "type": "boolean" } }, - "required": ["db_pass", "name", "organization_id", "plan", "region"] + "required": [ + "db_pass", + "name", + "organization_id", + "plan", + "region" + ] } } ``` + #### Responses @@ -278,8 +359,11 @@ POST https://api.supabase.com/v1/projects + + + ```json { "schema": { @@ -301,11 +385,18 @@ POST https://api.supabase.com/v1/projects "type": "string" } }, - "required": ["id", "organization_id", "name", "region", "created_at"] + "required": [ + "id", + "organization_id", + "name", + "region", + "created_at" + ] } } ``` + @@ -314,6 +405,7 @@ POST https://api.supabase.com/v1/projects
+ ### List all functions {#list-all-functions} @@ -324,24 +416,39 @@ GET https://api.supabase.com/v1/projects/{ref}/functions + #### Path Parameters + + + + + + #### Responses @@ -350,8 +457,11 @@ GET https://api.supabase.com/v1/projects/{ref}/functions + + + ```json { "schema": { @@ -369,7 +479,11 @@ GET https://api.supabase.com/v1/projects/{ref}/functions "type": "string" }, "status": { - "enum": ["ACTIVE", "REMOVED", "THROTTLED"], + "enum": [ + "ACTIVE", + "REMOVED", + "THROTTLED" + ], "type": "string" }, "version": { @@ -399,14 +513,19 @@ GET https://api.supabase.com/v1/projects/{ref}/functions } ``` + + + + + @@ -417,6 +536,8 @@ Failed to retrieve project's functions + + @@ -425,6 +546,7 @@ Failed to retrieve project's functions
+ ### Create a function {#create-a-function} @@ -435,24 +557,38 @@ POST https://api.supabase.com/v1/projects/{ref}/functions + #### Path Parameters + + + + + #### Body Parameters ```json @@ -474,11 +610,16 @@ POST https://api.supabase.com/v1/projects/{ref}/functions "type": "boolean" } }, - "required": ["slug", "name", "body"] + "required": [ + "slug", + "name", + "body" + ] } } ``` + #### Responses @@ -487,8 +628,11 @@ POST https://api.supabase.com/v1/projects/{ref}/functions + + + ```json { "schema": { @@ -504,7 +648,11 @@ POST https://api.supabase.com/v1/projects/{ref}/functions "type": "string" }, "status": { - "enum": ["ACTIVE", "REMOVED", "THROTTLED"], + "enum": [ + "ACTIVE", + "REMOVED", + "THROTTLED" + ], "type": "string" }, "version": { @@ -533,14 +681,19 @@ POST https://api.supabase.com/v1/projects/{ref}/functions } ``` + + + + + @@ -551,6 +704,8 @@ Failed to create project's function + + @@ -559,6 +714,7 @@ Failed to create project's function
+ ### Retrieve a function {#retrieve-a-function} @@ -569,34 +725,57 @@ GET https://api.supabase.com/v1/projects/{ref}/functions/{function_slug} + #### Path Parameters + + + + + + #### Responses @@ -605,8 +784,11 @@ GET https://api.supabase.com/v1/projects/{ref}/functions/{function_slug} + + + ```json { "schema": { @@ -622,7 +804,11 @@ GET https://api.supabase.com/v1/projects/{ref}/functions/{function_slug} "type": "string" }, "status": { - "enum": ["ACTIVE", "REMOVED", "THROTTLED"], + "enum": [ + "ACTIVE", + "REMOVED", + "THROTTLED" + ], "type": "string" }, "version": { @@ -654,14 +840,19 @@ GET https://api.supabase.com/v1/projects/{ref}/functions/{function_slug} } ``` + + + + + @@ -672,6 +863,8 @@ Failed to retrieve function with given slug + + @@ -680,6 +873,7 @@ Failed to retrieve function with given slug
+ ### Update a function {#update-a-function} @@ -690,34 +884,56 @@ PATCH https://api.supabase.com/v1/projects/{ref}/functions/{function_slug} + #### Path Parameters + + + + + #### Body Parameters ```json @@ -739,6 +955,7 @@ PATCH https://api.supabase.com/v1/projects/{ref}/functions/{function_slug} } ``` + #### Responses @@ -747,8 +964,11 @@ PATCH https://api.supabase.com/v1/projects/{ref}/functions/{function_slug} + + + ```json { "schema": { @@ -764,7 +984,11 @@ PATCH https://api.supabase.com/v1/projects/{ref}/functions/{function_slug} "type": "string" }, "status": { - "enum": ["ACTIVE", "REMOVED", "THROTTLED"], + "enum": [ + "ACTIVE", + "REMOVED", + "THROTTLED" + ], "type": "string" }, "version": { @@ -793,14 +1017,19 @@ PATCH https://api.supabase.com/v1/projects/{ref}/functions/{function_slug} } ``` + + + + + @@ -811,6 +1040,8 @@ Failed to update function with given slug + + @@ -819,6 +1050,7 @@ Failed to update function with given slug
+ ### Delete a function {#delete-a-function} @@ -829,34 +1061,57 @@ DELETE https://api.supabase.com/v1/projects/{ref}/functions/{function_slug} + #### Path Parameters + + + + + + #### Responses @@ -865,16 +1120,24 @@ DELETE https://api.supabase.com/v1/projects/{ref}/functions/{function_slug} + + + + + + + + @@ -885,6 +1148,8 @@ Failed to delete function with given slug + + @@ -893,6 +1158,7 @@ Failed to delete function with given slug
+ ### List all secrets {#list-all-secrets} @@ -903,24 +1169,39 @@ GET https://api.supabase.com/v1/projects/{ref}/secrets + #### Path Parameters + + + + + + #### Responses @@ -929,8 +1210,11 @@ GET https://api.supabase.com/v1/projects/{ref}/secrets + + + ```json { "schema": { @@ -945,20 +1229,28 @@ GET https://api.supabase.com/v1/projects/{ref}/secrets "type": "string" } }, - "required": ["name", "value"] + "required": [ + "name", + "value" + ] } } } ``` + + + + + @@ -969,6 +1261,8 @@ Failed to retrieve project's secrets + + @@ -977,6 +1271,7 @@ Failed to retrieve project's secrets
+ ### Bulk create secrets {#bulk-create-secrets} @@ -987,24 +1282,38 @@ POST https://api.supabase.com/v1/projects/{ref}/secrets + #### Path Parameters + + + + + #### Body Parameters ```json @@ -1022,12 +1331,16 @@ POST https://api.supabase.com/v1/projects/{ref}/secrets "pattern": "/^(?!SUPABASE_).*/" } }, - "required": ["name", "value"] + "required": [ + "name", + "value" + ] } } } ``` + #### Responses @@ -1036,16 +1349,24 @@ POST https://api.supabase.com/v1/projects/{ref}/secrets + + + + + + + + @@ -1056,6 +1377,8 @@ Failed to create project's secrets + + @@ -1064,6 +1387,7 @@ Failed to create project's secrets
+ ### Bulk delete secrets {#bulk-delete-secrets} @@ -1074,24 +1398,38 @@ DELETE https://api.supabase.com/v1/projects/{ref}/secrets + #### Path Parameters + + + + + #### Body Parameters ```json @@ -1105,6 +1443,7 @@ DELETE https://api.supabase.com/v1/projects/{ref}/secrets } ``` + #### Responses @@ -1113,8 +1452,11 @@ DELETE https://api.supabase.com/v1/projects/{ref}/secrets + + + ```json { "schema": { @@ -1123,14 +1465,19 @@ DELETE https://api.supabase.com/v1/projects/{ref}/secrets } ``` + + + + + @@ -1141,6 +1488,8 @@ Failed to delete secrets with given names + + @@ -1148,3 +1497,4 @@ Failed to delete secrets with given names
+ diff --git a/apps/reference/_auth_helpers/intro.md b/apps/reference/_auth_helpers/intro.md deleted file mode 100644 index 51239a87b48..00000000000 --- a/apps/reference/_auth_helpers/intro.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -slug: / -sidebar_label: Auth Helpers ---- - -# Auth Helpers - -A collection of framework specific Auth utilities for working with Supabase. - -## Status - -The Auth Helpers are in `beta`. They are usable in their current state, but it's likely that there will be breaking changes. - -## Additional Links - -- [Source code](https://github.com/supabase/auth-helpers) -- [Known bugs and issues](https://github.com/supabase/auth-helpers/issues) diff --git a/apps/reference/_auth_helpers/next-js.md b/apps/reference/_auth_helpers/next-js.md deleted file mode 100644 index e5306de5ccd..00000000000 --- a/apps/reference/_auth_helpers/next-js.md +++ /dev/null @@ -1,321 +0,0 @@ ---- -id: next-js -slug: next-js -sidebar_label: With Next.js ---- - -# Supabase Auth with Next.js - -This submodule provides convenience helpers for implementing user authentication in Next.js applications. - -## Installation - -Using [npm](https://npmjs.org): - -```sh -npm install @supabase/auth-helpers-nextjs - -# Main components and hooks for React based frameworks (optional) -npm install @supabase/auth-helpers-react -``` - -Using [yarn](https://yarnpkg.com/): - -```sh -yarn add @supabase/auth-helpers-nextjs - -# Main components and hooks for React based frameworks (optional) -yarn add @supabase/auth-helpers-react -``` - -This library supports the following tooling versions: - -- Node.js: `^10.13.0 || >=12.0.0` - -- Next.js: `>=10` - -## Getting Started - -### Configuration - -Set up the following env vars. 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 -# Find these in your Supabase project settings > API -NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key -``` - -### Basic Setup - -- Create an `auth` directory under the `/pages/api/` directory. - -- Create a `[...supabase].js` file under the newly created `auth` directory. - -The path to your dynamic API route file would be `/pages/api/auth/[...supabase].js`. Populate that file as follows: - -```js -import { handleAuth } from '@supabase/auth-helpers-nextjs' - -export default handleAuth({ logout: { returnTo: '/' } }) -``` - -Executing `handleAuth()` creates the following route handlers under the hood that perform different parts of the authentication flow: - -- `/api/auth/callback`: The `UserProvider` forwards the session details here every time `onAuthStateChange` fires on the client side. This is needed to set up the cookies for your application so that SSR works seamlessly. - -- `/api/auth/user`: You can fetch user profile information in JSON format. - -- `/api/auth/logout`: Your Next.js application logs out the user. You can optionally pass a `returnTo` parameter to return to a custom relative URL after logout, eg `/api/auth/logout?returnTo=/login`. This will overwrite the logout `returnTo` option specified `handleAuth()` - -Wrap your `pages/_app.js` component with the `UserProvider` component: - -```jsx -// pages/_app.js -import React from 'react' -import { UserProvider } from '@supabase/auth-helpers-react' -import { supabaseClient } from '@supabase/auth-helpers-nextjs' - -export default function App({ Component, pageProps }) { - return ( - - - - ) -} -``` - -You can now determine if a user is authenticated by checking that the `user` object returned by the `useUser()` hook is defined. - -## 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 import the `{ supabaseClient }` from `# @supabase/auth-helpers-nextjs` and only run your query once the user is defined client-side in the `useUser()` hook: - -```js -import { Auth } from '@supabase/ui' -import { useUser } from '@supabase/auth-helpers-react' -import { supabaseClient } from '@supabase/auth-helpers-nextjs' -import { useEffect, useState } from 'react' - -const LoginPage = () => { - const { user, error } = useUser() - const [data, setData] = useState() - - useEffect(() => { - async function loadData() { - const { data } = await supabaseClient.from('test').select('*') - setData(data) - } - // Only run query once user is logged in. - if (user) loadData() - }, [user]) - - if (!user) - return ( - <> - {error &&

{error.message}

} - - - ) - - return ( - <> - -

user:

-
{JSON.stringify(user, null, 2)}
-

client-side data fetching with RLS

-
{JSON.stringify(data, null, 2)}
- - ) -} - -export default LoginPage -``` - -### Server-side rendering (SSR) - withPageAuth - -If you wrap your `getServerSideProps` with `withPageAuth` your props object will be augmented with the user object. - -```js -// pages/profile.js -import { withPageAuth } from '@supabase/auth-helpers-nextjs' - -export default function Profile({ user }) { - return
Hello {user.name}
-} - -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: - -```js -// pages/protected-page.js -import { withPageAuth, getUser } from '@supabase/auth-helpers-nextjs' - -export default function ProtectedPage({ user, customProp }) { - return
Protected content
-} - -export const getServerSideProps = withPageAuth({ - redirectTo: '/foo', - async getServerSideProps(ctx) { - // Access the user object - const { user, accessToken } = await getUser(ctx) - return { props: { email: user?.email } } - }, -}) -``` - -### Server-side data fetching with RLS - -For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work in a server environment, you need to inject the request context into the supabase client: - -```js -import { - User, - withPageAuth, - supabaseServerClient, -} from '@supabase/auth-helpers-nextjs' - -export default function ProtectedPage({ - user, - data, -}: { - user: User, - data: any, -}) { - return ( - <> -
Protected content for {user.email}
-
{JSON.stringify(data, null, 2)}
-
{JSON.stringify(user, null, 2)}
- - ) -} - -export const getServerSideProps = withPageAuth({ - redirectTo: '/', - async getServerSideProps(ctx) { - // Run queries with RLS on the server - const { data } = await supabaseServerClient(ctx).from('test').select('*') - return { props: { data } } - }, -}) -``` - -### 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 { - /* ... */ -} - -export default function ProtectedPage({ - user, - data, -}: { - user: User, - profile: Profile, -}) { - return
Protected content
-} - -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}`, - }, - }) - ).json() - return { props: { profile } } - }, -}) -``` - -## Protecting API routes - -Wrap an API Route to check that the user has a valid session. If they're not logged in the handler will return a -401 Unauthorized. - -```js -// pages/api/protected-route.js -import { - withApiAuth, - supabaseServerClient, -} from '@supabase/auth-helpers-nextjs' - -export default withApiAuth(async function ProtectedRoute(req, res) { - // Run queries with RLS on the server - const { data } = await supabaseServerClient({ req, res }) - .from('test') - .select('*') - res.json(data) -}) -``` - -If you visit `/api/protected-route` without a valid session cookie, you will get a 401 response. - -## Protecting routes with [Nextjs Middleware](https://nextjs.org/docs/middleware) - -As an alternative to protecting individual pages using `getServerSideProps` with `withPageAuth`, `withMiddlewareAuth` can be used from inside a `middleware` file to protect the entire directory or those that match the config object. In the following example, all requests to `/middleware-protected/*` will check whether a user is signed in, if successful the request will be forwarded to the destination route, otherwise the user will be redirected to `/login` (defaults to: `/`) with a 307 Temporary Redirect response status: - -```ts title="/middleware.ts" -import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs' - -export const middleware = withMiddlewareAuth({ redirectTo: '/' }) - -export const config = { - matcher: ['/middleware-protected/:path*'], -} -``` - -It is also possible to add finer granularity based on the user logged in. I.e. you can specify a promise to determine if a specific user has permission or not. - -```ts title="/middleware.ts" -import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs/middleware' - -export const middleware = withMiddlewareAuth({ - redirectTo: '/login', - authGuard: { - isPermitted: async (user) => user.email?.endsWith('@example.com') ?? false, - redirectTo: '/insufficient-permissions', - }, -}) - -export const config = { - matcher: ['/middleware-protected/:path*'], -} -``` - -## Migrating from @supabase/supabase-auth-helpers to @supabase/auth-helpers - -This is a step by step guide on migrating away from the `@supabase/supabase-auth-helpers` to the newly released `@supabase/auth-helpers`. - -1. Install `@supabase/supabase-js`, `@supabase/auth-helpers-nextjs` and `@supabase/auth-helpers-react` libraries from npm. -2. Replace all imports of `@supabase/supabase-auth-helpers/nextjs` in your project with `@supabase/auth-helpers-nextjs`. -3. Replace all imports of `@supabase/supabase-auth-helpers/react` in your project with `@supabase/auth-helpers-react`. -4. Replace all instances of `withAuthRequired` in any of your NextJS pages with `withPageAuth`. -5. Replace all instances of `withAuthRequired` in any of your NextJS API endpoints with `withApiAuth`. -6. Uninstall `@supabase/supabase-auth-helpers`. diff --git a/apps/reference/_auth_helpers/svelte-kit.md b/apps/reference/_auth_helpers/svelte-kit.md deleted file mode 100644 index 46980643cb9..00000000000 --- a/apps/reference/_auth_helpers/svelte-kit.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -id: sveltekit -slug: sveltekit -sidebar_label: With SvelteKit ---- - -# Supabase Auth with SvelteKit - -This submodule provides convenience helpers for implementing user authentication in [SvelteKit](https://kit.svelte.dev/) applications. - -## Installation - -Using [npm](https://npmjs.org): - -```sh -npm install @supabase/auth-helpers-sveltekit - -# Main component for Svelte based frameworks (optional but recommended) -npm install @supabase/auth-helpers-svelte -``` - -Using [yarn](https://yarnpkg.com/): - -```sh -yarn add @supabase/auth-helpers-sveltekit - -# Main component for Svelte based frameworks (optional but recommended) -yarn add @supabase/auth-helpers-svelte -``` - -This library supports the following tooling versions: - -- Node.js: `^16.15.0` - -## Getting Started - -### Configuration - -Set up the fillowing env vars. For local development you can set them in a `.env` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/sveltekit/.env.example). - -```bash -# Find these in your Supabase project settings > API -VITE_SUPABASE_URL=https://your-project.supabase.co -VITE_SUPABASE_ANON_KEY=your-anon-key -``` - -### SupabaseClient and SupaAuthHelper component setup - -We will start off by creating a `db.ts` file inside of our `src/lib` directory. Now lets instantiate our `supabaseClient` by using our `createSupabaseClient` function from the `@supabase/auth-helpers-sveltekit` library. - -```ts -// src/lib/db.ts -import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit' - -const { supabaseClient } = createSupabaseClient( - import.meta.env.VITE_SUPABASE_URL as string, - import.meta.env.VITE_SUPABASE_ANON_KEY as string -) - -export { supabaseClient } -``` - -Edit your `__layout.svelte` file and add import the `SupaAuthHelper` component, the `supabaseClient` we just instantiated and the `session` store. - -```html -// src/routes/__layout.svelte - - - - - -``` - -### Hooks setup - -Our `hooks.ts` file is where the heavy lifting of this library happens, we need to import our function to handle the sign in, signing out and cookie creation phase. we can import all the hooks using `handleAuth` function and destructure its returned data. - -```ts -// src/hooks.ts -import { handleAuth } from '@supabase/auth-helpers-sveltekit' -import type { GetSession, Handle } from '@sveltejs/kit' -import { sequence } from '@sveltejs/kit/hooks' - -export const handle: Handle = sequence(...handleAuth()) - -export const getSession: GetSession = async (event) => { - const { user, accessToken, error } = event.locals - return { - user, - accessToken, - error, - } -} -``` - -These will create the handlers under the hood that perform different parts of the authentication flow: - -- `/api/auth/callback`: The `UserHelper` forwards the session details here every time `onAuthStateChange` fires on the client side. This is needed to set up the cookies for your application so that SSR works seamlessly. -- `/api/auth/user`: You can fetch user profile information in JSON format. -- `/api/auth/logout`: You can logout the user. - -### Typings - -In order to get the most out of TypeScript and its intellisense, you should import our types into the `app.d.ts` type definition file that comes with your SvelteKit project. - -```ts -// src/app.d.ts -/// -// See https://kit.svelte.dev/docs/types#app -// for information about these interfaces -declare namespace App { - interface UserSession { - user: import('@supabase/supabase-js').User - accessToken?: string - } - interface Locals extends UserSession { - error: import('@supabase/supabase-js').ApiError - } - - interface Session extends UserSession {} // interface Platform {} // interface Stuff {} -} -``` - -### Signing out - -This library has provided a dedicated endpoint for you to use to sign a user out. This endpoint will sign the user out of the Gotrue server, clear the cookies that were set when the user logged in and redirect the user to a configurable path. - -The logout handler endpoint is `/api/auth/logout`, this will take a `GET` request which means it can be used as the href for a normal `a` tag in your html. - -```html -Sign out -``` - -### Logout handler configuration - -In your `src/hooks.ts` file the logout handler is already setup and you can configure the redirect path from here. - -> By default the redirect path after logging out will be `/`. - -```ts -export const handle = sequence( - ...handleAuth({ - logout: { returnTo: '/auth/signin' }, - }) -) -``` - -### Basic Setup - -You can now determine if a user is authenticated on the client-side by checking that the `user` object returned by the `$session` store is defined. - -```html -// example - - -{#if !$session.user} -

I am not logged in

-{:else} -

Welcome {$session.user.email}

-

I am logged in!

-{/if} -``` - -## 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 import the `{ supabaseClient }` from `@supabase/auth-helpers-sveltekit` and only run your query once the user is defined client-side in the `$session`: - -```html - - -{#if !$session.user} - {#if $error} -

{$error.message}

- {/if} -

{$isLoading ? `Loading...` : `Loaded!`}

- -{:else} - Sign out -

user:

-
{JSON.stringify($session.user, null, 2)}
-

client-side data fetching with RLS

-
{JSON.stringify(loadedData, null, 2)}
-{/if} -``` - -### Server-side data fetching with RLS - -For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work in a server environment, you need to inject the request context into the supabase client: - -```html - - - -
Protected content for {user.email}
-
{JSON.stringify(data, null, 2)}
-
{JSON.stringify(user, null, 2)}
-``` - -```ts -// src/routes/profile.ts -import { - supabaseServerClient, - withApiAuth, -} from '@supabase/auth-helpers-sveltekit' -import type { RequestHandler } from './__types/profile' - -interface TestTable { - id: string - created_at: string -} - -interface GetOutput { - user: User - data: TestTable[] -} - -export const GET: RequestHandler = async ({ locals }) => - withApiAuth( - { - redirectTo: '/', - user: locals.user, - }, - async () => { - const { data } = await supabaseServerClient(session.accessToken) - .from('test') - .select('*') - - return { - body: { - user: locals.user, - data, - }, - } - } - ) -``` - -## Protecting API routes - -Wrap an API Route to check that the user has a valid session. If they're not logged in the handler will return a -303 and redirect header. - -```ts -// src/routes/api/protected-route.ts -import { - supabaseServerClient, - withApiAuth, -} from '@supabase/auth-helpers-sveltekit' -import type { RequestHandler } from './__types/protected-route' - -interface TestTable { - id: string - created_at: string -} - -interface GetOutput { - data: TestTable[] -} - -export const GET: RequestHandler = async ({ locals, request }) => - withApiAuth({ user: locals.user }, async () => { - // Run queries with RLS on the server - const { data } = await supabaseServerClient(request) - .from('test') - .select('*') - - return { - status: 200, - body: { data }, - } - }) -``` - -If you visit `/api/protected-route` without a valid session cookie, you will get a 303 response. diff --git a/apps/reference/_gotrue/config.mdx b/apps/reference/_gotrue/config.mdx index 391c9378400..9a00a5a380e 100644 --- a/apps/reference/_gotrue/config.mdx +++ b/apps/reference/_gotrue/config.mdx @@ -11,21 +11,25 @@ A `config.toml` file is generated after running `supabase init`. This file is located in the `supabase` folder under `supabase/config.toml`. + + + ## General {#general} + + ### `project_id` {#project_id} A string used to distinguish different Supabase projects on the same host. Defaults to the working directory name when running `supabase init`. +
    -
  • - Required: true -
  • -
  • - Default: None -
  • +
  • Required: true
  • +
  • Default: None

+ + diff --git a/apps/reference/_gotrue/usage.mdx b/apps/reference/_gotrue/usage.mdx index 3a4a64e2d2f..64eed5538f0 100644 --- a/apps/reference/_gotrue/usage.mdx +++ b/apps/reference/_gotrue/usage.mdx @@ -5,11 +5,14 @@ title: Usage toc_max_heading_level: 3 --- -import Tabs from '@theme/Tabs' -import TabItem from '@theme/TabItem' +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + Documentation of the gotrue API. + + diff --git a/apps/reference/_supabase_js/intro.md b/apps/reference/_supabase_js/intro.md index 3d1394ddd95..bec6b4f975e 100644 --- a/apps/reference/_supabase_js/intro.md +++ b/apps/reference/_supabase_js/intro.md @@ -7,14 +7,6 @@ hide_table_of_contents: true # Supabase JavaScript Library -:::note - -You're viewing the Supabase docs for the next version of our library which is not yet released. - -Refer to the `v1` docs for a stable release. - -::: - This reference documents every object and method available in Supabase's isomorphic JavaScript library, `supabase-js`. You can use the `supabase-js` library to: @@ -28,6 +20,6 @@ You can use the `supabase-js` library to: ## Additional Links - Source Code: [github.com/supabase/supabase-js](https://github.com/supabase/supabase-js) -- TypeDoc: [supabase.github.io/supabase-js](https://supabase.github.io/supabase-js) +- TypeDoc: [supabase.github.io/supabase-js](https://supabase.github.io/supabase-js/v2/) - NPM: [npmjs.com/package/@supabase/supabase-js](https://www.npmjs.com/package/@supabase/supabase-js) - [Known bugs and issues](https://github.com/supabase/supabase-js/issues) diff --git a/apps/reference/_supabase_js/release-notes.md b/apps/reference/_supabase_js/release-notes.md index 9a242632754..57af05f0980 100644 --- a/apps/reference/_supabase_js/release-notes.md +++ b/apps/reference/_supabase_js/release-notes.md @@ -6,9 +6,9 @@ id: release-notes Supabase.js v2 release notes. -## 2.0.0 Release Candidate +## 2.0.0 -Install the latest with `npm install @supabase/supabase-js@rc`. +Install the latest with `npm install @supabase/supabase-js`. ### Explicit constructor options diff --git a/apps/reference/_supabase_js_versioned_docs/version-v1/intro.md b/apps/reference/_supabase_js_versioned_docs/version-v1/intro.md index c076ac88b67..97230e84aea 100644 --- a/apps/reference/_supabase_js_versioned_docs/version-v1/intro.md +++ b/apps/reference/_supabase_js_versioned_docs/version-v1/intro.md @@ -5,6 +5,10 @@ slug: / sidebar_label: Supabase JavaScript Library --- +:::caution +You're viewing the docs for an older version of the `supabase-js` library. +::: + This reference documents every object and method available in Supabase's isomorphic JavaScript library, `supabase-js`. You can use the `supabase-js` library to: diff --git a/apps/reference/docs/architecture.mdx b/apps/reference/docs/architecture.mdx index 33ac9feb6c1..f969f70fa33 100755 --- a/apps/reference/docs/architecture.mdx +++ b/apps/reference/docs/architecture.mdx @@ -35,5 +35,5 @@ Most notably, we use Postgres rather than a NoSQL store. This choice was deliber | Auth | SvelteKit Auth Helpers | Alpha | [Link](/docs/guides/auth/auth-helpers/sveltekit) | | Public API | | Beta | [Link](/docs/reference/api) | | CLI | | Beta | [Link](/docs/guides/cli) | -| Client Library: JavaScript | | GA | [Link](/docs/reference/javascript/next/) | +| Client Library: JavaScript | | GA | [Link](/docs/reference/javascript) | | Client Library: Dart | | Beta | [Link](/docs/reference/dart) | diff --git a/apps/reference/docs/guides/auth.mdx b/apps/reference/docs/guides/auth.mdx index 1505754d7ef..f292dccfa2b 100644 --- a/apps/reference/docs/guides/auth.mdx +++ b/apps/reference/docs/guides/auth.mdx @@ -177,7 +177,14 @@ Supabase provides a special function in Postgres, `auth.uid()`, which extracts t ## User Management -Supabase makes it simple to manage your users. +Supabase provides multiple endpoints to authenticate and manage your users: +- [Sign up](/docs/reference/javascript/auth-signup) +- [Sign in with password](/docs/reference/javascript/auth-signinwithpassword) +- [Sign in with passwordless / one-time password (OTP)](/docs/reference/javascript/auth-signinwithotp) +- [Sign in with OAuth](/docs/reference/javascript/auth-signinwithoauth) +- [Sign out](/docs/reference/javascript/auth-signout) + +When users sign up, Supabase assigns them a unique ID. You can reference this ID anywhere in your database. For example, you might create a `profiles` table referencing `id` in the `auth.users` table using a `user_id` field. - -When users sign up, Supabase assigns them a unique ID. You can reference this ID anywhere in your database. For example, you might create a `profiles` table referencing `id` in the `auth.users` table using a `user_id` field. - -Supabase provides the routes to [sign up](/docs/reference/javascript/auth-signup), [log in](/docs/reference/javascript/auth-signin), -[log out](/docs/reference/javascript/auth-signout), and manage users in your apps and websites. - -## Next Steps - -- Sign in: [app.supabase.com](https://app.supabase.com) diff --git a/apps/reference/docs/guides/auth/auth-apple.mdx b/apps/reference/docs/guides/auth/auth-apple.mdx index 653ebcd527b..524212403b3 100644 --- a/apps/reference/docs/guides/auth/auth-apple.mdx +++ b/apps/reference/docs/guides/auth/auth-apple.mdx @@ -20,16 +20,14 @@ Apple OAuth consists of six broad steps: - Add your `client id` and `client secret` keys to your [Supabase Project](https://app.supabase.com). - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js). -## Steps - -### Access your Apple Developer account +## Access your Apple Developer account - Go to [developer.apple.com](https://developer.apple.com). - Click on `Account` at the top right to log in. ![Apple Developer Portal.](/img/guides/auth-apple/apple-developer-portal.png) -### Obtain an App ID +## Obtain an App ID - Go to `Certificates, Identifiers & Profiles`. - Click on `Identifiers` at the left. @@ -43,7 +41,7 @@ Apple OAuth consists of six broad steps: - Click `Continue` at the top right. - Click `Register` at the top right. -### Obtain a Services ID +## Obtain a Services ID This will serve as the `client_id` when you make API calls to authenticate the user. @@ -58,7 +56,7 @@ This will serve as the `client_id` when you make API calls to authenticate the u - Click `Continue` at the top right. - Click `Register` at the top right. -### Find your callback URL +## Find your callback URL The next step requires a callback URL, which looks like this: @@ -79,7 +77,7 @@ The next step requires a callback URL, which looks like this: /> -### Configure your Services ID +## Configure your Services ID - Under `Identifiers`, click on your newly-created Services ID. - Check the box next to `Sign In With Apple` to enable it. @@ -91,7 +89,7 @@ The next step requires a callback URL, which looks like this: - Click `Continue` at the top right. - Click `Save` at the top right. -### Download your secret key +## Download your secret key Now you'll need to download a `secret key` file from Apple that will be used to generate your `client_secret`. @@ -109,7 +107,7 @@ Now you'll need to download a `secret key` file from Apple that will be used to - Save the downloaded file -- this contains your "secret key" that will be used to generate your `client_secret`. - Click `Done` at the top right. -### Generate a `client_secret` +## Generate a `client_secret` The `secret key` you downloaded is used to create the `client_secret` string you'll need to authenticate your users. @@ -173,7 +171,7 @@ puts token 2. From the command line, run: `ruby secret_gen.rb > client_secret.txt`. 3. Your `client_secret` is now stored in this `client_secret.txt` file. -### Add your OAuth credentials to Supabase +## Add your OAuth credentials to Supabase - Go to your [Supabase Dashboard](https://app.supabase.com). - In the left sidebar, click the `Authentication` icon (near the top). @@ -183,27 +181,19 @@ puts token - Enter your `client_id` and `client_secret` saved in the previous steps. - Click `Save`. -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented in the [Supabase OAuth Reference](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers). - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'apple', -}) -``` - -Add a function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `apple` as the `provider`: ```js async function signInWithApple() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'apple', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-azure.mdx b/apps/reference/docs/guides/auth/auth-azure.mdx index 0f7984fdaa5..93d1a6c11c6 100644 --- a/apps/reference/docs/guides/auth/auth-azure.mdx +++ b/apps/reference/docs/guides/auth/auth-azure.mdx @@ -18,14 +18,12 @@ Azure OAuth consists of four broad steps: - Create a `Secret ID` with “Sign In with Azure” capabilities. The value of the secret will be used as the `client secret`. - Add the callback url of your application to the allowlist. -## Steps - -### Access your Azure Developer account +## Access your Azure Developer account - Go to [portal.azure.com](https://portal.azure.com/#home). - Login and select "Azure Active Directory" under the list of Azure Services. -### Register an application +## Register an application - Under Azure Active Directory, select "App registrations" in the side panel. - Select "New registration". @@ -36,7 +34,7 @@ Azure OAuth consists of four broad steps: ![Register an application.](/img/guides/auth-azure/azure-register-app.png) -### Obtain a Client ID +## Obtain a Client ID This will serve as the `client_id` when you make API calls to authenticate the user. @@ -44,7 +42,7 @@ This will serve as the `client_id` when you make API calls to authenticate the u ![Obtain the client id](/img/guides/auth-azure/azure-client-id.png) -### Obtain a Secret ID +## Obtain a Secret ID This will serve as the `client_secret` when you make API calls to authenticate the user. @@ -65,21 +63,13 @@ This will allow your users to use your custom Azure login page when logging in. ![Obtain the tenant url](/img/guides/auth-azure/azure-tenant-url.png) -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented in the [Supabase OAuth Reference](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers). - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'azure', -}) -``` - -Add a function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `azure` as the `provider`: ```js async function signInWithAzure() { - const { user, session, error } = await supabase.auth.signIn( + const { data, error } = await supabase.auth.signInWithOAuth( { provider: 'azure', }, @@ -90,7 +80,7 @@ async function signInWithAzure() { } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { @@ -100,5 +90,5 @@ async function signout() { ## Resources -- [Azure Developer Account](https://portal.azure.com). -- [GitHub Discussion](https://github.com/supabase/gotrue/pull/54#issuecomment-757043573). +- [Azure Developer Account](https://portal.azure.com) +- [GitHub Discussion](https://github.com/supabase/gotrue/pull/54#issuecomment-757043573) diff --git a/apps/reference/docs/guides/auth/auth-bitbucket.mdx b/apps/reference/docs/guides/auth/auth-bitbucket.mdx index 6b1f4e96a9a..d7e279cd3c3 100644 --- a/apps/reference/docs/guides/auth/auth-bitbucket.mdx +++ b/apps/reference/docs/guides/auth/auth-bitbucket.mdx @@ -17,16 +17,14 @@ Setting up Bitbucket logins for your application consists of 3 parts: - Add your Bitbucket OAuth Consumer keys to your [Supabase Project](https://app.supabase.com) - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js) -## Steps - -### Access your Bitbucket account +## Access your Bitbucket account - Go to [bitbucket.org](https://bitbucket.org/). - Click on `Login` at the top right to log in. ![Bitbucket Developer Portal.](/img/guides/auth-bitbucket/bitbucket-portal.png) -### Find your callback URL +## Find your callback URL The next step requires a callback URL, which looks like this: @@ -47,7 +45,7 @@ The next step requires a callback URL, which looks like this: /> -### Create a Bitbucket OAuth app +## Create a Bitbucket OAuth app - Click on your profile icon at the bottom left - Click on `All Workspaces` @@ -62,7 +60,7 @@ The next step requires a callback URL, which looks like this: - Click on your app name (the name of your new OAuth Consumer) - Copy your `Key` (`client_key`) and `Secret` (`client_secret`) codes -### Add your Bitbucket credentials into your Supabase Project +## Add your Bitbucket credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com) - In the left sidebar, click the `Authentication` icon (near the top) @@ -72,27 +70,19 @@ The next step requires a callback URL, which looks like this: - Enter your `client_id` and `client_secret` saved in the previous step - Click `Save` -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers) - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'bitbucket', -}) -``` - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `bitbucket` as the `provider`: ```js async function signInWithBitbucket() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'bitbucket', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-discord.mdx b/apps/reference/docs/guides/auth/auth-discord.mdx index 8591a232d16..11193ec9c8a 100644 --- a/apps/reference/docs/guides/auth/auth-discord.mdx +++ b/apps/reference/docs/guides/auth/auth-discord.mdx @@ -17,9 +17,7 @@ Setting up Discord logins for your application consists of 3 parts: - Add your Discord OAuth Consumer keys to your [Supabase Project](https://app.supabase.com) - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js) -## Steps - -### Access your Discord account +## Access your Discord account - Go to [discord.com](https://discord.com/). - Click on `Login` at the top right to log in. @@ -30,7 +28,7 @@ Setting up Discord logins for your application consists of 3 parts: ![Discord Portal.](/img/guides/auth-discord/discord-developer-portal.png) -### Find your callback URL +## Find your callback URL In the next step you require a callback URL, which looks like this: @@ -51,7 +49,7 @@ In the next step you require a callback URL, which looks like this: /> -### Create a Discord Application +## Create a Discord Application - Click on `New Application` at the top right. - Enter the name of your application and click `Create`. @@ -61,7 +59,7 @@ In the next step you require a callback URL, which looks like this: - Click `Save Changes` at the bottom. - Copy your `Client ID` and `Client Secret` under `Client information`. -### Add your Discord credentials into your Supabase Project +## Add your Discord credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com) - In the left sidebar, click the `Authentication` icon (near the top) @@ -71,33 +69,21 @@ In the next step you require a callback URL, which looks like this: - Enter your `client_id` and `client_secret` saved in the previous step - Click `Save` -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers) - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'discord', -}) -``` - -:::note - -If you call `signIn()` when already logged in, Discord will prompt the user again for authorization. - -::: - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `discord` as the `provider`: ```js async function signInWithDiscord() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'discord', }) } ``` -To log out: +If your user is already signed in, Discord prompts the user again for authorization. + +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-email.mdx b/apps/reference/docs/guides/auth/auth-email.mdx index d776dc8e6af..27c3f83874c 100644 --- a/apps/reference/docs/guides/auth/auth-email.mdx +++ b/apps/reference/docs/guides/auth/auth-email.mdx @@ -30,12 +30,7 @@ See the [self-hosting docs](/docs/guides/hosting/overview#configuration) for det ::: -### Add login code to your client app - -Add logins using our client libraries: - -- [JavaScript](/docs/reference/javascript/auth-signin#sign-in-with-email) -- [Dart](/docs/reference/dart/auth-signin#sign-in-with-email) +## Add login code to your client app -```js -const { user, error } = await supabase.auth.signIn({ - email: 'example@email.com', - password: 'example-password', -}) -``` - - - - - -```dart -final res = await supabase.auth.signIn( - email: 'example@email.com', - password: 'example-password' -); - -final user = res.data?.user; -final error = res.error; -``` - - - - - -Add this function which you can call from a button, link, or UI element. - - - - +When your user signs in, call [signInWithPassword()](/docs/reference/javascript/auth-signinwithpassword) with their email address and password: ```js async function signInWithEmail() { - const { user, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithPassword({ email: 'example@email.com', password: 'example-password', }) @@ -93,6 +55,8 @@ async function signInWithEmail() { +When your user signs in, call [signIn()](/docs/reference/dart/auth-signin) with their email address and password: + ```dart Future signInWithEmail() async { await supabase.auth.signIn( @@ -106,8 +70,6 @@ Future signInWithEmail() async { -To log out: - +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: + ```js async function signOut() { const { error } = await supabase.auth.signOut() @@ -126,6 +90,8 @@ async function signOut() { +When your user signs out, call [signOut()](/docs/reference/dart/auth-signout) to remove them from the browser session and any objects from localStorage: + ```dart Future signOut() async { await supabase.auth.signOut(); diff --git a/apps/reference/docs/guides/auth/auth-facebook.mdx b/apps/reference/docs/guides/auth/auth-facebook.mdx index c7a76ef1902..3cb6ac68947 100644 --- a/apps/reference/docs/guides/auth/auth-facebook.mdx +++ b/apps/reference/docs/guides/auth/auth-facebook.mdx @@ -17,16 +17,14 @@ Setting up Facebook logins for your application consists of 3 parts: - Add your Facebook keys to your [Supabase Project](https://app.supabase.com) - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js) -## Steps - -### Access your Facebook Developer account +## Access your Facebook Developer account - Go to [developers.facebook.com](https://developers.facebook.com). - Click on `Log In` at the top right to log in. ![Facebook Developer Portal.](/img/guides/auth-facebook/facebook-portal.png) -### Create a Facebook App +## Create a Facebook App - Click on `My Apps` at the top right. - Click `Create App` near the top right. @@ -34,7 +32,7 @@ Setting up Facebook logins for your application consists of 3 parts: - Fill in your app information, then click `Create App`. - This should bring you to the screen: `Add Products to Your App`. (Alternatively you can click on `Add Product` in the left sidebar to get to this screen.) -### Find your callback URI +## Find your callback URI The next step requires a callback URI, which looks like this: @@ -55,7 +53,7 @@ The next step requires a callback URI, which looks like this: /> -### Set up FaceBook Login for your Facebook App +## Set up FaceBook Login for your Facebook App From the `Add Products to your App` screen: @@ -72,14 +70,14 @@ From the `App Review -> Permissions and Features` screen: You can read more about access levels [here](https://developers.facebook.com/docs/graph-api/overview/access-levels/) -### Copy your Facebook App ID and Secret +## Copy your Facebook App ID and Secret - Click `Settings / Basic` in the left sidebar - Copy your App ID from the top of the `Basic Settings` page - Under `App Secret` click `Show` then copy your secret - Make sure all required fields are completed on this screen. -### Enter your Facebook App ID and Secret into your Supabase Project +## Enter your Facebook App ID and Secret into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com) - In the left sidebar, click the `Authentication` icon (near the top) @@ -89,27 +87,19 @@ You can read more about access levels [here](https://developers.facebook.com/doc - Enter your `Facebook client ID` and `Facebook secret` saved in the previous step - Click `Save` -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers) - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'facebook', -}) -``` - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `facebook` as the `provider`: ```js async function signInWithFacebook() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'facebook', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-github.mdx b/apps/reference/docs/guides/auth/auth-github.mdx index d171495bafb..471d37935b1 100644 --- a/apps/reference/docs/guides/auth/auth-github.mdx +++ b/apps/reference/docs/guides/auth/auth-github.mdx @@ -17,16 +17,14 @@ Setting up GitHub logins for your application consists of 3 parts: - Add your GitHub OAuth keys to your [Supabase Project](https://app.supabase.com) - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js) -## Steps - -### Access your GitHub account +## Access your GitHub account - Go to [github.com](https://github.com). - Click on `Sign In` at the top right to log in. ![GitHub Developer Portal.](/img/guides/auth-github/github-portal.png) -### Create a GitHub Oauth App +## Create a GitHub Oauth App Go to the [GitHub Developer Settings](https://github.com/settings/developers) page: @@ -35,7 +33,7 @@ Go to the [GitHub Developer Settings](https://github.com/settings/developers) pa - In the left sidebar, click `Developer settings` (near the bottom) - In the left sidebar, click `OAuth Apps` -### Find your callback URL +## Find your callback URL The next step requires a callback URL, which looks like this: @@ -56,7 +54,7 @@ The next step requires a callback URL, which looks like this: /> -### Register a new OAuth application +## Register a new OAuth application - Click `Register a new application`. If you've created an app before, click `New OAuth App` here. - In `Application name`, type the name of your app. @@ -72,7 +70,7 @@ Copy your new OAuth credentials - Click `Generate a new client secret`. - Copy and save your `Client secret`. -### Enter your GitHub credentials into your Supabase Project +## Enter your GitHub credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com) - In the left sidebar, click the `Authentication` icon (near the top) @@ -82,27 +80,19 @@ Copy your new OAuth credentials - Enter your `GitHub Client ID` and `GitHub Client Secret` saved in the previous step - Click `Save` -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers) +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `github` as the `provider`: ```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'github', -}) -``` - -Add this function which you can call from a button, link, or UI element. - -```js -async function signInWithGithub() { - const { user, session, error } = await supabase.auth.signIn({ +async function signInWithGitHub() { + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'github', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-gitlab.mdx b/apps/reference/docs/guides/auth/auth-gitlab.mdx index adf4287ff03..9f5bfd29360 100644 --- a/apps/reference/docs/guides/auth/auth-gitlab.mdx +++ b/apps/reference/docs/guides/auth/auth-gitlab.mdx @@ -17,16 +17,14 @@ Setting up GitLab logins for your application consists of 3 parts: - Add your GitLab Application keys to your [Supabase Project](https://app.supabase.com) - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js) -## Steps - -### Access your GitLab account +## Access your GitLab account - Go to [gitlab.com](https://gitlab.com). - Click on `Login` at the top right to log in. ![GitLab Developer Portal.](/img/guides/auth-gitlab/gitlab-portal.png) -### Find your callback URL +## Find your callback URL The next step requires a callback URL, which looks like this: @@ -47,7 +45,7 @@ The next step requires a callback URL, which looks like this: /> -### Create your GitLab Application +## Create your GitLab Application - Click on your `profile logo` (avatar) in the top-right corner. - Select `Edit profile`. @@ -59,7 +57,7 @@ The next step requires a callback URL, which looks like this: - Click `Save Application` at the bottom. - Copy and save your `Application ID` (`client_id`) and `Secret` (`client_secret`) which you'll need later. -### Add your GitLab credentials into your Supabase Project +## Add your GitLab credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com). - In the left sidebar, click the `Authentication` icon (near the top). @@ -69,27 +67,19 @@ The next step requires a callback URL, which looks like this: - Enter your `client_id` and `client_secret` saved in the previous step. - Click `Save`. -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers) - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'gitlab', -}) -``` - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `gitlab` as the `provider`: ```js async function signInWithGitLab() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'gitlab', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-google.mdx b/apps/reference/docs/guides/auth/auth-google.mdx index e3788c3f3aa..051ce6cab03 100644 --- a/apps/reference/docs/guides/auth/auth-google.mdx +++ b/apps/reference/docs/guides/auth/auth-google.mdx @@ -17,16 +17,14 @@ Setting up Google logins for your application consists of 3 parts: - Add your Google OAuth keys to your [Supabase Project](https://app.supabase.com) - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js) -## Steps - -### Access your Google Cloud Platform account +## Access your Google Cloud Platform account - Go to [cloud.google.com](https://cloud.google.com). - Click on `Sign in` at the top right to log in. ![Google Developer Portal.](/img/guides/auth-google/google-portal.png) -### Create a Google Cloud Platform Project +## Create a Google Cloud Platform Project - Click on `Select a Project` at the top left. - (Or, if a project is currently selected, click on the current project name at the top left.) @@ -35,7 +33,7 @@ Setting up Google logins for your application consists of 3 parts: - (This can take a few minutes.) - This should bring you to the dashboard for your new project. -### Create the OAuth Keys for your project +## Create the OAuth Keys for your project From your project's dashboard screen: @@ -44,12 +42,12 @@ From your project's dashboard screen: - On the `OAuth consent screen` page select `External`. - Click `Create`. -### Edit your app information +## Edit your app information - On the `Edit app registration` page fill out your app information. - Click `Save and continue` at the bottom. -### Find your callback URL +## Find your callback URL The next step requires a callback URL, which looks like this: @@ -70,7 +68,7 @@ The next step requires a callback URL, which looks like this: /> -### Create your Google credentials +## Create your Google credentials - Click `Credentials` at the left to go to the `Credentials` page on the Google Cloud Platform console. - Click `Create Credentials` near the top then select `OAuth client ID` @@ -87,7 +85,7 @@ Copy your new OAuth credentials - A box will appear called `OAuth client created`. - Copy and save the values under `Your Client ID` and `Your Client Secret`. -### Enter your Google credentials into your Supabase Project +## Enter your Google credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com) - In the left sidebar, click the `Authentication` icon (near the top) @@ -97,27 +95,19 @@ Copy your new OAuth credentials - Enter your `Google Client ID` and `Google Client Secret` saved in the previous step - Click `Save` -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers) - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'google', -}) -``` - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `google` as the `provider`: ```js async function signInWithGoogle() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'google', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-helpers/auth-ui.mdx b/apps/reference/docs/guides/auth/auth-helpers/auth-ui.mdx index f92a18f9322..0ee2493a61e 100644 --- a/apps/reference/docs/guides/auth/auth-helpers/auth-ui.mdx +++ b/apps/reference/docs/guides/auth/auth-helpers/auth-ui.mdx @@ -18,10 +18,10 @@ It supports custom themes and extensible styles to match your brand and aestheti ## Set up Auth UI -Install the latest version of [supabase-js](/docs/reference/javascript/next/) and the Auth UI package: +Install the latest version of [supabase-js](/docs/reference/javascript) and the Auth UI package: ```bash -npm install @supabase/supabase-js@rc @supabase/auth-ui-react +npm install @supabase/supabase-js @supabase/auth-ui-react ``` ### Import the Auth component diff --git a/apps/reference/docs/guides/auth/auth-helpers/nextjs.mdx b/apps/reference/docs/guides/auth/auth-helpers/nextjs.mdx index 724088ac743..f17a9fe926e 100644 --- a/apps/reference/docs/guides/auth/auth-helpers/nextjs.mdx +++ b/apps/reference/docs/guides/auth/auth-helpers/nextjs.mdx @@ -5,99 +5,136 @@ description: Authentication helpers for Next.js API routes, middleware, and SSR. sidebar_label: 'Next.js' --- +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + This submodule provides convenience helpers for implementing user authentication in Next.js applications. -## Installation +## Install the Next.js helper library -Using [npm](https://npmjs.org): + + + ```sh npm install @supabase/auth-helpers-nextjs - -# Main components and hooks for React based frameworks (optional) -npm install @supabase/auth-helpers-react -``` - -Using [yarn](https://yarnpkg.com/): - -```sh -yarn add @supabase/auth-helpers-nextjs - -# Main components and hooks for React based frameworks (optional) -yarn add @supabase/auth-helpers-react ``` This library supports the following tooling versions: - Node.js: `^10.13.0 || >=12.0.0` - - Next.js: `>=10` -## Getting Started +Optionally, install **React Auth Helpers** for components and hooks that can be used across all React-based frameworks. +```sh +npm install @supabase/auth-helpers-react +``` -### Configuration + + -Set up the following env vars. 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). +```sh +yarn add @supabase/auth-helpers-nextjs +``` + +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. + +```sh +yarn add @supabase/auth-helpers-react +``` + + + + +## Set up environment variables + +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 -# Find these in your Supabase project settings > API NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key ``` -### Basic Setup +## Basic Setup -- Create an `auth` directory under the `/pages/api/` directory. +Wrap your `pages/_app.js` component with the `SessionContextProvider` component: -- Create a `[...supabase].js` file under the newly created `auth` directory. - -The path to your dynamic API route file would be `/pages/api/auth/[...supabase].js`. Populate that file as follows: - -```js -import { handleAuth } from '@supabase/auth-helpers-nextjs' - -export default handleAuth({ logout: { returnTo: '/' } }) -``` - -Executing `handleAuth()` creates the following route handlers under the hood that perform different parts of the authentication flow: - -- `/api/auth/callback`: The `UserProvider` forwards the session details here every time `onAuthStateChange` fires on the client side. This is needed to set up the cookies for your application so that SSR works seamlessly. - -- `/api/auth/user`: You can fetch user profile information in JSON format. - -- `/api/auth/logout`: Your Next.js application logs out the user. You can optionally pass a `returnTo` parameter to return to a custom relative URL after logout, eg `/api/auth/logout?returnTo=/login`. This will overwrite the logout `returnTo` option specified `handleAuth()` - -Wrap your `pages/_app.js` component with the `UserProvider` component: - -```jsx -// pages/_app.js +```jsx title="pages/_app.js" import React from 'react' -import { UserProvider } from '@supabase/auth-helpers-react' -import { supabaseClient } from '@supabase/auth-helpers-nextjs' +import { useRouter } from 'next/router' +import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' +import { SessionContextProvider } from '@supabase/auth-helpers-react' + +function MyApp({ Component, pageProps }: AppProps) { + const router = useRouter() + const [supabaseClient] = useState(() => createBrowserSupabaseClient()) -export default function App({ Component, pageProps }) { return ( - + + + - + ) } ``` You can now determine if a user is authenticated by checking that the `user` object returned by the `useUser()` hook is defined. -## Client-side data fetching with RLS +## Usage with TypeScript -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 import the `{ supabaseClient }` from `# @supabase/auth-helpers-nextjs` and only run your query once the user is defined client-side in the `useUser()` hook: +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 -import { Auth } from '@supabase/ui' -import { useUser } from '@supabase/auth-helpers-react' -import { supabaseClient } from '@supabase/auth-helpers-nextjs' +// Creating a new supabase client object: +import { Database } from '../db_types'; + +const [supabaseClient] = useState(() => + createBrowserSupabaseClient() + ); +``` + +```js +// Retrieving a supabase client object from the SessionContext: +import { useSupabaseClient } from '@supabase/auth-helpers-react'; +import { Database } from '../db_types'; + +const supabaseClient = useSupabaseClient(); +``` + +## 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: + +```js +import { Auth, ThemeSupa } from '@supabase/auth-ui-react' +import { useUser, useSessionContext } from '@supabase/auth-helpers-react' import { useEffect, useState } from 'react' const LoginPage = () => { - const { user, error } = useUser() + const { isLoading, session, error, supabaseClient } = useSessionContext() + const user = useUser() const [data, setData] = useState() useEffect(() => { @@ -114,10 +151,11 @@ const LoginPage = () => { <> {error &&

{error.message}

} ) @@ -140,8 +178,7 @@ export default LoginPage If you wrap your `getServerSideProps` with `withPageAuth` your props object will be augmented with the user object. -```js -// pages/profile.js +```js title="pages/profile.js" import { withPageAuth } from '@supabase/auth-helpers-nextjs' export default function Profile({ user }) { @@ -156,9 +193,8 @@ If there is no authenticated user, they will be redirect to your home page, unle 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: -```js -// pages/protected-page.js -import { withPageAuth, getUser } from '@supabase/auth-helpers-nextjs' +```js title="pages/protected-page.js" +import { withPageAuth } from '@supabase/auth-helpers-nextjs' export default function ProtectedPage({ user, customProp }) { return
Protected content
@@ -166,9 +202,11 @@ export default function ProtectedPage({ user, customProp }) { export const getServerSideProps = withPageAuth({ redirectTo: '/foo', - async getServerSideProps(ctx) { + async getServerSideProps(ctx, supabase) { // Access the user object - const { user, accessToken } = await getUser(ctx) + const { + data: { user }, + } = await supabase.auth.getUser() return { props: { email: user?.email } } }, }) @@ -176,14 +214,10 @@ export const getServerSideProps = withPageAuth({ ### Server-side data fetching with RLS -For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work in a server environment, you need to inject the request context into the supabase client: +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: ```js -import { - User, - withPageAuth, - supabaseServerClient, -} from '@supabase/auth-helpers-nextjs' +import { User, withPageAuth } from '@supabase/auth-helpers-nextjs' export default function ProtectedPage({ user, @@ -203,7 +237,7 @@ export default function ProtectedPage({ export const getServerSideProps = withPageAuth({ redirectTo: '/', - async getServerSideProps(ctx) { + async getServerSideProps(ctx, supabaseServerClient) { // Run queries with RLS on the server const { data } = await supabaseServerClient(ctx).from('test').select('*') return { props: { data } } @@ -258,18 +292,16 @@ export const getServerSideProps = withPageAuth({ Wrap an API Route to check that the user has a valid session. If they're not logged in the handler will return a 401 Unauthorized. -```js -// pages/api/protected-route.js -import { - withApiAuth, - supabaseServerClient, -} from '@supabase/auth-helpers-nextjs' +```js title="pages/api/protected-route.js" +import { withApiAuth } from '@supabase/auth-helpers-nextjs' -export default withApiAuth(async function ProtectedRoute(req, res) { +export default withApiAuth(async function ProtectedRoute( + req, + res, + supabaseServerClient +) { // Run queries with RLS on the server - const { data } = await supabaseServerClient({ req, res }) - .from('test') - .select('*') + const { data } = await supabaseServerClient.from('test').select('*') res.json(data) }) ``` @@ -278,20 +310,22 @@ If you visit `/api/protected-route` without a valid session cookie, you will get ## Protecting routes with [Nextjs Middleware](https://nextjs.org/docs/middleware) -As an alternative to protecting individual pages using `getServerSideProps` with `withPageAuth`, `withMiddlewareAuth` can be used from inside a `_middleware` file to protect an entire directory. In the following example, all requests to `/protected/*` will check whether a user is signed in, if successful the request will be forwarded to the destination route, otherwise the user will be redirected to `/login` (defaults to: `/`) with a 307 Temporary Redirect response status: +As an alternative to protecting individual pages using `getServerSideProps` with `withPageAuth`, `withMiddlewareAuth` can be used from inside a `middleware` file to protect the entire directory or those that match the config object. In the following example, all requests to `/middleware-protected/*` will check whether a user is signed in, if successful the request will be forwarded to the destination route, otherwise the user will be redirected to `/login` (defaults to: `/`) with a 307 Temporary Redirect response status: -```ts -// pages/protected/_middleware.ts -import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs/middleware' +```ts title="middleware.ts" +import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs' export const middleware = withMiddlewareAuth({ redirectTo: '/login' }) + +export const config = { + matcher: ['/middleware-protected/:path*'], +} ``` It is also possible to add finer granularity based on the user logged in. I.e. you can specify a promise to determine if a specific user has permission or not. -```ts -// pages/protected/_middleware.ts -import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs/dist/middleware' +```ts title="middleware.ts" +import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs' export const middleware = withMiddlewareAuth({ redirectTo: '/login', @@ -300,20 +334,61 @@ export const middleware = withMiddlewareAuth({ redirectTo: '/insufficient-permissions', }, }) + +export const config = { + matcher: ['/middleware-protected/:path*'], +} ``` -## Migrating from @supabase/supabase-auth-helpers to @supabase/auth-helpers +## Migrating to version `0.4.X` {#migration} -This is a step by step guide on migrating away from the `@supabase/supabase-auth-helpers` to the newly released `@supabase/auth-helpers`. +- With `supabase-js` v2, the `auth` API routes are no longer required and you can delete the `auth` directory under the `/pages/api/` directory. +- The `/api/auth/logout` API route has been removed—use the `signout` method instead. + ```js + + ``` +- 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'; + const [supabaseClient] = useState(() => + createBrowserSupabaseClient() + ); -1. Install `@supabase/supabase-js`, `@supabase/auth-helpers-nextjs` and `@supabase/auth-helpers-react` libraries from npm. -2. Replace all imports of `@supabase/supabase-auth-helpers/nextjs` in your project with `@supabase/auth-helpers-nextjs`. -3. Replace all imports of `@supabase/supabase-auth-helpers/react` in your project with `@supabase/auth-helpers-react`. -4. Replace all instances of `withAuthRequired` in any of your NextJS pages with `withPageAuth`. -5. Replace all instances of `withAuthRequired` in any of your NextJS API endpoints with `withApiAuth`. -6. Uninstall `@supabase/supabase-auth-helpers`. + // server-side API route + import type { NextApiRequest, NextApiResponse } from 'next' + import type { Database } from 'types_db'; -## Additional Links + export default async (req: NextApiRequest, res: NextApiResponse) => { + const supabaseServerClient = createServerSupabaseClient({ req, res }) + const { data:{ user } } = await supabaseServerClient.auth.getUser() -- [Auth Helpers Source code](https://github.com/supabase/auth-helpers) -- [Next.js example](https://github.com/supabase/auth-helpers/tree/main/examples/nextjs) + 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'; + + const [supabaseClient] = useState(() => + createBrowserSupabaseClient() + ); + ``` + + ```js + // Retrieving a supabase client object from the SessionContext: + import { useSupabaseClient } from '@supabase/auth-helpers-react'; + import { Database } from '../db_types'; + + const supabaseClient = useSupabaseClient(); + ``` diff --git a/apps/reference/docs/guides/auth/auth-keycloak.mdx b/apps/reference/docs/guides/auth/auth-keycloak.mdx index 0282eb6cb75..76e536063c3 100644 --- a/apps/reference/docs/guides/auth/auth-keycloak.mdx +++ b/apps/reference/docs/guides/auth/auth-keycloak.mdx @@ -24,13 +24,11 @@ Keycloak OAuth consists of five broad steps: - Obtain the `Secret` from the credentials tab which will be used as the `client secret`. - Add the callback url of your application to your allowlist. -## Steps - -### Access your Keycloak Admin console +## Access your Keycloak Admin console - Login by visiting [`http://localhost:8080`](http://localhost:8080) and clicking on "Administration Console". -### Create a Keycloak Realm +## Create a Keycloak Realm - Once you've logged in to the Keycloak console, you can add a realm from the side panel. The default realm should be named "Master". - After you've added a new realm, you can retrieve the `issuer` from the "OpenID Endpoint Configuration" endpoint. The `issuer` will be used as the `Keycloak URL`. @@ -38,13 +36,13 @@ Keycloak OAuth consists of five broad steps: ![Add a Keycloak Realm.](/img/guides/auth-keycloak/keycloak-create-realm.png) -### Create a Keycloak Client +## Create a Keycloak Client The "Client ID" of the created client will serve as the `client_id` when you make API calls to authenticate the user. ![Add a Keycloak client](/img/guides/auth-keycloak/keycloak-add-client.png) -### Client Settings +## Client Settings After you've created the client successfully, ensure that you set the following settings: @@ -55,34 +53,26 @@ After you've created the client successfully, ensure that you set the following ![Obtain the client id, set the client protocol and access type](/img/guides/auth-keycloak/keycloak-client-id.png) ![Set redirect uri](/img/guides/auth-keycloak/keycloak-redirect-uri.png) -### Obtain the Client Secret +## Obtain the Client Secret This will serve as the `client_secret` when you make API calls to authenticate the user. Under the "Credentials" tab, the `Secret` value will be used as the `client secret`. ![Obtain the client secret](/img/guides/auth-keycloak/keycloak-client-secret.png) -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented in the [Supabase OAuth Reference](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers). - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'keycloak', -}) -``` - -Add a function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `keycloak` as the `provider`: ```js async function signInWithKeycloak() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'keycloak', - } + }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-linkedin.mdx b/apps/reference/docs/guides/auth/auth-linkedin.mdx index db857f6fed4..207d19c0439 100644 --- a/apps/reference/docs/guides/auth/auth-linkedin.mdx +++ b/apps/reference/docs/guides/auth/auth-linkedin.mdx @@ -17,16 +17,14 @@ Setting up LinkedIn logins for your application consists of 3 parts: - Add your LinkedIn `client_id` and `client_secret` to your [Supabase Project](https://app.supabase.com). - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js). -## Steps - -### Access your LinkedIn Developer account +## Access your LinkedIn Developer account - Go to [LinkedIn Developer Dashboard](https://www.linkedin.com/developers/apps). - Log in (if necessary.) ![LinkedIn Developer Portal](/img/guides/auth-linkedin/linkedin_developers_page.png) -### Find your callback URL +## Find your callback URL The next step requires a callback URL, which looks like this: @@ -47,7 +45,7 @@ The next step requires a callback URL, which looks like this: /> -### Create a LinkedIn OAuth app +## Create a LinkedIn OAuth app - Go to [LinkedIn Developer Dashboard](https://www.linkedin.com/developers/apps). - Click on `Create App` at the top right @@ -58,7 +56,7 @@ The next step requires a callback URL, which looks like this: - Copy and save your newly-generated `Client ID` - Copy and save your newly-generated `Client Secret` -### Enter your LinkedIn credentials into your Supabase Project +## Enter your LinkedIn credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com). - In the left sidebar, click the `Authentication` icon (near the top). @@ -68,27 +66,19 @@ The next step requires a callback URL, which looks like this: - Enter your `client_id` and `client_secret` saved in the previous step. - Click `Save`. -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers). - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'LinkedIn', -}) -``` - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `linkedin` as the `provider`: ```js async function signInWithLinkedIn() { - const { user, session, error } = await supabase.auth.signIn({ - provider: 'LinkedIn', + const { data, error } = await supabase.auth.signInWithOAuth({ + provider: 'linkedin', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-magic-link.mdx b/apps/reference/docs/guides/auth/auth-magic-link.mdx index 5d320a55abe..391f1ad9365 100644 --- a/apps/reference/docs/guides/auth/auth-magic-link.mdx +++ b/apps/reference/docs/guides/auth/auth-magic-link.mdx @@ -1,13 +1,14 @@ --- id: auth-magic-link title: 'Login With Magic Link' -description: Use Supabase to Authenticate and Authorize your users using Magic Link. +description: Use Supabase to authenticate and authorize your users using magic links. --- import Tabs from '@theme/Tabs' import TabItem from '@theme/TabItem' -By default, if no password is provided, the user will be sent a "magic link" to their email address, which they can click to open your application with a valid session. By default, a given user can only request a Magic Link once every 60 seconds. +Magic links are a form of passwordless logins where users click on a link sent to their email address to log in to their accounts. +Magic links only work with email addresses. By default, a user can only request a magic link once every 60 seconds. ## Overview @@ -25,12 +26,7 @@ Setting up Magic Link logins for your Supabase application. - Under `Email Auth` turn `Enable Email Signup` to ON - Click `Save` -### Add login code to your client app - -Add logins using our client libraries: - -- [JavaScript](/docs/reference/javascript/auth-signin#sign-in-with-magic-link) -- [Dart](/docs/reference/dart/auth-signin#sign-in-with-magic-link) +## Add login code to your client app -```js -const { user, error } = await supabase.auth.signIn({ - email: 'example@email.com', -}) -``` - -
- - - -```dart -final res = await supabase.auth.signIn(email: 'example@email.com'); - -final error = res.error; -``` - - - -
- -Add this function which you can call from a button, link, or UI element. - - - - +When your user signs in, call [signInWithOtp()](/docs/reference/javascript/auth-signinwithotp) with their email address: ```js async function signInWithEmail() { - const { user, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOtp({ email: 'example@email.com', }) } @@ -82,6 +50,8 @@ async function signInWithEmail() { +When your user signs in, call [signIn()](/docs/reference/dart/auth-signin) with their email address: + ```dart Future signInWithEmail() async { await supabase.auth.signIn(email: 'example@email.com'); @@ -92,8 +62,6 @@ Future signInWithEmail() async { -To log out: - +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: + ```js async function signOut() { const { error } = await supabase.auth.signOut() @@ -112,6 +82,8 @@ async function signOut() { +When your user signs out, call [signOut()](/docs/reference/dart/auth-signout) to remove them from the browser session and any objects from localStorage: + ```dart Future signOut() async { await supabase.auth.signOut(); diff --git a/apps/reference/docs/guides/auth/auth-messagebird.mdx b/apps/reference/docs/guides/auth/auth-messagebird.mdx index b67591f0b73..9c7c2bb2756 100644 --- a/apps/reference/docs/guides/auth/auth-messagebird.mdx +++ b/apps/reference/docs/guides/auth/auth-messagebird.mdx @@ -172,7 +172,7 @@ values={[ ```js -let { user, error } = await supabase.auth.signIn({ +let { user, error } = await supabase.auth.signInWithPassword({ phone: '+13334445555', password: 'some-password', }) @@ -209,7 +209,7 @@ values={[ ```js -let { user, error } = await supabase.auth.signIn({ +let { user, error } = await supabase.auth.signInWithOtp({ phone: '+13334445555', }) ``` diff --git a/apps/reference/docs/guides/auth/auth-notion.mdx b/apps/reference/docs/guides/auth/auth-notion.mdx index 1f615f76bbd..31b5d76e778 100644 --- a/apps/reference/docs/guides/auth/auth-notion.mdx +++ b/apps/reference/docs/guides/auth/auth-notion.mdx @@ -17,9 +17,7 @@ Setting up Notion logins for your application consists of 3 parts: - Retrieve your OAuth client ID and OAuth client secret and add them to your [Supabase Project](https://app.supabase.com) - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js) -## Steps - -### Create your notion integration +## Create your notion integration - Go to [developers.notion.com](https://developers.notion.com/). - Click "View my integrations" and login. @@ -33,7 +31,7 @@ Setting up Notion logins for your application consists of 3 parts: ![notion.so](/img/guides/auth-notion/notion-developer.png) -### Add the redirect uri +## Add the redirect URI - After selecting "Public integration", you should see an option to add "Redirect URIs". @@ -58,7 +56,7 @@ Your redirect uri should look like the following: `https://.supabas /> -### Add your Notion credentials into your Supabase Project +## Add your Notion credentials into your Supabase Project - Once you've created your notion integration, you should be able to retrieve the "OAuth client ID" and "OAuth client secret" from the "OAuth Domain and URIs" tab. @@ -71,27 +69,19 @@ Your redirect uri should look like the following: `https://.supabas - Enter the "OAuth client ID" and "OAuth client secret" obtained in the `client id` and `client secret` fields. - Click `Save` -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers) - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'notion', -}) -``` - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `notion` as the `provider`: ```js async function signInWithNotion() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'notion', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-slack.mdx b/apps/reference/docs/guides/auth/auth-slack.mdx index c9568eaa593..c00e377747b 100644 --- a/apps/reference/docs/guides/auth/auth-slack.mdx +++ b/apps/reference/docs/guides/auth/auth-slack.mdx @@ -17,16 +17,14 @@ Setting up Slack logins for your application consists of 3 parts: - Add your Slack `API Key` and `API Secret Key` to your [Supabase Project](https://app.supabase.com). - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js). -## Steps - -### Access your Slack Developer account +## Access your Slack Developer account - Go to [api.slack.com](https://api.slack.com/apps). - Click on `Your Apps` at the top right to log in. ![Slack Developer Portal.](/img/guides/auth-slack/slack-portal.png) -### Find your callback URL +## Find your callback URL The next step requires a callback URL, which looks like this: @@ -47,7 +45,7 @@ The next step requires a callback URL, which looks like this: /> -### Create a Slack OAuth app +## Create a Slack OAuth app - Go to [api.slack.com](https://api.slack.com/apps). - Click on `Create an App` @@ -71,7 +69,7 @@ Under `Redirect URLs`: - Paste your `Callback URL` then click `Add` - Click `Save URLs` -### Enter your Slack credentials into your Supabase Project +## Enter your Slack credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com). - In the left sidebar, click the `Authentication` icon (near the top). @@ -81,27 +79,19 @@ Under `Redirect URLs`: - Enter your `Client ID` (`client_id`) and `Client Secret` (`client_secret`) saved in the previous step. - Click `Save`. -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers). - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'slack', -}) -``` - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `slack` as the `provider`: ```js async function signInWithSlack() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'slack', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-spotify.mdx b/apps/reference/docs/guides/auth/auth-spotify.mdx index c617021e79b..cf282bea973 100644 --- a/apps/reference/docs/guides/auth/auth-spotify.mdx +++ b/apps/reference/docs/guides/auth/auth-spotify.mdx @@ -17,16 +17,14 @@ Setting up Spotify logins for your application consists of 3 parts: - Add your Spotify `API Key` and `API Secret Key` to your [Supabase Project](https://app.supabase.com). - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js). -## Steps - -### Access your Spotify Developer account +## Access your Spotify Developer account - Log into [Spotify](https://spotify.com) - Access the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard) ![Spotify Developer Portal.](/img/guides/auth-spotify/spotify-portal.png) -### Find your callback URL +## Find your callback URL The next step requires a callback URL, which looks like this: @@ -47,7 +45,7 @@ The next step requires a callback URL, which looks like this: /> -### Create a Spotify OAuth app +## Create a Spotify OAuth app - Log into [Spotify](https://spotify.com). - Go to the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard) @@ -66,7 +64,7 @@ Under `Redirect URIs`: - Click `Add` - Click `Save` at the bottom -### Enter your Spotify credentials into your Supabase Project +## Enter your Spotify credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com). - In the left sidebar, click the `Authentication` icon (near the top). @@ -76,27 +74,19 @@ Under `Redirect URIs`: - Enter your `Client ID` (`client_id`) and `Client Secret` (`client_secret`) saved in the previous step. - Click `Save`. -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers). - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'spotify', -}) -``` - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `spotify` as the `provider`: ```js async function signInWithSpotify() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'spotify', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-twilio.mdx b/apps/reference/docs/guides/auth/auth-twilio.mdx index 685f6554746..96abd4aa723 100644 --- a/apps/reference/docs/guides/auth/auth-twilio.mdx +++ b/apps/reference/docs/guides/auth/auth-twilio.mdx @@ -189,7 +189,7 @@ values={[ ```js -let { user, error } = await supabase.auth.signIn({ +let { user, error } = await supabase.auth.signInWithPassword({ phone: '+13334445555', password: 'some-password', }) @@ -226,7 +226,7 @@ values={[ ```js -let { user, error } = await supabase.auth.signIn({ +let { user, error } = await supabase.auth.signInWithOtp({ phone: '+13334445555', }) ``` diff --git a/apps/reference/docs/guides/auth/auth-twitch.mdx b/apps/reference/docs/guides/auth/auth-twitch.mdx index ef76d915913..56a67e6c82c 100644 --- a/apps/reference/docs/guides/auth/auth-twitch.mdx +++ b/apps/reference/docs/guides/auth/auth-twitch.mdx @@ -17,9 +17,7 @@ Setting up Twitch logins for your application consists of 3 parts: - Add your Twitch OAuth Consumer keys to your [Supabase Project](https://app.supabase.com) - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js) -## Steps - -### Access your Twitch Developer account +## Access your Twitch Developer account - Go to [dev.twitch.tv](https://dev.twitch.tv). - Click on `Log in with Twitch` at the top right to log in. @@ -31,7 +29,7 @@ Setting up Twitch logins for your application consists of 3 parts: ![Twitch Developer Console](/img/guides/auth-twitch/twitch-console.png) -### Find your callback URL +## Find your callback URL In the next step you require a callback URL, which looks like this: @@ -52,7 +50,7 @@ In the next step you require a callback URL, which looks like this: /> -### Create a Twitch Application +## Create a Twitch Application ![Twitch Developer Console](/img/guides/auth-twitch/twitch-console.png) @@ -65,7 +63,7 @@ In the next step you require a callback URL, which looks like this: - Select a category for your app. - Check the Captcha box and click `Create`. -### Retrieve your Twitch OAuth Client ID and Client Secret +## Retrieve your Twitch OAuth Client ID and Client Secret - Click `Manage` at the right of your application entry in the list. @@ -77,7 +75,7 @@ In the next step you require a callback URL, which looks like this: ![Get Client ID and Secret](/img/guides/auth-twitch/twitch-get-keys.png) -### Add your Twitch credentials into your Supabase Project +## Add your Twitch credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com) - In the left sidebar, click the `Authentication` icon (near the top) @@ -87,27 +85,19 @@ In the next step you require a callback URL, which looks like this: - Enter your `client_id` and `client_secret` saved in the previous step - Click `Save` -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers) - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'twitch', -}) -``` - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `twitch` as the `provider`: ```js async function signInWithTwitch() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'twitch', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-twitter.mdx b/apps/reference/docs/guides/auth/auth-twitter.mdx index d5f5726d756..88448a12eeb 100644 --- a/apps/reference/docs/guides/auth/auth-twitter.mdx +++ b/apps/reference/docs/guides/auth/auth-twitter.mdx @@ -17,16 +17,14 @@ Setting up Twitter logins for your application consists of 3 parts: - Add your Twitter `API Key` and `API Secret Key` to your [Supabase Project](https://app.supabase.com). - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js). -## Steps - -### Access your Twitter Developer account +## Access your Twitter Developer account - Go to [developer.twitter.com](https://developer.twitter.com). - Click on `Sign in` at the top right to log in. ![Twitter Developer Portal.](/img/guides/auth-twitter/twitter-portal.png) -### Find your callback URL +## Find your callback URL The next step requires a callback URL, which looks like this: @@ -47,7 +45,7 @@ The next step requires a callback URL, which looks like this: /> -### Create a Twitter OAuth app +## Create a Twitter OAuth app - Click `+ Create Project`. - Enter your project name, click `Next`. @@ -67,7 +65,7 @@ The next step requires a callback URL, which looks like this: - Enter your `Privacy policy URL`. - Click `Save`. -### Enter your Twitter credentials into your Supabase Project +## Enter your Twitter credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com). - In the left sidebar, click the `Authentication` icon (near the top). @@ -77,27 +75,19 @@ The next step requires a callback URL, which looks like this: - Enter your `API Key` (`client_id`) and `API Secret Key` (`client_secret`) saved in the previous step. - Click `Save`. -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers). - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'twitter', -}) -``` - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `twitter` as the `provider`: ```js async function signInWithTwitter() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'twitter', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-vonage.mdx b/apps/reference/docs/guides/auth/auth-vonage.mdx index d1a71db6c2b..10fa9378c0c 100644 --- a/apps/reference/docs/guides/auth/auth-vonage.mdx +++ b/apps/reference/docs/guides/auth/auth-vonage.mdx @@ -163,7 +163,7 @@ values={[ ```js -let { user, error } = await supabase.auth.signIn({ +let { user, error } = await supabase.auth.signInWithPassword({ phone: '491512223334444', password: 'some-password', }) @@ -200,7 +200,7 @@ values={[ ```js -let { user, error } = await supabase.auth.signIn({ +let { user, error } = await supabase.auth.signInWithOtp({ phone: '491512223334444', }) ``` diff --git a/apps/reference/docs/guides/auth/auth-workos.mdx b/apps/reference/docs/guides/auth/auth-workos.mdx index 9e2ef7c79e7..e7e9c932b8d 100644 --- a/apps/reference/docs/guides/auth/auth-workos.mdx +++ b/apps/reference/docs/guides/auth/auth-workos.mdx @@ -21,27 +21,25 @@ The procedure consists of five broad steps: - Connect a WorkOS Supported Identity Provider - Add your WorkOS credentials into your Supabase project -## Steps - -### Create a WorkOS Organization +## Create a WorkOS Organization Log in to the dashboard and hop over to the Organizations tab to create and organization ![Create an Organization](/img/guides/auth-workos/workos-create-organization.png) -### Obtain the Client ID and configure Redirect URI +## Obtain the Client ID and configure Redirect URI Head over to the Configuration tab and configure the redirect URI.The redirect URI should look like `https://.supabase.co/auth/v1/callback` Note that this is distinct from the redirect URI referred to in the Supabase dashboard ![Fetch Client ID and configure Redirect URI](/img/guides/auth-workos/workos-clientid-redirect-uri.png) -### Obtain the WorkOS Secret +## Obtain the WorkOS Secret Head over to the API Keys page and obtain the secret key. ![WorkOS Secret Key](/img/guides/auth-workos/workos-secret-key.png) -### Connect a WorkOS Supported Identity Provider +## Connect a WorkOS Supported Identity Provider Set up the identity provider by visiting the setup link. @@ -49,7 +47,7 @@ Set up the identity provider by visiting the setup link. You can pick between any one of the many identity providers that WorkOS supports. -### Add your WorkOS credentials into your Supabase Project +## Add your WorkOS credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com) - In the left sidebar, click the `Authentication` icon (near the top) @@ -58,29 +56,13 @@ You can pick between any one of the many identity providers that WorkOS supports - Enter the `Client ID`, `Secret`, and `WorkOS URL` saved in the previous steps. The ``WorkOS URL` setting should be set to https://api.workos.com/ - Click `Save` -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented in the [Supabase OAuth Reference](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers). Note that you only need to include one of the three parameters: `connection`, `organization`, and `provider`. -You can refer to the [WorkOS Documentation](https://workos.com/docs/reference/sso/authorize/) to learn more about the different methods. - -```js -const { user, session, error } = await supabase.auth.signIn( - { - provider: 'workos', - }, - { - connection: '', - organization: '', - } -) -``` - -Add a function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `workos` as the `provider`: ```js async function signInWithWorkOS() { - const { user, session, error } = await supabase.auth.signIn( + const { data, error } = await supabase.auth.signInWithOAuth( { provider: 'workos', }, @@ -93,7 +75,9 @@ async function signInWithWorkOS() { } ``` -To log out: +Refer to the [WorkOS Documentation](https://workos.com/docs/reference/sso/authorize/) to learn more about the different methods. + +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/auth/auth-zoom.mdx b/apps/reference/docs/guides/auth/auth-zoom.mdx index cab2ad3e6f5..98763329bc8 100644 --- a/apps/reference/docs/guides/auth/auth-zoom.mdx +++ b/apps/reference/docs/guides/auth/auth-zoom.mdx @@ -17,16 +17,14 @@ Setting up Zoom logins for your application consists of 3 parts: - Add your Zoom OAuth keys to your [Supabase Project](https://app.supabase.com) - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js) -## Steps - -### Access your Zoom Developer account +## Access your Zoom Developer account - Go to [marketplace.zoom.us](https://marketplace.zoom.us/). - Click on `Sign In` at the top right to log in. ![Zoom Developer Portal.](/img/guides/auth-zoom/zoom-portal.png) -### Find your callback URL +## Find your callback URL The next step requires a callback URL, which looks like this: @@ -47,7 +45,7 @@ The next step requires a callback URL, which looks like this: /> -### Create a Zoom Oauth App +## Create a Zoom Oauth App - Go to [marketplace.zoom.us](https://marketplace.zoom.us/). - Click on `Sign In` at the top right to log in. @@ -67,7 +65,7 @@ Under `Redirect URL for OAuth` - Paste your `Callback URL` - Click `Continue` -### Enter your Zoom credentials into your Supabase Project +## Enter your Zoom credentials into your Supabase Project - Go to your [Supabase Project Dashboard](https://app.supabase.com) - In the left sidebar, click the `Authentication` icon (near the top) @@ -77,27 +75,19 @@ Under `Redirect URL for OAuth` - Enter your `Zoom Client ID` and `Zoom Client Secret` saved in the previous step - Click `Save` -### Add login code to your client app +## Add login code to your client app -The JavaScript client code is documented here: [Supabase OAuth Client Code](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers) - -```js -const { user, session, error } = await supabase.auth.signIn({ - provider: 'zoom', -}) -``` - -Add this function which you can call from a button, link, or UI element. +When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `zoom` as the `provider`: ```js async function signInWithZoom() { - const { user, session, error } = await supabase.auth.signIn({ + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'zoom', }) } ``` -To log out: +When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage: ```js async function signout() { diff --git a/apps/reference/docs/guides/functions.mdx b/apps/reference/docs/guides/functions.mdx index 5c358426ad1..0391adce248 100644 --- a/apps/reference/docs/guides/functions.mdx +++ b/apps/reference/docs/guides/functions.mdx @@ -7,29 +7,6 @@ sidebar_label: Overview import Tabs from '@theme/Tabs' import TabItem from '@theme/TabItem' -import ButtonCard from '@site/src/components/ButtonCard' -const examples = [ - { - name: 'With supabase-js', - description: 'Use the Supabase client inside your Edge Function.', - href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts', - }, - { - name: 'With CORS headers', - description: 'Send CORS headers for invoking from the browser.', - href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts', - }, - { - name: 'React Native with Stripe', - description: 'Full example for using Supabase and Stripe, with Expo.', - href: 'https://github.com/supabase-community/expo-stripe-payments-with-supabase-functions', - }, - { - name: 'Flutter with Stripe', - description: 'Full example for using Supabase and Stripe, with Flutter.', - href: 'https://github.com/supabase-community/flutter-stripe-payments-with-supabase-functions', - }, -] Edge Functions are server-side TypeScript functions, distributed globally at the edge—close to your users. They can be used for listening to webhooks or integrating your Supabase project with third-parties [like Stripe](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/stripe-webhooks). @@ -235,26 +212,6 @@ To see all the secrets which you have set remotely, use [`supabase secrets list` supabase secrets list ``` -## Examples - -You can find a list of useful [Edge Function Examples](https://github.com/supabase/supabase/tree/master/examples/edge-functions) in our GitHub repository. - -
-
- {examples.map((x) => ( -
- -
- ))} -
-
- ## Suggestions ### Database Functions vs Edge Functions @@ -313,5 +270,5 @@ serve(async (req) => { - Cannot write to File System - Edge Functions - Local development - only one function at a time - - Supabase Functions only supports `GET` and `POST` requests. - - Supabase Functions do not support HTML responses. + - Supabase Functions only supports `POST`, `OPTIONS`, and `GET` requests. + - Serving of HTML content is not supported (`GET` requests that return `text/html` will be rewritten to `text/plain`). diff --git a/apps/reference/docs/guides/functions/auth.mdx b/apps/reference/docs/guides/functions/auth.mdx new file mode 100644 index 00000000000..da021238a02 --- /dev/null +++ b/apps/reference/docs/guides/functions/auth.mdx @@ -0,0 +1,63 @@ +--- +id: auth +title: Auth +description: Supabase Edge Functions and Auth. +--- + +Edge Functions work seamlessly with [Supabase Auth](/docs/guides/auth), allowing you to identify which user called your function. + +When [invoking](/docs/reference/javascript/invoke) a function with one of the [client libraries](/docs/reference), the logged in user's JWT is automatically attached to the function call and becomes accessible within your function. + +This is important, for example, to identify which customer's credit card should be charged. You can see this concept end-to-end in our [Stripe example app](https://github.com/supabase-community/expo-stripe-payments-with-supabase-functions). + +## Auth Context & RLS + +By creating a supabase client with the auth context from the function, you can do two things: + +1. Get the user object. +2. Run queries in the context of the user with [Row Level Security (RLS)](/docs/guides/auth/row-level-security) policies enforced. + +```js title="supabase/functions/select-from-table-with-auth-rls/index.ts" +import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.0.0-rc.12' + +serve(async (req: Request) => { + try { + // Create a Supabase client with the Auth context of the logged in user. + const supabaseClient = createClient( + // Supabase API URL - env var exported by default. + Deno.env.get('SUPABASE_URL') ?? '', + // Supabase API ANON KEY - env var exported by default. + Deno.env.get('SUPABASE_ANON_KEY') ?? '', + // Create client with Auth context of the user that called the function. + // This way your row-level-security (RLS) policies are applied. + // highlight-next-line + { global: { headers: { Authorization: req.headers.get('Authorization')! } } } + ) + // Now we can get the session or user object + // highlight-start + const { + data: { user }, + } = await supabaseClient.auth.getUser() + // highlight-end + + // And we can run queries in the context of our authenticated user + // highlight-start + const { data, error } = await supabaseClient.from('users').select('*') + if (error) throw error + // highlight-end + + return new Response(JSON.stringify({ user, data }), { + headers: { 'Content-Type': 'application/json' }, + status: 200, + }) + } catch (error) { + return new Response(JSON.stringify({ error: error.message }), { + headers: { 'Content-Type': 'application/json' }, + status: 400, + }) + } +}) +``` + +See the [example on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts). diff --git a/apps/reference/docs/guides/functions/examples.mdx b/apps/reference/docs/guides/functions/examples.mdx new file mode 100644 index 00000000000..e56dc4da517 --- /dev/null +++ b/apps/reference/docs/guides/functions/examples.mdx @@ -0,0 +1,47 @@ +--- +id: examples +title: Examples +description: Useful Supabase Edge Functions Examples. +--- + +import ButtonCard from '@site/src/components/ButtonCard' +const examples = [ + { + name: 'With supabase-js', + description: 'Use the Supabase client inside your Edge Function.', + href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts', + }, + { + name: 'With CORS headers', + description: 'Send CORS headers for invoking from the browser.', + href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts', + }, + { + name: 'React Native with Stripe', + description: 'Full example for using Supabase and Stripe, with Expo.', + href: 'https://github.com/supabase-community/expo-stripe-payments-with-supabase-functions', + }, + { + name: 'Flutter with Stripe', + description: 'Full example for using Supabase and Stripe, with Flutter.', + href: 'https://github.com/supabase-community/flutter-stripe-payments-with-supabase-functions', + }, +] + +You can find a list of useful [Edge Function Examples](https://github.com/supabase/supabase/tree/master/examples/edge-functions) in our GitHub repository. + +
+
+ {examples.map((x) => ( +
+ +
+ ))} +
+
diff --git a/apps/reference/docs/guides/platform/logs.mdx b/apps/reference/docs/guides/platform/logs.mdx index bcc2173f994..6810cce4433 100644 --- a/apps/reference/docs/guides/platform/logs.mdx +++ b/apps/reference/docs/guides/platform/logs.mdx @@ -34,7 +34,32 @@ To update the screenshots, ensure that at least one log line is selected to disp
-[Postgres logs](https://app.supabase.com/project/_/database/postgres-logs) show all queries and activity for your [database](../../guides/database). +[Postgres logs](https://app.supabase.com/project/_/database/postgres-logs) show queries and activity for your [database](../../guides/database). + +By default, query logs are disabled for all new Supabase projects, as they can reveal metadata about the contents of your database (e.g. table and column names). If you'd like to enable query logs, you can enable and configure the [pgAudit extension](https://www.pgaudit.org/): + +- [Enable the pgAudit extension for your project](https://app.supabase.com/project/_/database/extensions) +- [Reboot the DB](https://app.supabase.com/project/_/settings/general); a "Fast database reboot" is sufficient, and faster than a full project restart +- Configure `pgaudit.log` to the [appropriate value](https://github.com/pgaudit/pgaudit/blob/master/README.md#pgauditlog) +- You should be able to view query logs in your [log explorer](https://app.supabase.com/project/_/database/postgres-logs) now + +As an example, if you wanted to enable logs for writes and DDL statements for a single session, you could execute within the session: + +```sql +set pgaudit.log = 'write, ddl'; +``` + +To _permanently_ enable logs for all statements that relate to roles and privileges, you could execute (followed by a db reboot): + +```sql +alter system set pgaudit.log to 'role'; +``` + +To remove a system-wide setting, you can use (followed by a db reboot): + +```sql +alter system reset pgaudit.log +``` ![Postgres Logs](/img/guides/platform/logs/logs-database.png) diff --git a/apps/reference/docs/guides/realtime/postgres-cdc.mdx b/apps/reference/docs/guides/realtime/postgres-cdc.mdx index f8ca7c4728b..5752274dbbc 100644 --- a/apps/reference/docs/guides/realtime/postgres-cdc.mdx +++ b/apps/reference/docs/guides/realtime/postgres-cdc.mdx @@ -10,8 +10,14 @@ Anyone with access to a valid JWT signed with the project's JWT secret is able t Clients can choose to receive `INSERT`, `UPDATE`, `DELETE`, or `*` (all) changes for all changes in a schema, a table in a schema, or a column's value in a table. Your clients should only listen to tables in the `public` schema and you must first enable the tables you want your clients to listen to. +Postgres CDC works out of the box for tables in the `public` schema. You can listen to tables in your private schemas by granting table `SELECT` permissions to the database role found in your access token. You can run a query similar to the following: + +```sql +GRANT SELECT ON "private_schema"."table" TO authenticated; +``` + :::caution -Postgres CDC works out of the box for tables in the `public` schema. We do not yet recommend attempting to listen to tables in private schemas due to security concerns and additional permission requirements. +We strongly encourage you to enable RLS and create policies for tables in private schemas. Otherwise, any role you grant access to will have unfettered read access to the table. ::: You can do this in the [Replication](https://app.supabase.com/project/_/database/replication) section in the Dashboard or with the [SQL editor](https://app.supabase.com/project/_/sql): diff --git a/apps/reference/docs/guides/realtime/quickstart.mdx b/apps/reference/docs/guides/realtime/quickstart.mdx index e099abd3a46..f1918fe9ab6 100644 --- a/apps/reference/docs/guides/realtime/quickstart.mdx +++ b/apps/reference/docs/guides/realtime/quickstart.mdx @@ -235,7 +235,7 @@ const roomId = 'room1' const userId = 'user1' channel.on( - 'postgres-changes', + 'postgres_changes', { event: 'INSERT', schema: 'public', diff --git a/apps/reference/docs/guides/storage-cdn.mdx b/apps/reference/docs/guides/storage-cdn.mdx index 7527c9e8c3c..c4b918880fe 100644 --- a/apps/reference/docs/guides/storage-cdn.mdx +++ b/apps/reference/docs/guides/storage-cdn.mdx @@ -19,7 +19,7 @@ Aside from performance, CDNs also help with security and availability by mitigat By default, assets are cached both in the CDN and in the user’s browser for 1 hour. After this, the CDN nodes ping the storage server to see if an object has been updated. -You can modify this cache time when you are [uploading](https://supabase.com/docs/reference/javascript/next/storage-from-upload) or [updating](https://supabase.com/docs/reference/javascript/next/storage-from-update) an object by modifying the `cacheControl` parameter. +You can modify this cache time when you are [uploading](https://supabase.com/docs/reference/javascript/storage-from-upload) or [updating](https://supabase.com/docs/reference/javascript/storage-from-update) an object by modifying the `cacheControl` parameter. If you expect the object to not change at a given URL, setting a longer cache duration is preferable. diff --git a/apps/reference/docs/guides/storage.mdx b/apps/reference/docs/guides/storage.mdx index 0dfc47ef526..7170f5857c9 100644 --- a/apps/reference/docs/guides/storage.mdx +++ b/apps/reference/docs/guides/storage.mdx @@ -272,7 +272,7 @@ You can access the storage API directly with the same endpoint. See the -## Community - -Reference documentation for community-contributed utilities. - -
-
- {/* Auth */} -
- -
- {/* */} -
-
- ## Self-hosting Reference documentation for self-hosting Supabase features. diff --git a/apps/reference/docs/support.md b/apps/reference/docs/support.md index 4c8f1572ffa..2a0bf4f01dd 100644 --- a/apps/reference/docs/support.md +++ b/apps/reference/docs/support.md @@ -12,10 +12,10 @@ For help and questions about best practices, we have a discussion forum set up o ## Business Support -We offer email based support for business users. You can email us at support@supabase.io. +We offer email based support for business users. You can email us at support@supabase.com. If you need SLAs, guaranteed response times, or other enterprise level services, please contact us at this email address. ## Slack Support -If you are building with Supabase and you would like to set up a shared Slack channel (via [Slack Connect](https://slack.com/connect)), please reach out to us at support@supabase.io. +If you are building with Supabase and you would like to set up a shared Slack channel (via [Slack Connect](https://slack.com/connect)), please reach out to us at support@supabase.com. diff --git a/apps/reference/docusaurus.config.js b/apps/reference/docusaurus.config.js index 7534657645b..ba1093b6fb0 100644 --- a/apps/reference/docusaurus.config.js +++ b/apps/reference/docusaurus.config.js @@ -112,11 +112,10 @@ const config = { breadcrumbs: false, editUrl: 'https://github.com/supabase/supabase/edit/master/apps/reference/', - lastVersion: 'v1', + lastVersion: 'current', versions: { current: { - label: 'v2 RC', - path: '/next', + label: 'v2', }, v1: { label: 'v1', @@ -124,18 +123,6 @@ const config = { }, }, ], - [ - '@docusaurus/plugin-content-docs', - { - id: '_auth_helpers', - path: '_auth_helpers', - routeBasePath: '/reference/auth-helpers', - sidebarPath: require.resolve('./nav/auth_helpers_sidebars.js'), - breadcrumbs: false, - editUrl: - 'https://github.com/supabase/supabase/edit/master/apps/reference/', - }, - ], ], presets: [ diff --git a/apps/reference/nav/_referenceSidebars.js b/apps/reference/nav/_referenceSidebars.js index 0234ea1bf78..e0150208708 100644 --- a/apps/reference/nav/_referenceSidebars.js +++ b/apps/reference/nav/_referenceSidebars.js @@ -156,7 +156,12 @@ const sidebars = { type: 'category', label: 'Edge Functions', collapsed: true, - items: ['guides/functions', 'guides/functions/cicd-workflow'], + items: [ + 'guides/functions', + 'guides/functions/auth', + 'guides/functions/examples', + 'guides/functions/cicd-workflow', + ], }, { type: 'category', @@ -251,19 +256,20 @@ const sidebars = { { type: 'link', label: 'Supabase API', href: '/reference/api' }, ], }, - { - type: 'category', - label: 'Community', - // link: { type: 'doc', id: 'about' }, - collapsed: false, - items: [ - { - type: 'link', - label: 'Supabase Auth Helpers', - href: '/reference/auth-helpers', - }, - ], - }, + // TODO: add back in with typedocs + // { + // type: 'category', + // label: 'Community', + // // link: { type: 'doc', id: 'about' }, + // collapsed: false, + // items: [ + // { + // type: 'link', + // label: 'Supabase Auth Helpers', + // href: '/reference/auth-helpers', + // }, + // ], + // }, { type: 'category', label: 'Self-hosting', diff --git a/apps/reference/nav/auth_helpers_sidebars.js b/apps/reference/nav/auth_helpers_sidebars.js deleted file mode 100644 index ac225b54381..00000000000 --- a/apps/reference/nav/auth_helpers_sidebars.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Creating a sidebar enables you to: - - create an ordered group of docs - - render a sidebar for each doc of that group - - provide next/previous navigation - - The sidebars can be generated from the filesystem, or explicitly defined here. - - Create as many sidebars as you want. - */ - -// @ts-check - -/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ -const sidebars = { - // By default, Docusaurus generates a sidebar from the docs folder structure - // tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }], - - sidebar: [ - 'intro', - 'next-js', - 'sveltekit', - // 'usage', - // 'config', - // 'release-notes', - // { - // type: "category", - // label: "Release Notes", - // items: ["release-notes"], - // }, - ], -} - -module.exports = sidebars diff --git a/apps/www/_blog/2021-03-02-supabase-beta-february-2021.mdx b/apps/www/_blog/2021-03-02-supabase-beta-february-2021.mdx index 429fb7939b5..63cc53838f8 100644 --- a/apps/www/_blog/2021-03-02-supabase-beta-february-2021.mdx +++ b/apps/www/_blog/2021-03-02-supabase-beta-february-2021.mdx @@ -44,7 +44,7 @@ Writing SQL just got 10x easier. We added autocomplete to the SQL editor, includ ### Auth Redirects -Redirect your users to specific route within your site on [`signIn()`](/docs/reference/javascript/auth-signin#sign-in-with-redirect) and [`signUp()`](/docs/reference/javascript/auth-signup#sign-up-with-redirect). +Redirect your users to specific route within your site on [`signIn()`](/docs/reference/javascript/v1/auth-signin#sign-in-with-redirect) and [`signUp()`](/docs/reference/javascript/auth-signup#sign-up-with-redirect). ![Redirect your users after sign up](/images/blog/feb/auth-redirect.png) diff --git a/apps/www/_blog/2022-08-16-supabase-js-v2.mdx b/apps/www/_blog/2022-08-16-supabase-js-v2.mdx index f64617a7e0b..b327552f6b7 100644 --- a/apps/www/_blog/2022-08-16-supabase-js-v2.mdx +++ b/apps/www/_blog/2022-08-16-supabase-js-v2.mdx @@ -302,7 +302,7 @@ Update today by running: npm i @supabase/supabase-js@rc ``` -You can find a full list of updates, breaking changes, and a migration guide in our [Release Notes](https://supabase.com/docs/reference/javascript/next/release-notes). +You can find a full list of updates, breaking changes, and a migration guide in our [Release Notes](https://supabase.com/docs/reference/javascript/release-notes). We'll continuing merging security fixes to v1, with maintenance patches for the next three months. @@ -321,8 +321,8 @@ We'll continuing merging security fixes to v1, with maintenance patches for the ## Links -- [v2 Release Candidate Documentation](https://supabase.com/docs/reference/javascript/next) -- [Release Notes & Migration Guide](https://supabase.com/docs/reference/javascript/next/release-notes) +- [v2 Release Candidate Documentation](https://supabase.com/docs/reference/javascript) +- [Release Notes & Migration Guide](https://supabase.com/docs/reference/javascript/release-notes) ## More Launch Week 5 diff --git a/apps/www/_blog/2022-08-19-supabase-vault.mdx b/apps/www/_blog/2022-08-19-supabase-vault.mdx index 0939e662607..62b386bd1b9 100644 --- a/apps/www/_blog/2022-08-19-supabase-vault.mdx +++ b/apps/www/_blog/2022-08-19-supabase-vault.mdx @@ -2,8 +2,8 @@ title: 'Supabase Vault' description: "Today we're announcing Vault, a Postgres extension for managing secrets and encryption inside your database." author: michel -image: lw5-vault/lw5-vault.jpg -thumb: lw5-vault/lw5-vault.jpg +image: lw5-vault/supabase-vault.jpg +thumb: lw5-vault/supabase-vault.jpg tags: - launch-week date: '2022-08-19' diff --git a/apps/www/_blog/2022-09-08-choosing-a-postgres-primary-key.mdx b/apps/www/_blog/2022-09-08-choosing-a-postgres-primary-key.mdx index 821ee8191f5..6fa24cafbc4 100644 --- a/apps/www/_blog/2022-09-08-choosing-a-postgres-primary-key.mdx +++ b/apps/www/_blog/2022-09-08-choosing-a-postgres-primary-key.mdx @@ -2,8 +2,8 @@ title: Choosing a Postgres Primary Key description: Turns out the question of which identifier to use as a Primary Key is complicated -- we're going to dive into some of the complexity and inherent trade-offs, and figure things out author: victor -image: primary-keys/choosing-a-postgres-primary-key-og.jpg -thumb: primary-keys/choosing-a-postgres-primary-key-og.jpg +image: primary-keys/postgres-primary-key.jpg +thumb: primary-keys/postgres-primary-key.jpg tags: - postgres date: '2022-09-08' diff --git a/apps/www/components/LaunchWeek/days.json b/apps/www/components/LaunchWeek/days.json index d2ac292e237..bdc0150c0a2 100644 --- a/apps/www/components/LaunchWeek/days.json +++ b/apps/www/components/LaunchWeek/days.json @@ -43,12 +43,12 @@ "products": [ { "title": "Supabase.js v2 Docs", - "url": "/docs/reference/javascript/next/installing", + "url": "/docs/reference/javascript/installing", "description": "Updated documentation for supabase-js" }, { "title": "Typescript support", - "url": "/docs/reference/javascript/next/typescript-support", + "url": "/docs/reference/javascript/typescript-support", "description": "Using types generated directly from your database." } ] diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index 078cce07d4d..97e6e3bfc04 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -255,7 +255,7 @@ module.exports = [ { permanent: false, source: '/docs/client/auth-signin', - destination: '/docs/reference/javascript/auth-signin', + destination: '/docs/reference/javascript/v1/auth-signin', }, { permanent: false, @@ -1331,7 +1331,7 @@ module.exports = [ { permanent: true, source: '/docs/reference/javascript/next/migration-guide', - destination: '/docs/reference/javascript/next/release-notes', + destination: '/docs/reference/javascript/release-notes', }, { permanent: true, @@ -1343,6 +1343,21 @@ module.exports = [ source: '/docs/guides/client-libraries', destination: '/docs/reference', }, + { + permanent: true, + source: '/docs/reference/auth-helpers', + destination: '/docs/guides/auth/auth-helpers/', + }, + { + permanent: true, + source: '/docs/reference/auth-helpers/next-js', + destination: '/docs/guides/auth/auth-helpers/nextjs', + }, + { + permanent: true, + source: '/docs/reference/auth-helpers/sveltekit', + destination: '/docs/guides/auth/auth-helpers/sveltekit', + }, // V2 redirects // { diff --git a/apps/www/public/images/blog/lw5-vault/supabase-vault.jpg b/apps/www/public/images/blog/lw5-vault/supabase-vault.jpg new file mode 100644 index 00000000000..da4d20af662 Binary files /dev/null and b/apps/www/public/images/blog/lw5-vault/supabase-vault.jpg differ diff --git a/apps/www/public/images/blog/primary-keys/postgres-primary-key.jpg b/apps/www/public/images/blog/primary-keys/postgres-primary-key.jpg new file mode 100644 index 00000000000..f6d21099f66 Binary files /dev/null and b/apps/www/public/images/blog/primary-keys/postgres-primary-key.jpg differ diff --git a/examples/edge-functions/supabase/functions/get-tshirt-competition/README.md b/examples/edge-functions/supabase/functions/get-tshirt-competition/README.md index 9f6033295a7..8d0bcb0dd72 100644 --- a/examples/edge-functions/supabase/functions/get-tshirt-competition/README.md +++ b/examples/edge-functions/supabase/functions/get-tshirt-competition/README.md @@ -2,10 +2,10 @@ To celebrate the support of GET requests in Supabase Edge Functions we're giving away some limited edition Functions Shirts! -To win a Supabase Edge Functions T-Shirt, make a GET request to https://obuldanrptloktxcffvn.functions.supabase.co/get-tshirt-competition with your email address, your Twitter handle (optional), and the correct answer, e.g. +To win a Supabase Edge Functions T-Shirt, make a GET request to https://obuldanrptloktxcffvn.functions.supabase.co/get-tshirt-competition with your email address, your Twitter handle (optional), your t-shirt size (S-2XL) and the correct answer, e.g. ```text -https://obuldanrptloktxcffvn.functions.supabase.co/get-tshirt-competition?email=testr@test.de&twitter=thorwebdev&answer=20 +https://obuldanrptloktxcffvn.functions.supabase.co/get-tshirt-competition?email=testr@test.de&twitter=thorwebdev&size=2XL&answer=20 ``` You can read the functions source code to figure out the answer ;) diff --git a/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts b/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts index 75f0985258d..392da9f29f0 100644 --- a/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts +++ b/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts @@ -27,11 +27,17 @@ serve(async (req) => { const url = new URL(req.url) const email = url.searchParams.get('email') const twitter = url.searchParams.get('twitter') + const size = url.searchParams.get('size') const answer = url.searchParams.get('answer') - if (!/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email ?? '') || !answer) + if ( + !/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email ?? '') || + !answer || + !twitter || + !size + ) throw new Error( - `You're missing either a valid 'email' and/or 'answer' param(s). HINT: https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts` + `Please provide valid 'email', 'twitter', 'size', and 'answer' params. HINT: https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts` ) if (Number(answer) !== positionInAlphabet(email!)) @@ -50,6 +56,7 @@ serve(async (req) => { { email, twitter, + size, }, { onConflict: 'email' } ) @@ -71,4 +78,4 @@ serve(async (req) => { }) // To invoke: -// curl -i --location --request GET 'http://localhost:54321/functions/v1/get-tshirt-competition?email=testr@test.de&answer=20' +// curl -i --location --request GET 'http://localhost:54321/functions/v1/get-tshirt-competition?email=testr@test.de&twitter=thorwebdev&size=2XL&answer=20' diff --git a/packages/config/tailwind.config.js b/packages/config/tailwind.config.js index 9aa9601e9e1..7032d04e011 100644 --- a/packages/config/tailwind.config.js +++ b/packages/config/tailwind.config.js @@ -102,6 +102,9 @@ module.exports = ui({ border: '1px solid ' + theme('borderColor.DEFAULT'), borderRadius: theme('borderRadius.lg'), }, + td: { + borderBottom: '1px solid ' + theme('colors.scale[400]'), + }, code: { fontWeight: '400', padding: '0.2rem 0.4rem', diff --git a/spec/enrichments/tsdoc_v2/combined.json b/spec/enrichments/tsdoc_v2/combined.json index a081431e174..16cdf1b5fda 100644 --- a/spec/enrichments/tsdoc_v2/combined.json +++ b/spec/enrichments/tsdoc_v2/combined.json @@ -63702,7 +63702,7 @@ "kindString": "Type parameter", "flags": {}, "comment": { - "shortText": "Types for the schema from the [type\ngenerator](https://supabase.com/docs/reference/javascript/next/typescript-support)\n" + "shortText": "Types for the schema from the [type\ngenerator](https://supabase.com/docs/reference/javascript/typescript-support)\n" }, "default": { "type": "intrinsic", diff --git a/spec/enrichments/tsdoc_v2/postgrest.json b/spec/enrichments/tsdoc_v2/postgrest.json index 7be98a509dd..9f28fa4b0c1 100644 --- a/spec/enrichments/tsdoc_v2/postgrest.json +++ b/spec/enrichments/tsdoc_v2/postgrest.json @@ -1717,7 +1717,7 @@ "kindString": "Type parameter", "flags": {}, "comment": { - "shortText": "Types for the schema from the [type\ngenerator](https://supabase.com/docs/reference/javascript/next/typescript-support)\n" + "shortText": "Types for the schema from the [type\ngenerator](https://supabase.com/docs/reference/javascript/typescript-support)\n" }, "default": { "type": "intrinsic", diff --git a/spec/enrichments/tsdoc_v2/postgrest_dereferenced.json b/spec/enrichments/tsdoc_v2/postgrest_dereferenced.json index 41afb1bd394..be2e4eb6e32 100644 --- a/spec/enrichments/tsdoc_v2/postgrest_dereferenced.json +++ b/spec/enrichments/tsdoc_v2/postgrest_dereferenced.json @@ -1792,7 +1792,7 @@ "kindString": "Type parameter", "flags": {}, "comment": { - "shortText": "Types for the schema from the [type\ngenerator](https://supabase.com/docs/reference/javascript/next/typescript-support)\n" + "shortText": "Types for the schema from the [type\ngenerator](https://supabase.com/docs/reference/javascript/typescript-support)\n" }, "default": { "type": "intrinsic", diff --git a/spec/supabase_js_v1.yml b/spec/supabase_js_v1.yml index 82644287122..cc798ad9daa 100644 --- a/spec/supabase_js_v1.yml +++ b/spec/supabase_js_v1.yml @@ -58,10 +58,10 @@ pages: - name: Sign up with third-party providers. hideCodeBlock: true description: | - You can sign up with OAuth providers using the [`signIn()`](/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers) method. + You can sign up with OAuth providers using the [`signIn()`](/docs/reference/javascript/v1/auth-signin#sign-in-using-third-party-providers) method. - name: Sign up with Phone. description: | - Supabase supports Phone Auth. After a user has verified their number, they can use the [`signIn()`](/docs/reference/javascript/auth-signin#sign-in-using-phone) method. + Supabase supports Phone Auth. After a user has verified their number, they can use the [`signIn()`](/docs/reference/javascript/v1/auth-signin#sign-in-using-phone) method. js: | ```js const { user, session, error } = await supabase.auth.signUp({ diff --git a/spec/supabase_js_v2.yml b/spec/supabase_js_v2.yml index 0f029ebf386..ebc143c4017 100644 --- a/spec/supabase_js_v2.yml +++ b/spec/supabase_js_v2.yml @@ -93,7 +93,7 @@ pages: - If signUp() is called for an existing confirmed user: - If **Confirm email** is enabled in [your project](https://app.supabase.com/project/_/auth/settings), an obfuscated/fake user object is returned. - If **Confirm email** is disabled, the error message, `User already registered` is returned. - - To fetch the currently logged-in user, refer to [`getUser()`](/docs/reference/javascript/next/auth-getuser). + - To fetch the currently logged-in user, refer to [`getUser()`](/docs/reference/javascript/auth-getuser). examples: - name: Sign up. isSpotlight: true @@ -641,8 +641,8 @@ pages: $ref: '@supabase/postgrest-js.PostgrestQueryBuilder.select' notes: | - By default, Supabase projects return a maximum of 1,000 rows. This setting can be changed in your project's [API settings](https://app.supabase.com/project/_/settings/api). It's recommended that you keep it low to limit the payload size of accidental or malicious requests. You can use `range()` queries to paginate through your data. - - `select()` can be combined with [Filters](/docs/reference/javascript/next/using-filters) - - `select()` can be combined with [Modifiers](/docs/reference/javascript/next/using-modifiers) + - `select()` can be combined with [Filters](/docs/reference/javascript/using-filters) + - `select()` can be combined with [Modifiers](/docs/reference/javascript/using-modifiers) - `apikey` is a reserved keyword if you're using the [Supabase Platform](/docs/guides/hosting/platform) and should be avoided as a column name](https://github.com/supabase/supabase/issues/5465). examples: - name: Getting your data @@ -1088,7 +1088,7 @@ pages: - name: Querying with count option description: | You can get the number of rows by using the - [count](/docs/reference/javascript/next/select#parameters) option. For + [count](/docs/reference/javascript/select#parameters) option. For example, to get the table count without returning all rows: @@ -1367,7 +1367,7 @@ pages: title: 'Modify data: update()' $ref: '@supabase/postgrest-js.PostgrestQueryBuilder.update' notes: | - - `update()` should always be combined with [Filters](/docs/reference/javascript/next/using-filters) to target the item(s) you wish to update. + - `update()` should always be combined with [Filters](/docs/reference/javascript/using-filters) to target the item(s) you wish to update. examples: - name: Updating your data description: | @@ -1758,7 +1758,7 @@ pages: title: 'Delete data: delete()' $ref: '@supabase/postgrest-js.PostgrestQueryBuilder.delete' notes: | - - `delete()` should always be combined with [filters](/docs/reference/javascript/next/using-filters) to target the item(s) you wish to delete. + - `delete()` should always be combined with [filters](/docs/reference/javascript/using-filters) to target the item(s) you wish to delete. - If you use `delete()` with filters and you have [RLS](/docs/learn/auth-deep-dive/auth-row-level-security) enabled, only rows visible through `SELECT` policies are deleted. Note that by default @@ -1958,8 +1958,8 @@ pages: - name: Call a Postgres function with filters description: | Postgres functions that return tables can also be combined with - [Filters](/docs/reference/javascript/next/using-filters) and - [Modifiers](/docs/reference/javascript/next/using-modifiers). + [Filters](/docs/reference/javascript/using-filters) and + [Modifiers](/docs/reference/javascript/using-modifiers). @@ -2019,7 +2019,7 @@ pages: Filters must be applied after any of `select()`, `update()`, `upsert()`, `delete()`, and `rpc()` and before - [modifiers](/docs/reference/javascript/next/using-modifiers). + [modifiers](/docs/reference/javascript/using-modifiers). ```ts const { data, error } = await supabase diff --git a/studio/components/grid/components/header/Header.tsx b/studio/components/grid/components/header/Header.tsx index 74f76a9fa89..d12de67b882 100644 --- a/studio/components/grid/components/header/Header.tsx +++ b/studio/components/grid/components/header/Header.tsx @@ -112,12 +112,13 @@ const RowHeader: FC = ({ sorts, filters }) => { } const onRowsDelete = () => { + const numRows = allRowsSelected ? totalRows : selectedRows.size confirmAlert({ title: 'Confirm to delete', - message: `Are you sure you want to delete the selected ${selectedRows.size} row${ - selectedRows.size > 1 ? 's' : '' + message: `Are you sure you want to delete the selected ${numRows} row${ + numRows > 1 ? 's' : '' }? This action cannot be undone.`, - confirmText: `Delete ${selectedRows.size} rows`, + confirmText: `Delete ${numRows} rows`, onAsyncConfirm: async () => { if (allRowsSelected) { const { error } = diff --git a/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx b/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx index 09a46d60bbf..7bca0b40e61 100644 --- a/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx +++ b/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx @@ -27,10 +27,10 @@ const PolicyRow: FC = ({ 'w-full space-x-4 border-b py-4 lg:items-center', ].join(' ')} > -
+

{policy.command}

-

{policy.name}

+

{policy.name}

Applied to:

diff --git a/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx b/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx index 506b50e2370..f7893223a7e 100644 --- a/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx +++ b/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx @@ -1,23 +1,37 @@ import { FC } from 'react' import { IconAlertCircle } from 'ui' import InformationBox from 'components/ui/InformationBox' +import { MemberWithFreeProjectLimit } from 'hooks' interface Props { - limit: number + membersExceededLimit: MemberWithFreeProjectLimit[] } -const FreeProjectLimitWarning: FC = ({ limit }) => { +const FreeProjectLimitWarning: FC = ({ membersExceededLimit }) => { return (
} defaultVisibility={true} hideCollapse - title="Your account has reached its free project limit" + title="The organization has members who have exceeded their free project limits" description={

- {`Your account can only have up to ${limit} free projects - to create another free project, you'll need to delete or pause an existing free project first. Otherwise, you may create a project on the Pro tier instead.`} + The following members have reached their maximum limits for the number of active free + tier projects within organizations where they are an administrator or owner: +

+
    + {membersExceededLimit.map((member, idx: number) => ( +
  • + {member.username || member.primary_email} (Limit: {member.free_project_limit} free + projects) +
  • + ))} +
+

+ These members will need to either delete, pause, or upgrade one or more of these + projects before you're able to create a free project within this organization.

} diff --git a/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx b/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx index c048cb59d2c..9a4e472c322 100644 --- a/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx +++ b/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx @@ -13,9 +13,10 @@ interface Props { const DatePickers: React.FC = ({ to, from, onChange, helpers }) => { const defaultHelper = getDefaultHelper(helpers) const [helperValue, setHelperValue] = useState(to || from ? '' : defaultHelper.text) + const handleHelperChange = (newValue: string) => { - setHelperValue(newValue) const selectedHelper = helpers.find((h) => h.text === newValue) + if (onChange && selectedHelper) { onChange({ to: selectedHelper.calcTo(), from: selectedHelper.calcFrom() }) } @@ -28,6 +29,7 @@ const DatePickers: React.FC = ({ to, from, onChange, helpers }) => { return false } }) + useEffect(() => { if (selectedHelper && helperValue !== selectedHelper.text) { setHelperValue(selectedHelper.text) diff --git a/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/studio/components/interfaces/Settings/Logs/Logs.constants.ts index b54c691a328..d3a6d2de484 100644 --- a/studio/components/interfaces/Settings/Logs/Logs.constants.ts +++ b/studio/components/interfaces/Settings/Logs/Logs.constants.ts @@ -21,7 +21,7 @@ export const TEMPLATES: LogTemplate[] = [ description: 'Count of commits made by users on the database', mode: 'custom', searchString: `select - p.user_name, + p.user_name, count(*) as count from postgres_logs left join unnest(metadata) as m on true @@ -38,7 +38,7 @@ group by description: 'List all IP addresses that used the Supabase API', mode: 'custom', searchString: `select - cast(timestamp as datetime) as timestamp, + cast(timestamp as datetime) as timestamp, h.x_real_ip from edge_logs left join unnest(metadata) as m on true @@ -52,8 +52,8 @@ where h.x_real_ip is not null label: 'Requests by Country', description: 'List all ISO 3166-1 alpha-2 country codes that used the Supabase API', mode: 'custom', - searchString: `select - cf.country, + searchString: `select + cf.country, count(*) as count from edge_logs left join unnest(metadata) as m on true @@ -71,11 +71,11 @@ order by mode: 'custom', description: 'List all Supabase API requests that are slow', searchString: `select - cast(timestamp as datetime) as timestamp, + cast(timestamp as datetime) as timestamp, event_message, r.origin_time from edge_logs - cross join unnest(metadata) as m + cross join unnest(metadata) as m cross join unnest(m.response) as r where r.origin_time > 1000 @@ -90,11 +90,11 @@ limit 100 description: 'List all Supabase API requests that responded witha 5XX status code', mode: 'custom', searchString: `select - cast(timestamp as datetime) as timestamp, + cast(timestamp as datetime) as timestamp, event_message, r.status_code from edge_logs - cross join unnest(metadata) as m + cross join unnest(metadata) as m cross join unnest(m.response) as r where r.status_code >= 500 @@ -113,11 +113,11 @@ limit 100 r.search as params, count(timestamp) as c from edge_logs - cross join unnest(metadata) as m + cross join unnest(metadata) as m cross join unnest(m.request) as r -group by +group by path, - params + params order by c desc limit 100 @@ -132,7 +132,7 @@ limit 100 cast(timestamp as datetime) as timestamp, event_message from edge_logs - cross join unnest(metadata) as m + cross join unnest(metadata) as m cross join unnest(m.request) as r where path like '%rest/v1%' @@ -453,14 +453,14 @@ export const PREVIEWER_DATEPICKER_HELPERS: DatetimeHelper[] = [ calcTo: () => '', }, { - text: 'Last day', + text: 'Last 24 hours', calcFrom: () => dayjs().subtract(1, 'day').startOf('day').toISOString(), calcTo: () => '', }, ] export const EXPLORER_DATEPICKER_HELPERS: DatetimeHelper[] = [ { - text: 'Last day', + text: 'Last 24 hours', calcFrom: () => dayjs().subtract(1, 'day').startOf('day').toISOString(), calcTo: () => '', default: true, diff --git a/studio/components/interfaces/Settings/Logs/Logs.utils.ts b/studio/components/interfaces/Settings/Logs/Logs.utils.ts index eb910277bc9..22c495dd261 100644 --- a/studio/components/interfaces/Settings/Logs/Logs.utils.ts +++ b/studio/components/interfaces/Settings/Logs/Logs.utils.ts @@ -1,6 +1,7 @@ import { Filters, LogData, LogsEndpointParams, LogsTableName, SQL_FILTER_TEMPLATES } from '.' import dayjs, { Dayjs } from 'dayjs' import { get } from 'lodash' +import { StripeSubscription } from 'components/interfaces/Billing' /** * Convert a micro timestamp from number/string to iso timestamp @@ -130,10 +131,10 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => { ` case 'postgres_logs': - return `select postgres_logs.timestamp, id, event_message, parsed.error_severity from ${table} - cross join unnest(metadata) as m - cross join unnest(m.parsed) as parsed - ${where} + return `select postgres_logs.timestamp, id, event_message, parsed.error_severity from ${table} + cross join unnest(metadata) as m + cross join unnest(m.parsed) as parsed + ${where} limit 100 ` @@ -145,7 +146,7 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => { ` case 'function_edge_logs': - return `select id, ${table}.timestamp, event_message, response.status_code, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table} + return `select id, ${table}.timestamp, event_message, response.status_code, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from ${table} cross join unnest(metadata) as m cross join unnest(m.response) as response cross join unnest(m.request) as request @@ -156,7 +157,7 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => { default: return `select id, ${table}.timestamp, event_message from ${table} ${where} - limit 100 + limit 100 ` } } @@ -167,6 +168,18 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => { export const genSingleLogQuery = (table: LogsTableName, id: string) => `select id, timestamp, event_message, metadata from ${table} where id = '${id}' limit 1` +/** + * Determine if we should show the user an upgrade prompt while browsing logs + */ +export const maybeShowUpgradePrompt = ( + from: string | null | undefined, + tierKey?: StripeSubscription['tier']["key"] +) => { + const day = Math.abs(dayjs().diff(dayjs(from), 'day')) + + return (day > 1 && tierKey === 'FREE') || (day > 7 && tierKey === 'PRO') || day > 90 && tierKey === 'ENTERPRISE' +} + export const genCountQuery = (table: string): string => `SELECT count(*) as count FROM ${table}` /** calculates how much the chart start datetime should be offset given the current datetime filter params */ @@ -209,11 +222,10 @@ SELECT FROM ${table} t cross join unnest(t.metadata) as metadata - ${ - where + ${where ? where + ` and t.timestamp > '${startOffset.toISOString()}'` : `where t.timestamp > '${startOffset.toISOString()}'` - } + } GROUP BY timestamp ORDER BY diff --git a/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx b/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx index 47616568488..9b12ccac9e7 100644 --- a/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx +++ b/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react' import { useRouter } from 'next/router' -import { IconAlertCircle, IconRewind, Button, Card, Input } from 'ui' +import { IconRewind, Button } from 'ui' import { LogTable, @@ -12,6 +12,7 @@ import { LogEventChart, Filters, ensureNoTimestampConflict, + maybeShowUpgradePrompt, } from 'components/interfaces/Settings/Logs' import useLogsPreview from 'hooks/analytics/useLogsPreview' import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilterPanel' @@ -19,6 +20,9 @@ import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilte import { LOGS_TABLES } from './Logs.constants' import ShimmerLine from 'components/ui/ShimmerLine' import LoadingOpacity from 'components/ui/LoadingOpacity' +import { useProjectSubscription } from 'hooks' +import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt' +import { StripeProduct } from 'components/interfaces/Billing' import UpgradePrompt from './UpgradePrompt' /** @@ -46,8 +50,10 @@ export const LogsPreviewer: React.FC = ({ tableName, }) => { const router = useRouter() - const { s, ite, its } = router.query + const { s, ite, its, ref } = router.query const [showChart, setShowChart] = useState(true) + const { subscription } = useProjectSubscription(ref as string) + const tier = subscription?.tier const table = !tableName ? LOGS_TABLES[queryType] : tableName @@ -56,6 +62,10 @@ export const LogsPreviewer: React.FC = ({ { loadOlder, setFilters, refresh, setParams }, ] = useLogsPreview(projectRef as string, table, filterOverride) + const { showUpgradePrompt, setShowUpgradePrompt } = useUpgradePrompt( + params.iso_timestamp_start as string + ) + useEffect(() => { setFilters((prev) => ({ ...prev, search_query: s as string })) if (ite || its) { @@ -67,6 +77,16 @@ export const LogsPreviewer: React.FC = ({ } }, [s, ite, its]) + // Show the prompt on page load based on query params + useEffect(() => { + if (its) { + const shouldShowUpgradePrompt = maybeShowUpgradePrompt(its as string, tier?.key) + if (shouldShowUpgradePrompt) { + setShowUpgradePrompt(!showUpgradePrompt) + } + } + }, [its, tier]) + const onSelectTemplate = (template: LogTemplate) => { setFilters((prev: any) => ({ ...prev, search_query: template.searchString })) } @@ -109,19 +129,25 @@ export const LogsPreviewer: React.FC = ({ }, }) } else if (event === 'datepicker-change') { - setParams((prev) => ({ - ...prev, - iso_timestamp_start: from || '', - iso_timestamp_end: to || '', - })) - router.push({ - pathname: router.pathname, - query: { - ...router.query, - its: from || '', - ite: to || '', - }, - }) + const shouldShowUpgradePrompt = maybeShowUpgradePrompt(from, tier?.key) + + if (shouldShowUpgradePrompt) { + setShowUpgradePrompt(!showUpgradePrompt) + } else { + setParams((prev) => ({ + ...prev, + iso_timestamp_start: from || '', + iso_timestamp_end: to || '', + })) + router.push({ + pathname: router.pathname, + query: { + ...router.query, + its: from || '', + ite: to || '', + }, + }) + } } } @@ -198,7 +224,9 @@ export const LogsPreviewer: React.FC = ({ - +
+ +
)}
diff --git a/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx b/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx index 6f4b738c665..d29400e1a1b 100644 --- a/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx +++ b/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx @@ -51,11 +51,7 @@ const LogsQueryPanel: React.FC = ({ return (
diff --git a/studio/components/interfaces/Settings/Logs/UpgradePrompt.tsx b/studio/components/interfaces/Settings/Logs/UpgradePrompt.tsx index c0a3c28abb9..0d3ca8a64ca 100644 --- a/studio/components/interfaces/Settings/Logs/UpgradePrompt.tsx +++ b/studio/components/interfaces/Settings/Logs/UpgradePrompt.tsx @@ -1,101 +1,70 @@ -import Link from 'next/link' -import dayjs from 'dayjs' -import { useState } from 'react' -import { Button, IconHelpCircle, IconLoader, Modal } from 'ui' +import { Button, Modal } from 'ui' import { TIER_QUERY_LIMITS } from '.' -import { useProjectSubscription } from 'hooks' +import Link from 'next/link' +import { useRouter } from 'next/router' interface Props { - projectRef: string - from: string + show: boolean + setShowUpgradePrompt: (value: boolean) => void } -const UpgradePrompt: React.FC = ({ projectRef, from }) => { - const [showHelperModal, setShowHelperModal] = useState(false) - const { subscription, isLoading, isError } = useProjectSubscription(projectRef) - - if (isLoading) return - if (isError) console.error('Error fetching project subscription') - - const tier = subscription?.tier - const queryLimit = TIER_QUERY_LIMITS[(tier?.key || 'FREE') as keyof typeof TIER_QUERY_LIMITS] - - const fromValue = from ? dayjs(from) : dayjs() - const fromMax = dayjs().startOf('day').subtract(queryLimit.value, queryLimit.unit) - const isExceedingLimit = fromValue.isBefore(fromMax) +const UpgradePrompt: React.FC = ({ show, setShowUpgradePrompt }) => { + const router = useRouter() + const { ref } = router.query return ( - <> -
- {`${queryLimit.text} retention`} - setShowHelperModal(true)} - /> - {queryLimit.promptUpgrade && ( - - - - )} -
- - setShowHelperModal(false)} - > -
- -
-

- Logs can be retained up to a duration of 3 months depending on the plan that your - project is on. The table below shows an overview of the duration for which your logs - will be retained for based on each plan. -

-
-
-

Plan

-

Retention duration

+ setShowUpgradePrompt(false)} + > +
+ +
+

+ Logs can be retained up to a duration of 3 months depending on the plan that your + project is on. +

+
+
+

Plan

+

Retention duration

+
+
+
+

Free

+

{TIER_QUERY_LIMITS.FREE.text}

-
-
-

Free

-

{TIER_QUERY_LIMITS.FREE.text}

-
-
-

Pro

-

{TIER_QUERY_LIMITS.PRO.text}

-
-
-

Enterprise

-

{TIER_QUERY_LIMITS.ENTERPRISE.text}

-
+
+

Pro

+

{TIER_QUERY_LIMITS.PRO.text}

+
+
+

Enterprise

+

{TIER_QUERY_LIMITS.ENTERPRISE.text}

- - - -
- -
-
-
- - +
+
+ + +
+ + + + + +
+
+
+
) } diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts index 93b513f42d8..62b0128756c 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts @@ -127,10 +127,6 @@ const parseDescription = (description: string | null) => { } const convertPostgresDatetimeToInputDatetime = (format: string, value: string) => { - if (value) { - value = value.includes('-') ? value.replaceAll('-', '/') : value - } - if (!value || value.length == 0) return '' if (TIMESTAMP_TYPES.includes(format)) { return dayjs(value).format('YYYY-MM-DDTHH:mm:ss') diff --git a/studio/components/layouts/AccountLayout/AccountLayout.tsx b/studio/components/layouts/AccountLayout/AccountLayout.tsx index 24dfbbaa732..1bbcd7a941d 100644 --- a/studio/components/layouts/AccountLayout/AccountLayout.tsx +++ b/studio/components/layouts/AccountLayout/AccountLayout.tsx @@ -27,6 +27,7 @@ const AccountLayout: FC = ({ children, title, breadcrumbs }) => { const onClickLogout = async () => { await auth.signOut() + localStorage.clear(); router.reload() } diff --git a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 964f7f7f13d..75ffad9ef82 100644 --- a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -15,7 +15,10 @@ import { Route } from 'components/ui/ui.types' import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' export const generateProductRoutes = (ref?: string, project?: ProjectBase): Route[] => { - const isProjectBuilding = project?.status !== PROJECT_STATUS.ACTIVE_HEALTHY + const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP + const isProjectPaused = project?.status === PROJECT_STATUS.INACTIVE + + const homeUrl = `/project/${ref}` const buildingUrl = `/project/${ref}/building` return [ @@ -29,13 +32,21 @@ export const generateProductRoutes = (ref?: string, project?: ProjectBase): Rout preProcessor={(code) => code.replace(/svg/, 'svg class="m-auto text-color-inherit"')} /> ), - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/editor`), + link: + ref && + (isProjectPaused ? homeUrl : isProjectBuilding ? buildingUrl : `/project/${ref}/editor`), }, { key: 'auth', label: 'Authentication', icon: , - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/auth/users`), + link: + ref && + (isProjectPaused + ? homeUrl + : isProjectBuilding + ? buildingUrl + : `/project/${ref}/auth/users`), }, ...(IS_PLATFORM ? [ @@ -43,7 +54,13 @@ export const generateProductRoutes = (ref?: string, project?: ProjectBase): Rout key: 'storage', label: 'Storage', icon: , - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/storage/buckets`), + link: + ref && + (isProjectPaused + ? homeUrl + : isProjectBuilding + ? buildingUrl + : `/project/${ref}/storage/buckets`), }, ] : []), @@ -57,13 +74,21 @@ export const generateProductRoutes = (ref?: string, project?: ProjectBase): Rout preProcessor={(code) => code.replace(/svg/, 'svg class="m-auto text-color-inherit"')} /> ), - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/sql`), + link: + ref && + (isProjectPaused ? homeUrl : isProjectBuilding ? buildingUrl : `/project/${ref}/sql`), }, { key: 'database', label: 'Database', icon: , - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/database/tables`), + link: + ref && + (isProjectPaused + ? homeUrl + : isProjectBuilding + ? buildingUrl + : `/project/${ref}/database/tables`), }, ...(IS_PLATFORM ? [ @@ -71,7 +96,13 @@ export const generateProductRoutes = (ref?: string, project?: ProjectBase): Rout key: 'functions', label: 'Edge Functions', icon: , - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/functions`), + link: + ref && + (isProjectPaused + ? homeUrl + : isProjectBuilding + ? buildingUrl + : `/project/${ref}/functions`), }, ] : []), @@ -79,7 +110,10 @@ export const generateProductRoutes = (ref?: string, project?: ProjectBase): Rout } export const generateOtherRoutes = (ref?: string, project?: ProjectBase): Route[] => { - const isProjectBuilding = project?.status !== PROJECT_STATUS.ACTIVE_HEALTHY + const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP + const isProjectPaused = project?.status === PROJECT_STATUS.INACTIVE + + const homeUrl = `/project/${ref}` const buildingUrl = `/project/${ref}/building` return [ @@ -89,7 +123,13 @@ export const generateOtherRoutes = (ref?: string, project?: ProjectBase): Route[ key: 'logs-explorer', label: 'Logs Explorer', icon: , - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/logs-explorer`), + link: + ref && + (isProjectPaused + ? homeUrl + : isProjectBuilding + ? buildingUrl + : `/project/${ref}/logs-explorer`), }, ] : []), @@ -99,7 +139,13 @@ export const generateOtherRoutes = (ref?: string, project?: ProjectBase): Route[ key: 'reports', label: 'Reports', icon: , - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/reports`), + link: + ref && + (isProjectPaused + ? homeUrl + : isProjectBuilding + ? buildingUrl + : `/project/${ref}/reports`), }, ] : []), @@ -107,7 +153,9 @@ export const generateOtherRoutes = (ref?: string, project?: ProjectBase): Route[ key: 'api', label: 'API', icon: , - link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/api`), + link: + ref && + (isProjectPaused ? homeUrl : isProjectBuilding ? buildingUrl : `/project/${ref}/api`), }, ...(IS_PLATFORM ? [ diff --git a/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx b/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx index b344e3509f4..a6e40267b34 100644 --- a/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx +++ b/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx @@ -1,12 +1,12 @@ import { FC, useState } from 'react' import * as Tooltip from '@radix-ui/react-tooltip' -import { Alert, Button, IconPauseCircle } from 'ui' +import { Modal, Button, IconPauseCircle } from 'ui' import { PermissionAction } from '@supabase/shared-types/out/constants' import { Project } from 'types' -import { checkPermissions, useStore, useSubscriptionStats, useFlag } from 'hooks' +import { checkPermissions, useStore, useFlag, useFreeProjectLimitCheck } from 'hooks' import { post } from 'lib/common/fetch' -import { API_URL, PROJECT_STATUS, DEFAULT_FREE_PROJECTS_LIMIT } from 'lib/constants' +import { API_URL, PROJECT_STATUS } from 'lib/constants' import { DeleteProjectButton } from 'components/interfaces/Settings/General' import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' @@ -16,16 +16,17 @@ interface Props { const ProjectPausedState: FC = ({ project }) => { const { ui, app } = useStore() - const subscriptionStats = useSubscriptionStats() - const { total_active_free_projects: totalActiveFreeProjects } = subscriptionStats - const freeProjectsLimit = ui.profile?.free_project_limit ?? DEFAULT_FREE_PROJECTS_LIMIT - - const enablePermissions = useFlag('enablePermissions') - const kpsEnabled = useFlag('initWithKps') const isOwner = ui.selectedOrganization?.is_owner + const orgSlug = ui.selectedOrganization?.slug + + const kpsEnabled = useFlag('initWithKps') + const enablePermissions = useFlag('enablePermissions') const [showConfirmRestore, setShowConfirmRestore] = useState(false) - const hasExceedActiveFreeProjectsLimit = totalActiveFreeProjects >= freeProjectsLimit + const [showFreeProjectLimitWarning, setShowFreeProjectLimitWarning] = useState(false) + + const { membersExceededLimit } = useFreeProjectLimitCheck(orgSlug) + const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 const canResumeProject = enablePermissions ? checkPermissions(PermissionAction.INFRA_EXECUTE, 'queue_jobs.projects.initialize_or_resume') @@ -33,12 +34,12 @@ const ProjectPausedState: FC = ({ project }) => { const onSelectRestore = () => { if (!canResumeProject) { - return ui.setNotification({ + ui.setNotification({ category: 'error', message: 'You do not have the required permissions to restore this project', }) - } - setShowConfirmRestore(true) + } else if (hasMembersExceedingFreeTierLimit) setShowFreeProjectLimitWarning(true) + else setShowConfirmRestore(true) } const onConfirmRestore = async () => { @@ -50,17 +51,6 @@ const ProjectPausedState: FC = ({ project }) => { return ( <>
- {hasExceedActiveFreeProjectsLimit && ( -
- - To restore this project you'll need to pause or delete an existing free project. - -
- )}
@@ -76,7 +66,7 @@ const ProjectPausedState: FC = ({ project }) => {
- {!hasExceedActiveFreeProjectsLimit && ( -

- Restore this project and get back to building the next big thing! -

- )} +

+ Restore this project and get back to building the next big thing! +

@@ -119,6 +107,49 @@ const ProjectPausedState: FC = ({ project }) => { onSelectCancel={() => setShowConfirmRestore(false)} onSelectConfirm={onConfirmRestore} /> + setShowFreeProjectLimitWarning(false)} + > +
+ +
+

+ The following members have reached their maximum limits for the number of active + free tier projects within organizations where they are an administrator or owner: +

+
    + {(membersExceededLimit || []).map((member, idx: number) => ( +
  • + {member.username || member.primary_email} (Limit: {member.free_project_limit}{' '} + free projects) +
  • + ))} +
+

+ These members will need to either delete, pause, or upgrade one or more of these + projects before you're able to unpause this project. +

+
+
+ + +
+ +
+
+
+
) } diff --git a/studio/hooks/misc/useUpgradePrompt.tsx b/studio/hooks/misc/useUpgradePrompt.tsx new file mode 100644 index 00000000000..71aa956ca23 --- /dev/null +++ b/studio/hooks/misc/useUpgradePrompt.tsx @@ -0,0 +1,23 @@ +import { useEffect, useState } from 'react' +import { useProjectSubscription } from 'hooks' +import { useRouter } from 'next/router' +import { maybeShowUpgradePrompt } from 'components/interfaces/Settings/Logs' +import { StripeProduct } from 'components/interfaces/Billing' + +export const useUpgradePrompt = (from: string) => { + const router = useRouter() + const { ref } = router.query + const { subscription } = useProjectSubscription(ref as string) + const tier = subscription?.tier + const [showUpgradePrompt, setShowUpgradePrompt] = useState(false) + + const shouldShowUpgradePrompt = maybeShowUpgradePrompt(from, tier?.key) + + useEffect(() => { + if (shouldShowUpgradePrompt) { + setShowUpgradePrompt(true) + } + }, [from]) + + return { showUpgradePrompt, setShowUpgradePrompt, shouldShowUpgradePrompt } +} diff --git a/studio/hooks/queries/index.ts b/studio/hooks/queries/index.ts index 2964ef05c56..53c6ca96435 100644 --- a/studio/hooks/queries/index.ts +++ b/studio/hooks/queries/index.ts @@ -1,6 +1,7 @@ export * from './useJwtSecretUpdateStatus' export * from './useOrganizationDetail' export * from './useOrganizationRoles' +export * from './useFreeProjectLimitCheck' export * from './usePermissions' export * from './useProfile' export * from './useProjectAuthConfig' diff --git a/studio/hooks/queries/useFreeProjectLimitCheck.ts b/studio/hooks/queries/useFreeProjectLimitCheck.ts new file mode 100644 index 00000000000..3884388ebdd --- /dev/null +++ b/studio/hooks/queries/useFreeProjectLimitCheck.ts @@ -0,0 +1,22 @@ +import useSWR from 'swr' +import { get } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' + +export interface MemberWithFreeProjectLimit { + free_project_limit: number + primary_email: string + username: string +} + +export function useFreeProjectLimitCheck(slug?: string) { + const url = `${API_URL}/organizations/${slug}/members/reached-free-project-limit` + const { data, error } = useSWR(slug ? url : null, get) + const anyError = data?.error || error + + return { + membersExceededLimit: anyError ? undefined : (data as MemberWithFreeProjectLimit[]), + error: anyError, + isLoading: !anyError && !data, + isError: !!anyError, + } +} diff --git a/studio/pages/new/[slug].tsx b/studio/pages/new/[slug].tsx index 5785fe05861..a19a5d656af 100644 --- a/studio/pages/new/[slug].tsx +++ b/studio/pages/new/[slug].tsx @@ -4,7 +4,7 @@ import { debounce, isUndefined, values } from 'lodash' import { toJS } from 'mobx' import { observer } from 'mobx-react-lite' import generator from 'generate-password' -import { Button, Listbox, IconUsers, Input, IconLoader, Alert } from 'ui' +import { Button, Listbox, IconUsers, Input, Alert } from 'ui' import { PermissionAction } from '@supabase/shared-types/out/constants' import { NextPageWithLayout } from 'types' @@ -19,10 +19,16 @@ import { PRICING_TIER_LABELS, PRICING_TIER_DEFAULT_KEY, PRICING_TIER_FREE_KEY, - DEFAULT_FREE_PROJECTS_LIMIT, PRICING_TIER_PRODUCT_IDS, } from 'lib/constants' -import { useStore, useFlag, withAuth, useSubscriptionStats, checkPermissions } from 'hooks' +import { + useStore, + useFlag, + withAuth, + useSubscriptionStats, + checkPermissions, + useFreeProjectLimitCheck, +} from 'hooks' import { WizardLayoutWithoutAuth } from 'components/layouts' import Panel from 'components/ui/Panel' @@ -43,6 +49,8 @@ const Wizard: NextPageWithLayout = () => { const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate') const kpsEnabled = useFlag('initWithKps') const subscriptionStats = useSubscriptionStats() + const { membersExceededLimit, isLoading: isLoadingFreeProjectLimitCheck } = + useFreeProjectLimitCheck(slug as string) const [projectName, setProjectName] = useState('') const [dbPass, setDbPass] = useState('') @@ -59,31 +67,29 @@ const Wizard: NextPageWithLayout = () => { const stripeCustomerId = currentOrg?.stripe_customer_id const isOrganizationOwner = ui.selectedOrganization?.is_owner - const totalFreeProjects = subscriptionStats.total_active_free_projects - const freeProjectsLimit = ui.profile?.free_project_limit ?? DEFAULT_FREE_PROJECTS_LIMIT const availableRegions = getAvailableRegions() const isAdmin = enablePermissions ? checkPermissions(PermissionAction.CREATE, 'projects') : isOrganizationOwner + const isInvalidSlug = isUndefined(currentOrg) const isEmptyOrganizations = organizations.length <= 0 && app.organizations.isInitialized const isEmptyPaymentMethod = paymentMethods ? !paymentMethods.length : false - const isOverFreeProjectLimit = totalFreeProjects >= freeProjectsLimit - const isInvalidSlug = isUndefined(currentOrg) const isSelectFreeTier = dbPricingTierKey === PRICING_TIER_FREE_KEY + const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 const canCreateProject = isAdmin && !subscriptionStats.isError && !subscriptionStats.isLoading && - (!isSelectFreeTier || (isSelectFreeTier && !isOverFreeProjectLimit)) + (!isSelectFreeTier || (isSelectFreeTier && !hasMembersExceedingFreeTierLimit)) const canSubmit = - projectName != '' && + projectName !== '' && passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH && - dbRegion != '' && - dbPricingTierKey != '' && + dbRegion !== '' && + dbPricingTierKey !== '' && (isSelectFreeTier || (!isSelectFreeTier && !isEmptyPaymentMethod)) const delayedCheckPasswordStrength = useRef( @@ -202,7 +208,11 @@ const Wizard: NextPageWithLayout = () => { return (

Create a new project

@@ -243,7 +253,12 @@ const Wizard: NextPageWithLayout = () => { ) : ( <> - + {organizations.length > 0 && ( { {!isAdmin && } + {canCreateProject && ( <> - + { )} + {isAdmin && ( { })} - {isSelectFreeTier && isOverFreeProjectLimit && ( - + {isSelectFreeTier && hasMembersExceedingFreeTierLimit && ( + )} {!isSelectFreeTier && isEmptyPaymentMethod && ( @@ -385,13 +407,6 @@ const Wizard: NextPageWithLayout = () => { )} )} - {subscriptionStats.isLoading && ( - -
- -
-
- )} )} diff --git a/studio/pages/project/[ref]/logs-explorer/index.tsx b/studio/pages/project/[ref]/logs-explorer/index.tsx index e4db358c99a..c4bd0350289 100644 --- a/studio/pages/project/[ref]/logs-explorer/index.tsx +++ b/studio/pages/project/[ref]/logs-explorer/index.tsx @@ -5,7 +5,7 @@ import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' import { Input, Modal, Form, Button } from 'ui' -import { useStore } from 'hooks' +import { useProjectSubscription, useStore } from 'hooks' import useLogsQuery from 'hooks/analytics/useLogsQuery' import { NextPageWithLayout, UserContent } from 'types' import { uuidv4 } from 'lib/helpers' @@ -21,8 +21,11 @@ import { LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD, LogTable, LogTemplate, + maybeShowUpgradePrompt, TEMPLATES, } from 'components/interfaces/Settings/Logs' +import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt' +import { StripeProduct } from 'components/interfaces/Billing' import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt' export const LogsExplorerPage: NextPageWithLayout = () => { @@ -33,6 +36,8 @@ export const LogsExplorerPage: NextPageWithLayout = () => { const [saveModalOpen, setSaveModalOpen] = useState(false) const [warnings, setWarnings] = useState([]) const { content } = useStore() + const { subscription } = useProjectSubscription(ref as string) + const tier = subscription?.tier const [{ params, logData, error, isLoading }, { changeQuery, runQuery, setParams }] = useLogsQuery(ref as string, { @@ -40,6 +45,10 @@ export const LogsExplorerPage: NextPageWithLayout = () => { iso_timestamp_end: ite ? (ite as string) : undefined, }) + const { showUpgradePrompt, setShowUpgradePrompt } = useUpgradePrompt( + params.iso_timestamp_start as string + ) + useEffect(() => { // on mount, set initial values if (q) { @@ -65,6 +74,16 @@ export const LogsExplorerPage: NextPageWithLayout = () => { setWarnings(newWarnings) }, [editorValue, params.iso_timestamp_start, params.iso_timestamp_end]) + // Show the prompt on page load based on query params + useEffect(() => { + if (its) { + const shouldShowUpgradePrompt = maybeShowUpgradePrompt(its as string, tier?.key) + if (shouldShowUpgradePrompt) { + setShowUpgradePrompt(!showUpgradePrompt) + } + } + }, [its, tier]) + const onSelectTemplate = (template: LogTemplate) => { setEditorValue(template.searchString) changeQuery(template.searchString) @@ -110,15 +129,21 @@ export const LogsExplorerPage: NextPageWithLayout = () => { } const handleDateChange = ({ to, from }: DatePickerToFrom) => { - setParams((prev) => ({ - ...prev, - iso_timestamp_start: from || '', - iso_timestamp_end: to || '', - })) - router.push({ - pathname: router.pathname, - query: { ...router.query, its: from || '', ite: to || '' }, - }) + const shouldShowUpgradePrompt = maybeShowUpgradePrompt(from, tier?.key) + + if (shouldShowUpgradePrompt) { + setShowUpgradePrompt(!showUpgradePrompt) + } else { + setParams((prev) => ({ + ...prev, + iso_timestamp_start: from || '', + iso_timestamp_end: to || '', + })) + router.push({ + pathname: router.pathname, + query: { ...router.query, its: from || '', ite: to || '' }, + }) + } } return ( @@ -139,7 +164,6 @@ export const LogsExplorerPage: NextPageWithLayout = () => { isLoading={isLoading} warnings={warnings} /> -
{
- +
diff --git a/studio/pages/project/[ref]/settings/billing/update/index.tsx b/studio/pages/project/[ref]/settings/billing/update/index.tsx index dca71d855e0..a7a0587251d 100644 --- a/studio/pages/project/[ref]/settings/billing/update/index.tsx +++ b/studio/pages/project/[ref]/settings/billing/update/index.tsx @@ -3,14 +3,9 @@ import { useRouter } from 'next/router' import { observer } from 'mobx-react-lite' import { Button, Modal } from 'ui' -import { useStore, useSubscriptionStats } from 'hooks' +import { useFreeProjectLimitCheck, useStore } from 'hooks' import { get } from 'lib/common/fetch' -import { - API_URL, - DEFAULT_FREE_PROJECTS_LIMIT, - PRICING_TIER_PRODUCT_IDS, - STRIPE_PRODUCT_IDS, -} from 'lib/constants' +import { API_URL, PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants' import { BillingLayout } from 'components/layouts' import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' @@ -21,11 +16,11 @@ import { NextPageWithLayout } from 'types' const BillingUpdate: NextPageWithLayout = () => { const { ui } = useStore() const router = useRouter() + const orgSlug = ui.selectedOrganization?.slug const projectRef = ui.selectedProject?.ref - const subscriptionStats = useSubscriptionStats() - const freeProjectsLimit = ui?.profile?.free_project_limit ?? DEFAULT_FREE_PROJECTS_LIMIT - const freeProjectsOwned = subscriptionStats.total_active_free_projects ?? 0 + const { membersExceededLimit } = useFreeProjectLimitCheck(orgSlug as string) + const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 const [isLoadingProducts, setIsLoadingProducts] = useState(false) const [showConfirmDowngrade, setShowConfirmDowngrade] = useState(false) @@ -97,7 +92,7 @@ const BillingUpdate: NextPageWithLayout = () => { } const onConfirmDowngrade = () => { - if (freeProjectsOwned >= freeProjectsLimit) { + if (hasMembersExceedingFreeTierLimit) { setShowDowngradeError(true) setShowConfirmDowngrade(false) setSelectedPlan(undefined) @@ -143,17 +138,30 @@ const BillingUpdate: NextPageWithLayout = () => { setShowDowngradeError(false)} >
-

- Your account is entitled up to {freeProjectsLimit} free projects across all - organizations you own. You will need to delete or upgrade an existing free project - first, before being able to downgrade this project. -

+
+

+ The following members have reached their maximum limits for the number of active + free tier projects within organizations where they are an administrator or owner: +

+
    + {(membersExceededLimit || []).map((member, idx: number) => ( +
  • + {member.username || member.primary_email} (Limit: {member.free_project_limit}{' '} + free projects) +
  • + ))} +
+

+ These members will need to either delete, pause, or upgrade one or more of these + projects before you're able to downgrade this project. +

+
diff --git a/studio/tests/pages/projects/LogsPreviewer.test.js b/studio/tests/pages/projects/LogsPreviewer.test.js index 6e4a6722dc0..86cc684f691 100644 --- a/studio/tests/pages/projects/LogsPreviewer.test.js +++ b/studio/tests/pages/projects/LogsPreviewer.test.js @@ -393,3 +393,28 @@ test('filters accept filterOverride', async () => { expect(get).toHaveBeenCalledWith(expect.stringContaining('myvalue')) }) }) + +describe.each(['FREE', 'PRO', 'ENTERPRISE'])('upgrade modal for %s', (key) => { + beforeEach(() => { + useProjectSubscription.mockReturnValue({ + subscription: { + tier: { + supabase_prod_id: `tier_${key.toLocaleLowerCase()}`, + key, + }, + }, + }) + }) + test('based on query params', async () => { + const router = defaultRouterMock() + router.query = { + ...router.query, + q: 'some_query', + its: dayjs().subtract(4, 'months').toISOString(), + ite: dayjs().toISOString(), + } + useRouter.mockReturnValue(router) + render() + await screen.findByText('Log retention') // assert modal title is present + }) +}) diff --git a/studio/tests/pages/projects/logs-query.test.js b/studio/tests/pages/projects/logs-query.test.js index 84d63e92f85..562616fa426 100644 --- a/studio/tests/pages/projects/logs-query.test.js +++ b/studio/tests/pages/projects/logs-query.test.js @@ -7,6 +7,8 @@ import userEvent from '@testing-library/user-event' import { logDataFixture } from '../../fixtures' import { clickDropdown } from 'tests/helpers' import dayjs from 'dayjs' +import { useProjectSubscription } from 'hooks' +import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt' const defaultRouterMock = () => { const router = jest.fn() @@ -137,21 +139,6 @@ test('custom sql querying', async () => { await expect(screen.findByText(/Load older/)).rejects.toThrow() }) -test('datepicker interaction updates query params', async () => { - render() - clickDropdown(await screen.findByText(/Last day/)) - userEvent.click(await screen.findByText(/Last 3 days/)) - - const router = useRouter() - expect(router.push).toBeCalledWith( - expect.objectContaining({ - query: expect.objectContaining({ - its: expect.any(String), - }), - }) - ) -}) - test('query warnings', async () => { const router = defaultRouterMock() router.query = { @@ -164,3 +151,42 @@ test('query warnings', async () => { render() await screen.findByText('1 warning') }) + +describe.each(['FREE', 'PRO', 'ENTERPRISE'])('upgrade modal for %s', (key) => { + beforeEach(() => { + useProjectSubscription.mockReturnValue({ + subscription: { + tier: { + supabase_prod_id: `tier_${key.toLocaleLowerCase()}`, + key, + }, + }, + }) + }) + test('based on query params', async () => { + const router = defaultRouterMock() + router.query = { + ...router.query, + q: 'some_query', + its: dayjs().subtract(5, 'month').toISOString(), + ite: dayjs().toISOString(), + } + useRouter.mockReturnValue(router) + render() + await screen.findByText(/Log retention/) // assert modal title is present + }) + + test('based on datepicker helpers', async () => { + render() + // click on the dropdown + clickDropdown(await screen.findByText('Last 24 hours')) + userEvent.click(await screen.findByText('Last 3 days')) + + // only free tier will show modal + if (key === 'FREE') { + await screen.findByText('Log retention') // assert modal title is present + } else { + await expect(screen.findByText('Log retention')).rejects.toThrow() + } + }) +}) diff --git a/tests/.jest/teardown.ts b/tests/.jest/teardown.ts index 6ef6f4bc7c6..afb2b1cb2c9 100644 --- a/tests/.jest/teardown.ts +++ b/tests/.jest/teardown.ts @@ -1,20 +1,21 @@ -import { ApiError, createClient, User } from '@supabase/supabase-js' +import { createClient, User, UserResponse } from '@supabase/supabase-js' const removeAllUsers = async () => { - const sb = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const sb = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_KEY_ADMIN as string + ) - const { data: users } = await sb.auth.api.listUsers() + const { + data: { users }, + } = await sb.auth.admin.listUsers() - const promises: Promise<{ - user: User - data: User - error: ApiError - }>[] = [] + const promises: Promise[] = [] users.map((u) => { sb.from('profiles') .delete() .match({ id: u.id }) - .then(() => promises.push(sb.auth.api.deleteUser(u.id))) + .then(() => promises.push(sb.auth.admin.deleteUser(u.id))) }) await Promise.all(promises) } diff --git a/tests/data/func.ts b/tests/data/func.ts new file mode 100644 index 00000000000..37809150683 --- /dev/null +++ b/tests/data/func.ts @@ -0,0 +1,12 @@ +import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' + +console.log('Hello from Functions!') + +serve(async (req) => { + const { name } = await req.json() + const data = { + message: `Hello ${name}!`, + } + + return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } }) +}) diff --git a/tests/features/javascript/authAdmin.spec.ts b/tests/features/javascript/authAdmin.spec.ts new file mode 100644 index 00000000000..57b55cf495f --- /dev/null +++ b/tests/features/javascript/authAdmin.spec.ts @@ -0,0 +1,319 @@ +import { params, retries, suite, test } from '@testdeck/jest' +import { faker } from '@faker-js/faker' +import { Severity } from 'allure-js-commons' + +import { AdminUserAttributes, AuthError, SupabaseClient, UserResponse } from '@supabase/supabase-js' + +import { FEATURE } from '../templates/enums' +import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' +import { Hooks } from './hooks' + +@suite('auth_admin') +class AuthenticationAPI extends Hooks { + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you create user then it has to be in auth db schema') + @test + async 'create user via admin api'() { + const { user, error } = await this.createUserAsAdmin() + expect(error).toBeNull() + expect(user).not.toBeNull() + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you create user then he can sign in') + @test + async 'user created by admin can login'() { + const { user, error } = await this.createUserAsAdmin() + expect(error).toBeNull() + expect(user).not.toBeNull() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + + const { + data: { user: createdUser }, + error: getErr, + } = await supabase.auth.signInWithPassword({ + email: user.email, + password: user.password, + }) + expect(getErr).toBeNull() + expect(createdUser).not.toBeNull() + expect(createdUser.id).toBe(user.id) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.CRITICAL) + @description('When you try to create user with anon key then you should get error') + @test + async 'admin create user with anon key should fail'() { + const fakeUser = { + email: faker.internet.exampleEmail(), + password: faker.internet.password(), + email_confirm: true, + } + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + + const { + error, + data: { user }, + } = await supabase.auth.admin.createUser(fakeUser) + expect(user).toBeNull() + expect(error).not.toBeNull() + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.CRITICAL) + @description('When you try to create user as logged in user then you should get error') + @test + async 'admin create user with logged in user should fail'() { + const { user } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { error: signInError } = await supabase.auth.signInWithPassword({ + email: user.email, + password: user.password, + }) + expect(signInError).toBeNull() + + const fakeUser = { + email: faker.internet.exampleEmail(), + password: faker.internet.password(), + } + const { + error, + data: { user: newUser }, + } = await supabase.auth.admin.createUser(fakeUser) + expect(newUser).toBeNull() + expect(error).not.toBeNull() + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you list users then you should get all users') + @test + async 'list users with service key'() { + const { user: user1 } = await this.createUserAsAdmin() + const { user: user2 } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const { + data: { users }, + error, + } = await supabase.auth.admin.listUsers() + + expect(error).toBeNull() + expect(users).not.toBeNull() + expect(users.length).toBeGreaterThanOrEqual(2) + expect(users.map((u) => u.id)).toEqual(expect.arrayContaining([user1.id, user2.id])) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.CRITICAL) + @description('When you try to list user with anon key then you should get error') + @test + async 'list users with anon key should fail'() { + await this.createUserAsAdmin() + await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { + data: { users }, + error, + } = await supabase.auth.admin.listUsers() + + expect(error).not.toBeNull() + expect(users).toHaveLength(0) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.CRITICAL) + @description('When you try to list user as logged in user then you should get error') + @test + async 'list users as logged in user should fail'() { + const { user } = await this.createUserAsAdmin() + await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { error: signInError } = await supabase.auth.signInWithPassword({ + email: user.email, + password: user.password, + }) + expect(signInError).toBeNull() + + const { + data: { users }, + error, + } = await supabase.auth.admin.listUsers() + expect(error).not.toBeNull() + expect(users).toHaveLength(0) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you get provider url then corresponding auth provider url should be returned') + @params.skip({ provider: 'google', options: {}, expectedURL: 'todo' }) + @params.skip({ + provider: 'google', + options: { redirectTo: 'todo', scopes: 'todo' }, + expectedURL: 'todo', + }) + @params.skip({ provider: 'twitter', options: {}, expectedURL: 'todo' }) + // ... + async 'get url for provider'() { + // todo + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you get user by id he has to be returned') + @test + async 'get user should work'() { + const { user } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const { + data: { user: foundUser }, + error, + } = await supabase.auth.admin.getUserById(user.id) + + expect(error).toBeNull() + expect(foundUser).not.toBeNull() + expect(foundUser.id).toBe(user.id) + expect(foundUser.email).toBe(user.email) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you update user then this user has to be updated') + @test + async 'update user should work'() { + const { user } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + + const updatedUser = { + email: faker.internet.exampleEmail(), + phone: faker.phone.phoneNumber('!#!##!######'), + } + let { + data: { user: resultUser }, + error, + } = await this.updateWithRetries(supabase, user.id, updatedUser) + + expect(error).toBeNull() + expect(resultUser).not.toBeNull() + expect(resultUser.id).toBe(user.id) + expect(resultUser.email).toBe(updatedUser.email) + expect(resultUser.phone).toBe(updatedUser.phone) + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.NORMAL) + @description('When you delete user then this user has to be removed') + @test + async 'delete user should work'() { + const { user } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const { + data: { user: deletedUser }, + error, + } = await supabase.auth.admin.deleteUser(user.id) + expect(error).toBeNull() + + const { + data: { user: foundUser }, + error: getError, + } = await supabase.auth.admin.getUserById(user.id) + expect(getError).not.toBeNull() + expect(foundUser).toBeNull() + } + + @feature(FEATURE.AUTH_ADMIN) + @severity(Severity.CRITICAL) + @description('When you delete user with anon key you have to receive an error') + @test + async 'delete user with anon key should fail'() { + const { user } = await this.createUserAsAdmin() + const { user: villain } = await this.createUserAsAdmin() + + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + await supabase.auth.signInWithPassword({ + email: villain.email, + password: villain.password, + }) + + const { + data: { user: deletedUser }, + error, + } = await supabase.auth.admin.deleteUser(user.id) + + expect(error).not.toBeNull() + expect(deletedUser).toBeNull() + + const sbAdmin = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const { + data: { user: foundUser }, + error: getError, + } = await sbAdmin.auth.admin.getUserById(user.id) + expect(getError).toBeNull() + expect(foundUser).not.toBeNull() + expect(foundUser.email).toBe(user.email) + } + + @step('Create a user as admin') + async createUserAsAdmin(data: AdminUserAttributes = undefined): Promise<{ + user: { + email: string + password: string + username: string + id: string + } + error: AuthError + }> { + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + + let fakeUser: AdminUserAttributes + if (data) { + fakeUser = data + } else { + fakeUser = { + email: faker.internet.exampleEmail(), + password: faker.internet.password(), + email_confirm: true, + } + } + + const { + error, + data: { user }, + } = await supabase.auth.admin.createUser(fakeUser) + + return { + error: error, + user: { + email: user?.email, + password: fakeUser.password, + username: faker.internet.userName(), + id: user?.id, + }, + } + } + + @step('Update user with retries') + async updateWithRetries(supabase: SupabaseClient, uid: string, attributes: AdminUserAttributes) { + let result: UserResponse + for (let i = 1; i < 5; i++) { + result = await supabase.auth.admin.updateUserById(uid, attributes) + + if (result.error && result.error.name === 'AuthRetryableFetchError') { + await new Promise((resolve) => setTimeout(resolve, 1000 * i)) + } else { + break + } + } + return result + } +} diff --git a/tests/features/javascript/authentication.spec.ts b/tests/features/javascript/authentication.spec.ts index 6b19db8b198..c63af999c2c 100644 --- a/tests/features/javascript/authentication.spec.ts +++ b/tests/features/javascript/authentication.spec.ts @@ -1,8 +1,8 @@ -import { suite, test } from '@testdeck/jest' +import { suite, test, timeout } from '@testdeck/jest' import { faker } from '@faker-js/faker' import { Severity } from 'allure-js-commons' -import { ApiError, Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' +import { AuthChangeEvent, Session } from '@supabase/supabase-js' import { FEATURE } from '../templates/enums' import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' @@ -22,7 +22,10 @@ class Authentication extends Hooks { password: faker.internet.password(), username: faker.internet.userName(), } - const { user, session, error: signUpError } = await this.signUp(supabase, fakeUser) + const { + data: { user, session }, + error: signUpError, + } = await this.signUp(supabase, fakeUser) expect(signUpError).toBeNull() expect(user).toBeDefined() @@ -37,7 +40,7 @@ class Authentication extends Hooks { @severity(Severity.BLOCKER) @description('When user sign up then he should not be logged in until he confirms his email') @test - async 'new users'() { + async 'sing up new user and sign in'() { // sign up user const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) @@ -47,8 +50,7 @@ class Authentication extends Hooks { username: faker.internet.userName(), } const { - user, - session: emptySession, + data: { user, session: emptySession }, error: signUpError, } = await this.signUp(supabase, fakeUser) @@ -57,15 +59,22 @@ class Authentication extends Hooks { expect(user.email).toEqual(fakeUser.email.toLowerCase()) expect(emptySession).not.toBeNull() - const { session, error: signInError } = await supabase.auth.signIn({ + const { + data: { session }, + error: signInError, + } = await supabase.auth.signInWithPassword({ email: fakeUser.email, password: fakeUser.password, }) expect(signInError).toBeNull() expect(session).toBeDefined() + await supabase.auth.setSession(session) // check if user is signed in - const { data: profile, error: errorInsert } = await this.insertProfile(supabase, user, fakeUser) + const { + data: [profile], + error: errorInsert, + } = await this.insertProfile(supabase, user, fakeUser) expect(errorInsert).toBeNull() expect(profile.username).toMatch(fakeUser.username) @@ -81,7 +90,7 @@ class Authentication extends Hooks { @severity(Severity.BLOCKER) @description('When user sign up with phone then he should be logged in') @test - async 'new users by phone'() { + async 'create new users by phone auth'() { // sign up user const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) @@ -90,7 +99,10 @@ class Authentication extends Hooks { username: faker.internet.userName(), phone: faker.phone.phoneNumber('!#!##!######'), } - const { user, session, error: signUpError } = await this.signUpByPhone(supabase, fakeUser) + const { + data: { user, session }, + error: signUpError, + } = await this.signUpByPhone(supabase, fakeUser) expect(signUpError).toBeNull() expect(user).toBeDefined() @@ -98,7 +110,10 @@ class Authentication extends Hooks { expect(session).toBeDefined() // check if user is signed in - const { data: profile, error: errorInsert } = await this.insertProfile(supabase, user, fakeUser) + const { + data: [profile], + error: errorInsert, + } = await this.insertProfile(supabase, user, fakeUser) expect(errorInsert).toBeNull() expect(profile.username).toMatch(fakeUser.username) @@ -114,17 +129,16 @@ class Authentication extends Hooks { @severity(Severity.BLOCKER) @description('When user is already signed up then he should be able to log in') @test - async 'existing users'() { + async 'existing users should be able to login'() { // create user const fakeUser = await this.createUser() // sign in as user const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) const { - session, - user, + data: { session, user }, error: signInError, - } = await supabase.auth.signIn({ + } = await supabase.auth.signInWithPassword({ email: fakeUser.email, password: fakeUser.password, }) @@ -135,11 +149,10 @@ class Authentication extends Hooks { expect(user.email).toEqual(fakeUser.email.toLowerCase()) // check if user is signed in correctly and rls is working - const { data: profileInserted, error: errorInsert } = await this.insertProfile( - supabase, - user, - fakeUser - ) + const { + data: [profileInserted], + error: errorInsert, + } = await this.insertProfile(supabase, user, fakeUser) expect(errorInsert).toBeNull() expect(profileInserted.username).toMatch(fakeUser.username) @@ -155,7 +168,7 @@ class Authentication extends Hooks { @severity(Severity.NORMAL) @description('When user is signed in then he should be able to get his info and metadata') @test - async 'get user'() { + async 'get user should return logged in user'() { // create user const username = faker.internet.userName() const date = faker.date.recent().toUTCString() @@ -166,16 +179,20 @@ class Authentication extends Hooks { // sign in as user const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) - await supabase.auth.signIn({ + await supabase.auth.signInWithPassword({ email: fakeUser.email, password: fakeUser.password, }) // get signed in user data - const user = this.getUser(supabase) + const { + data: { user }, + error: getUserErr, + } = await this.getUser(supabase) log('Check if user is signed in correctly and can get his data') - expect(user).toBeDefined() + expect(getUserErr).toBeNull() + expect(user).not.toBeNull() expect(user.email).toEqual(fakeUser.email.toLowerCase()) expect(user.role).toEqual('authenticated') expect(user.aud).toEqual('authenticated') @@ -190,128 +207,124 @@ class Authentication extends Hooks { @severity(Severity.NORMAL) @description('When user is signed in then he should be able update himself in auth schema') @test.skip - async 'update user'() { + async 'update user should update logged in user'() { // create user const fakeUser = await this.createUser() // sign in as user const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) - await supabase.auth.signIn({ + await supabase.auth.signInWithPassword({ email: fakeUser.email, password: fakeUser.password, }) // get signed in user data - const user = this.getUser(supabase) + const user = await this.getUser(supabase) // update user - // todo update params + const updParams = { + email: faker.internet.email(), + password: faker.internet.password(), + phone: faker.phone.phoneNumber('!#!##!######'), + } const { - user: updUser, - data: updUserData, + data: { user: updUser }, error: updUserError, - } = await this.updateUser(supabase, {}) + } = await this.updateUser(supabase, updParams) + expect(updUserError).toBeNull() + expect(updUser).not.toBeNull() + expect(updUser.email).toEqual(updParams.email.toLowerCase()) + expect(updUser.phone).toEqual(updParams.phone) - // todo check if returned user is updated + // get user and check it was updated + const updatedUser = await this.getUser(supabase) + expect(updatedUser.data.user.email).toEqual(updParams.email.toLowerCase()) + expect(updatedUser.data.user.phone).toEqual(updParams.phone) - const updatedUser = this.getUser(supabase) - - // todo check if user is updated on backend + // sign in with new credentials + await supabase.auth.signOut() + const signIn = await supabase.auth.signInWithPassword({ + email: updParams.email, + password: updParams.password, + }) + expect(signIn.error).toBeNull() } @feature(FEATURE.AUTHENTICATION) @severity(Severity.NORMAL) @description('When user changes session then he still should be correctly logined') - @test.skip - async 'set session'() { - // todo - } + @test + async 'set session should set new auth'() { + // create user + const fakeUser = await this.createUser() - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When user changes auth then all new requests should have new JWT') - @test.skip - async 'set auth'() { - // todo - } + // sign in as user + const sb = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { + data: { session }, + } = await sb.auth.signInWithPassword({ + email: fakeUser.email, + password: fakeUser.password, + }) - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When user refreshes session then user and session have to be updated') - @test.skip - async 'refresh session'() { - // todo + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { error: sessionErr } = await supabase.auth.setSession(session) + expect(sessionErr).toBeNull() + + // check if user is signed in correctly and rls is working + const { data: profileInserted, error: errorInsert } = await this.insertProfile( + supabase, + fakeUser, + fakeUser + ) + expect(errorInsert).toBeNull() + expect(profileInserted).toHaveLength(1) + expect(profileInserted[0].username).toMatch(fakeUser.username) } @feature(FEATURE.AUTHENTICATION) @severity(Severity.NORMAL) @description('When user subscribes on auth changes then user has to receive auth updates') - @test.skip - async 'on auth state changed'() { - // todo - } - - // steps - - @step('I sign up with a valid email and password') - private async signUpByPhone( - supabase: SupabaseClient, - { - phone = faker.phone.phoneNumber(), - password = faker.internet.password(), - }: { - phone?: string - password?: string - } = {}, - options: { - redirectTo?: string - data?: object - } = {} - ): Promise<{ - user: User - session: Session - error: ApiError - }> { - return supabase.auth.signUp( - { - phone: phone, - password: password, - }, - options - ) - } - - @step('Check if I am logged in by checking if I can insert my profile') - private async insertProfile( - supabase: SupabaseClient, - user: User, - fakeUser: { - username: string + @test + async 'on auth state changed should return events'() { + // create user + const fakeUser = await this.createUser() + const events: { event: AuthChangeEvent; token: string }[] = [] + const onAuthStateChanged = (event: AuthChangeEvent, session: Session) => { + log('onAuthStateChanged triggered', event) + events.push({ event, token: session?.access_token }) } - ): Promise<{ data: any; error: any }> { - return supabase - .from('profiles') - .insert({ - id: user.id, - username: fakeUser.username, - }) - .single() - } - @step('Check if I am logged in by checking if I can get my profile') - private async getUserProfile(supabase: SupabaseClient): Promise<{ data: any }> { - return supabase.from('profiles').select().maybeSingle() - } + // create client and subscribe on auth state changes + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange(onAuthStateChanged) - @step('Update user info') - private async updateUser( - supabase: SupabaseClient, - attr: UserAttributes - ): Promise<{ - data: User - user: User - error: ApiError - }> { - return supabase.auth.update(attr) + // sign in as user + await supabase.auth.signInWithPassword({ + email: fakeUser.email, + password: fakeUser.password, + }) + + // update user + const updParams = { + email: faker.internet.email(), + password: faker.internet.password(), + phone: faker.phone.phoneNumber('!#!##!######'), + } + const { error: updUserError } = await this.updateUser(supabase, updParams) + expect(updUserError).toBeNull() + + // remove subscription and sign out + subscription.unsubscribe() + await supabase.auth.signOut() + + // check if sign in and update events were triggered and sign out event was not triggered + expect(events).toHaveLength(2) + expect(events.map((e) => e.event)).toEqual( + expect.arrayContaining(['SIGNED_IN', 'USER_UPDATED']) + ) + expect(events.map((e) => e.event)).not.toContain('SIGNED_OUT') } } diff --git a/tests/features/javascript/authenticationAPI.spec.ts b/tests/features/javascript/authenticationAPI.spec.ts deleted file mode 100644 index f330c3ef0ab..00000000000 --- a/tests/features/javascript/authenticationAPI.spec.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { params, suite, test } from '@testdeck/jest' -import { faker } from '@faker-js/faker' -import { Severity } from 'allure-js-commons' - -import { Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' - -import { FEATURE } from '../templates/enums' -import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' -import { Hooks } from './hooks' - -@suite('authentication API') -class AuthenticationAPI extends Hooks { - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you create user then it has to be in auth db schema') - @test.skip - async 'create user'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you create user then he can sign in') - @test.skip - async 'create user can login'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.CRITICAL) - @description('When you try to create user with anon key then you should get error') - @test.skip - async 'create user with anon key'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you list users then you should get all users') - @test.skip - async 'list users'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.CRITICAL) - @description('When you try to list user with anon key then you should get error') - @test.skip - async 'list users with anon key'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you sign up user with email then he should be able to login') - @test.skip - async 'sign up with email'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you sign in user with email then he should be able to update his profile') - @test.skip - async 'sign in with email'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you sign up user with phone then he should be able to login') - @test.skip - async 'sign up with phone'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you sign in user with phone then he should be able to update his profile') - @test.skip - async 'sign in with phone'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you send magic link then email should be sent to user') - @test.skip - async 'send magic link'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you send invite then corresponding email should be sent to user') - @test.skip - async 'send invite link'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you send reset password then corresponding email should be sent to user') - @test.skip - async 'send reset password'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you sign out user then current session has to be removed') - @test.skip - async 'sign out'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you get provider url then corresponding auth provider url should be returned') - @params.skip({ provider: 'google', options: {}, expectedURL: 'todo' }) - @params.skip({ - provider: 'google', - options: { redirectTo: 'todo', scopes: 'todo' }, - expectedURL: 'todo', - }) - @params.skip({ provider: 'twitter', options: {}, expectedURL: 'todo' }) - // ... - async 'get url for provider'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you get user then currently logined user date has to be returned') - @test.skip - async 'get user'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you update user then currently logined user date has to be updated') - @test.skip - async 'update user'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you delete user then this user has to be deleted and unable to login') - @test.skip - async 'delete user'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.CRITICAL) - @description('When you delete user with anon key you have to receive an error') - @test.skip - async 'delete user with anon key'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you change auth cookie then all new requests should have new JWT') - @test.skip - async 'set auth cookie'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you refreshes session then user and session have to be updated') - @test.skip - async 'refresh session'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you get user by cookie then the corresponding user data has to be returned') - @test.skip - async 'get user by cookie'() { - // todo - } - - @feature(FEATURE.AUTHENTICATION) - @severity(Severity.NORMAL) - @description('When you generate link then the right link has to be returned') - @params.skip({ type: 'signup', options: {}, email: 'todo' }) - @params.skip({ - type: 'signup', - options: { - redirectTo: 'todo', - password: 'todo', - data: { - /* todo */ - }, - }, - email: 'todo', - }) - @params.skip({ type: 'magiclink', options: {}, email: 'todo' }) - // ... - async 'generate link'() { - // todo - } -} diff --git a/tests/features/javascript/functions.spec.ts b/tests/features/javascript/functions.spec.ts index f29dc715641..0aa8fc33e42 100644 --- a/tests/features/javascript/functions.spec.ts +++ b/tests/features/javascript/functions.spec.ts @@ -1,12 +1,15 @@ import { params, suite, test } from '@testdeck/jest' import { faker } from '@faker-js/faker' import { Severity } from 'allure-js-commons' +import { exec, ExecException } from 'child_process' +import os from 'os' import { Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' import { FEATURE } from '../templates/enums' import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' import { Hooks } from './hooks' +import path from 'path' @suite('functions') class Functions extends Hooks { @@ -15,6 +18,25 @@ class Functions extends Hooks { @description('When you get functions client then you are able to set auth') @test.skip async 'set auth'() { - // todo + // execute cli command + const deno = path.join(os.homedir(), '.supabase', 'deno') + const funcPath = path.join(process.cwd(), 'data', 'func.ts') + let prom = new Promise<{ error: ExecException; stdout: string; stderr: string }>((resolve) => { + exec(`${deno} bundle --no-check=remote --quiet ${funcPath}`, (error, stdout, stderr) => + resolve({ error, stdout, stderr }) + ) + }) + const { supabase } = await this.createSignedInSupaClient() + const sb = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY) + const { + data: { session }, + } = await supabase.auth.getSession() + + sb.functions.setAuth(session.access_token) + const { error, stdout, stderr } = await prom + expect([null, undefined]).toContain(error) + expect([null, undefined]).toContain(stderr) + expect(stdout).toBeDefined() + sb.functions.invoke('get_user') } } diff --git a/tests/features/javascript/hooks.ts b/tests/features/javascript/hooks.ts index d162d8b688f..25993a4ee6e 100644 --- a/tests/features/javascript/hooks.ts +++ b/tests/features/javascript/hooks.ts @@ -2,12 +2,13 @@ import postgres from 'postgres' import crossFetch from 'cross-fetch' import { faker } from '@faker-js/faker' import { - ApiError, + AuthResponse, createClient, - Session, SupabaseClient, SupabaseClientOptions, User, + UserAttributes, + UserResponse, } from '@supabase/supabase-js' import { JasmineAllureReporter, step } from '../../.jest/jest-custom-reporter' @@ -32,7 +33,14 @@ export abstract class Hooks { } @step('Create Supabase client') - createSupaClient(url: string, key: string, options: SupabaseClientOptions = {}): SupabaseClient { + createSupaClient( + url: string, + key: string, + options: SupabaseClientOptions<'public'> = {} + ): SupabaseClient { + options.auth = options.auth || {} + options.auth.persistSession = false + return createClient(url, key, options) } @@ -41,6 +49,7 @@ export abstract class Hooks { email: string password: string username: string + id: string }> { const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) @@ -48,11 +57,17 @@ export abstract class Hooks { email: faker.internet.exampleEmail(), password: faker.internet.password(), username: faker.internet.userName(), + id: '', } - const { user, error: signUpError } = await this.signUp(supabase, fakeUser, { + const { + error: signUpError, + data: { user }, + } = await this.signUp(supabase, fakeUser, { data: data, }) expect(signUpError).toBeNull() + expect(user).not.toBeNull() + fakeUser.id = user.id return fakeUser } @@ -92,19 +107,14 @@ export abstract class Hooks { options: { redirectTo?: string data?: object + captchaToken?: string } = {} - ): Promise<{ - user: User - session: Session - error: ApiError - }> { - return supabase.auth.signUp( - { - email: email, - password: password, - }, - options - ) + ): Promise { + return supabase.auth.signUp({ + email: email, + password: password, + options: options, + }) } @step('Check if I am being able to log out') @@ -114,7 +124,7 @@ export abstract class Hooks { @step('Get user data, if there is a logged in user') getUser(supabase: SupabaseClient) { - return supabase.auth.user() + return supabase.auth.getUser() } @step((user: User) => `Get user by ID (${user.id}) from Supabase auth schema`) @@ -127,4 +137,75 @@ export abstract class Hooks { id = ${user.id} ` } + + @step('I sign up with a valid email and password') + async signUpByPhone( + supabase: SupabaseClient, + { + phone = faker.phone.phoneNumber(), + password = faker.internet.password(), + }: { + phone?: string + password?: string + } = {}, + options: { + redirectTo?: string + data?: object + } = {} + ): Promise { + return supabase.auth.signUp({ + phone: phone, + password: password, + options: options, + }) + } + + @step('User inserts profile') + async insertProfile( + supabase: SupabaseClient, + user: { + id: string + }, + fakeUser: { + username: string + } + ): Promise<{ data: any; error: any }> { + return await supabase + .from('profiles') + .insert({ + id: user.id, + username: fakeUser.username, + }) + .select() + } + + @step('I can get my profile via postgREST') + async getUserProfile(supabase: SupabaseClient): Promise<{ data: any; error: any }> { + return supabase.from('profiles').select().maybeSingle() + } + + @step('Update user info') + async updateUser(supabase: SupabaseClient, attr: UserAttributes): Promise { + return supabase.auth.updateUser(attr) + } + + @step('Create signed in supabase client') + async createSignedInSupaClient() { + // create user + const fakeUser = await this.createUser() + + // sign in as user + const supabase = this.createSupaClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ANON) + const { + data: { user }, + error: signInError, + } = await supabase.auth.signInWithPassword({ + email: fakeUser.email, + password: fakeUser.password, + }) + expect(signInError).toBeNull() + fakeUser.id = user.id + + return { supabase, user: fakeUser } + } } diff --git a/tests/features/javascript/postgrest.spec.ts b/tests/features/javascript/postgrest.spec.ts new file mode 100644 index 00000000000..d0602ce19ce --- /dev/null +++ b/tests/features/javascript/postgrest.spec.ts @@ -0,0 +1,86 @@ +import { suite, test, timeout } from '@testdeck/jest' +import { faker } from '@faker-js/faker' +import { Severity } from 'allure-js-commons' + +import { FEATURE } from '../templates/enums' +import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' +import { Hooks } from './hooks' + +@suite('postgrest') +class PostgREST extends Hooks { + @feature(FEATURE.POSTGREST) + @severity(Severity.BLOCKER) + @description('User can insert profile for himself according to RLS to profiles table') + @test + async 'insert row'() { + const { supabase, user } = await this.createSignedInSupaClient() + + // check if user can insert profile for himself + const { + data: [profileInserted], + error: errorInsert, + } = await this.insertProfile(supabase, user, user) + expect(errorInsert).toBeNull() + expect(profileInserted.username).toMatch(user.username) + } + + @feature(FEATURE.POSTGREST) + @severity(Severity.BLOCKER) + @description('User can select his profile from profiles table') + @test + async 'select row'() { + const { supabase, user } = await this.createSignedInSupaClient() + + // insert profile for user + await this.insertProfile(supabase, user, user) + + const { data: profileGot, error } = await this.getUserProfile(supabase) + expect(error).toBeNull() + expect(profileGot.username).toMatch(user.username) + } + + @feature(FEATURE.POSTGREST) + @severity(Severity.BLOCKER) + @description('User can update his profile in profiles table') + @test + async 'update row'() { + const { supabase, user } = await this.createSignedInSupaClient() + + // insert profile for user + await this.insertProfile(supabase, user, user) + + const newName = faker.internet.userName() + // update profile for user + const { data: updUser, error } = await supabase + .from('profiles') + .update({ + id: user.id, + username: newName, + }) + .select() + expect(error).toBeNull() + expect(updUser.length).toEqual(1) + expect(updUser[0].username).toMatch(newName) + + const { data: profileGot } = await this.getUserProfile(supabase) + expect(profileGot.username).toMatch(newName) + } + + @feature(FEATURE.POSTGREST) + @severity(Severity.BLOCKER) + @description('User can delete his profile in profiles table') + @test + async 'delete row'() { + const { supabase, user } = await this.createSignedInSupaClient() + + // insert profile for user + await this.insertProfile(supabase, user, user) + + // delete profile for user + const { error } = await supabase.from('profiles').delete().eq('id', user.id).select() + expect(error).toBeNull() + + const { data: profileGot } = await this.getUserProfile(supabase) + expect(profileGot).toBeNull() + } +} diff --git a/tests/features/javascript/realtime.spec.ts b/tests/features/javascript/realtime.spec.ts index 53988688ff4..59ba68289bf 100644 --- a/tests/features/javascript/realtime.spec.ts +++ b/tests/features/javascript/realtime.spec.ts @@ -1,29 +1,107 @@ -import { params, suite, test } from '@testdeck/jest' +import { params, suite, test, timeout } from '@testdeck/jest' import { faker } from '@faker-js/faker' import { Severity } from 'allure-js-commons' -import { Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' +import { RealtimeChannel } from '@supabase/supabase-js' import { FEATURE } from '../templates/enums' import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' import { Hooks } from './hooks' @suite('realtime') +@timeout(30000) class Realtime extends Hooks { @feature(FEATURE.REALTIME) @severity(Severity.BLOCKER) @description('When you call "on" table then connected realtime client should be returned') - @test.skip - async 'realtime connect'() { - // todo + @test + async '[skip-stage] connect to realtime'() { + const { supabase } = await this.createSignedInSupaClient() + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => + console.log(payload) + ) + channel.subscribe() + + expect(channel).toBeDefined() + const err = await this.waitForChannelJoined(channel) + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(err).toBeNull() + const ok = await supabase.removeChannel(channel) + expect(ok).toBe('ok') + } + + @feature(FEATURE.REALTIME) + @severity(Severity.BLOCKER) + @description('When you subscrive to realtime, you have to receive updates') + @timeout(60000) + @test + async '[skip-stage] receive event when connected to realtime'() { + let res: any + let t: NodeJS.Timeout + const { supabase, user } = await this.createSignedInSupaClient() + + let payloadReceived = (payload: any) => { + if (payload?.eventType !== 'INSERT') { + return + } + clearTimeout(t) + expect(payload.schema).toBe('public') + expect(payload.table).toBe('profiles') + expect(payload.new.id).toBe(user.id) + expect(payload.new.username).toBe(user.username) + expect(payload.old).toEqual({}) + expect(payload.error).toBeUndefined() + res(null) + } + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, payloadReceived) + channel.subscribe() + + expect(channel).toBeDefined() + await this.waitForChannelJoined(channel) + // we should wait some time seconds to connect to database changes + await new Promise((resolve) => setTimeout(resolve, 10000)) + + const eventPromise = new Promise((resolve) => { + res = resolve + new Promise(() => { + t = setTimeout(() => resolve(new Error('timeout')), 30000) + }) + }) + await this.insertProfile(supabase, user, user) + expect(await eventPromise).toBeNull() + + const ok = await supabase.removeChannel(channel) + expect(ok).toBe('ok') } @feature(FEATURE.REALTIME) @severity(Severity.NORMAL) @description('When you call "on" table but not subscribe then no events have to be returned') - @test.skip - async 'no event updates until subscribe'() { - // todo + @test + async 'you should get no events until subscribe'() { + const { supabase, user } = await this.createSignedInSupaClient() + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + + expect(channel).toBeDefined() + await this.insertProfile(supabase, user, user) + + // wait for 1 second to see if we receive any events + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(channel._isClosed).toBeTruthy() + const ok = await supabase.removeChannel(channel) + expect(ok).toBe('ok') } @feature(FEATURE.REALTIME) @@ -31,9 +109,27 @@ class Realtime extends Hooks { @description( 'When you create 2 subs (1 subscribed and 1 not yet) then both should be returned on get subs' ) - @test.skip - async 'get subscriptions'() { - // todo + @test + async 'get supabase client subscriptions'() { + const { supabase } = await this.createSignedInSupaClient() + + const channel1 = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + const channel2 = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + + const channels = supabase.getChannels() + expect(channels).toEqual(expect.arrayContaining([channel1, channel2])) + supabase.removeAllChannels() + expect(supabase.getChannels().length).toEqual(0) } @feature(FEATURE.REALTIME) @@ -125,24 +221,96 @@ class Realtime extends Hooks { @feature(FEATURE.REALTIME) @severity(Severity.CRITICAL) @description('When you unsubscribe from table then no events have to be returned') - @test.skip - async 'unsubscribe from table'() { - // todo + @test + async '[skip-stage] unsubscribe from table'() { + const { supabase, user } = await this.createSignedInSupaClient() + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + channel.subscribe() + // wait for subscription to postgres + await new Promise((resolve) => setTimeout(resolve, 8000)) + const ok = await channel.unsubscribe() + expect(ok).toEqual('ok') + + await this.insertProfile(supabase, user, user) + + // wait for 1 second to see if we receive any events + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(channel._isClosed).toBeTruthy() + await supabase.removeChannel(channel) } @feature(FEATURE.REALTIME) @severity(Severity.CRITICAL) @description('When you remove one subscription then only events from another have to be returned') - @test.skip - async 'remove one subscription'() { - // todo + @test + async '[skip-stage] remove one subscription from client'() { + const { supabase, user } = await this.createSignedInSupaClient() + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + channel.subscribe() + // wait for subscription to postgres + await new Promise((resolve) => setTimeout(resolve, 8000)) + const ok = await supabase.removeChannel(channel) + expect(ok).toEqual('ok') + + await this.insertProfile(supabase, user, user) + + // wait for 1 second to see if we receive any events + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(channel._isClosed).toBeTruthy() } @feature(FEATURE.REALTIME) @severity(Severity.CRITICAL) @description('When you remove all subscription then no events have to be returned') - @test.skip - async 'remove all subscriptions'() { - // todo + @test + async '[skip-stage] remove all subscriptions from client'() { + const { supabase, user } = await this.createSignedInSupaClient() + + const channel = supabase + .channel('profiles') + .on('postgres_changes', { event: '*', schema: 'public' }, (payload: any) => { + console.log(payload) + expect('event received').toBe('should not receive event') + }) + channel.subscribe() + // wait for subscription to postgres + await new Promise((resolve) => setTimeout(resolve, 8000)) + const ok = await supabase.removeAllChannels() + expect(ok).toEqual(expect.arrayContaining(['ok'])) + + await this.insertProfile(supabase, user, user) + + // wait for 1 second to see if we receive any events + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(channel._isClosed).toBeTruthy() + } + + @step('Wait until channel is joined') + async waitForChannelJoined(channel: RealtimeChannel): Promise { + for (let i = 0; i < 30; i++) { + if (channel._isJoined()) { + return null + } + if (channel._isLeaving()) { + return new Error('Channel is leaving') + } + if (channel._isClosed()) { + return new Error('Channel is closed') + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } + return new Error("Channel didn't join in 3 seconds") } } diff --git a/tests/features/javascript/rpc.spec.ts b/tests/features/javascript/rpc.spec.ts index d875ad73678..71883f2dd5c 100644 --- a/tests/features/javascript/rpc.spec.ts +++ b/tests/features/javascript/rpc.spec.ts @@ -1,20 +1,134 @@ -import { params, suite, test } from '@testdeck/jest' -import { faker } from '@faker-js/faker' +import { suite, test, timeout } from '@testdeck/jest' import { Severity } from 'allure-js-commons' -import { Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' +import { SupabaseClient } from '@supabase/supabase-js' import { FEATURE } from '../templates/enums' -import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' +import { description, feature, severity, step } from '../../.jest/jest-custom-reporter' import { Hooks } from './hooks' +import { PendingQuery, Row } from 'postgres' @suite('rpc') +@timeout(30000) class Procedures extends Hooks { - @feature(FEATURE.STORAGE) + @feature(FEATURE.RPC) @severity(Severity.BLOCKER) @description('When you call rpc then you are able to receive its result') - @test.skip - async 'call rpc'() { - // todo + @test + async 'call rpc and get result'() { + await this.createFunction(Procedures.sql` + CREATE OR REPLACE FUNCTION public.test_procedure() RETURNS int language plpgsql as $$ + declare + profile_count integer; + begin + select count(*) + into profile_count + from profiles; + + return profile_count; + end; + $$;`) + + const { supabase, user } = await this.createSignedInSupaClient() + await this.insertProfile(supabase, user, user) + + const result = await this.callRpc(supabase, 'test_procedure') + expect(result.error).toBeNull() + expect(result.data).toBeDefined() + expect(result.data).toBeGreaterThanOrEqual(1) + + await this.dropFunction(Procedures.sql`test_procedure()`) + } + + @feature(FEATURE.RPC) + @severity(Severity.BLOCKER) + @description('When you call rpc, params should be passed properly') + @test + async 'call rpc method that has params'() { + await this.createFunction(Procedures.sql` + CREATE OR REPLACE FUNCTION public.test_procedure(filter text) + RETURNS int language plpgsql as $$ + declare + profile_count integer; + begin + select count(*) + into profile_count + from profiles + where username LIKE '%' || filter || '%'; + + return profile_count; + end; + $$;`) + + const { supabase, user } = await this.createSignedInSupaClient() + await this.insertProfile(supabase, user, user) + + const result = await this.callRpc(supabase, 'test_procedure', { filter: user.username }) + expect(result.error).toBeNull() + expect(result.data).toBeDefined() + expect(result.data).toBeGreaterThanOrEqual(1) + + await this.dropFunction(Procedures.sql`test_procedure(filter text)`) + } + + @feature(FEATURE.RPC) + @severity(Severity.NORMAL) + @description('When you call rpc with head param, no data should be returned') + @test + async 'call rpc with head option'() { + await this.createFunction(Procedures.sql` + CREATE OR REPLACE FUNCTION public.test_procedure() RETURNS int language plpgsql as $$ + declare + profile_count integer; + begin + select count(*) + into profile_count + from profiles; + + return profile_count; + end; + $$;`) + + const { supabase, user } = await this.createSignedInSupaClient() + await this.insertProfile(supabase, user, user) + + const result = await this.callRpc(supabase, 'test_procedure', {}, { head: true }) + expect(result.error).toBeNull() + expect(result.data).toBeNull() + + await this.dropFunction(Procedures.sql`test_procedure()`) + } + + @step('create function') + private async createFunction(body: PendingQuery) { + await Procedures.sql`${body}` + await Procedures.sql`NOTIFY pgrst, 'reload schema';` + } + + @step('drop function') + private async dropFunction(signature: PendingQuery) { + await Procedures.sql`DROP FUNCTION public.${signature};` + } + + @step('call supabase rpc') + private async callRpc( + supabase: SupabaseClient, + name: string, + args?: any, + options?: { + head?: boolean + count?: 'exact' | 'planned' | 'estimated' + } + ) { + let result = await supabase.rpc(name, args, options) + for (let i = 1; i <= 5; i++) { + if (result.error) { + await new Promise((resolve) => setTimeout(resolve, 0.5 * 1000 * i)) + result = await supabase.rpc(name, args, options) + } else { + break + } + } + return result } } diff --git a/tests/features/javascript/storage.spec.ts b/tests/features/javascript/storage.spec.ts index 1a60f64adf8..455b1d51c75 100644 --- a/tests/features/javascript/storage.spec.ts +++ b/tests/features/javascript/storage.spec.ts @@ -2,27 +2,423 @@ import { params, suite, test } from '@testdeck/jest' import { faker } from '@faker-js/faker' import { Severity } from 'allure-js-commons' -import { Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' +import { createClient, Session, SupabaseClient, User, UserAttributes } from '@supabase/supabase-js' +import { Bucket } from '@supabase/storage-js' import { FEATURE } from '../templates/enums' import { description, feature, log, severity, step } from '../../.jest/jest-custom-reporter' import { Hooks } from './hooks' +import fetch from 'cross-fetch' @suite('storage') class Storage extends Hooks { - @feature(FEATURE.STORAGE) - @severity(Severity.BLOCKER) - @description('When you create public bucket then it has to be available') - @test.skip - async 'create public bucket'() { - // todo + static buckets: Pick[] = [] as any + static async after() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const requests = [] + for (const bucket of this.buckets) { + requests.push( + new Promise((resolve) => + (async () => { + await supabase.storage.emptyBucket(bucket.name) + await supabase.storage.deleteBucket(bucket.name) + resolve(null) + })() + ) + ) + } + await Promise.all(requests) + await Hooks.after() } @feature(FEATURE.STORAGE) @severity(Severity.BLOCKER) - @description('When you create private bucket then it has to be available') - @test.skip - async 'create private bucket'() { - // todo + @description('When you create public bucket then it has to be available') + @params({ public: true }) + @params({ public: false }) + async 'create bucket'(params: { public: boolean }) { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucketName = faker.unique(faker.random.word) + const { data: bucket, error } = await supabase.storage.createBucket(bucketName, { + public: params.public, + }) + expect(error).toBeNull() + expect(bucket).toBeDefined() + expect(bucket.name).toBe(bucketName) + Storage.buckets.push(bucket) + + const { data: gotBucket, error: error2 } = await supabase.storage.getBucket(bucketName) + expect(error2).toBeNull() + expect(gotBucket).toBeDefined() + expect(gotBucket.name).toBe(bucketName) + expect(gotBucket.public).toBe(params.public) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('There has to be default RLS policy to not allow users to create bucket') + @params({ public: true }) + @params({ public: false }) + async 'user cannot create bucket because of RLS'(params: { public: boolean }) { + const { supabase } = await this.createSignedInSupaClient() + const bucketName = faker.unique(faker.random.word) + + const { data: bucket, error } = await supabase.storage.createBucket(bucketName, { + public: params.public, + }) + expect(error).not.toBeNull() + expect(error.message).toContain('row-level security') + expect(error.message).toContain('"buckets"') + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('list buckets should return all buckets') + @test + async 'list buckets as admin'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + + const bucket1 = await this.createBucket() + const bucket2 = await this.createBucket() + + const { data: buckets, error } = await supabase.storage.listBuckets() + expect(buckets.length).toBeGreaterThanOrEqual(2) + expect(buckets.map((b) => b.name)).toEqual(expect.arrayContaining([bucket1.name, bucket2.name])) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('get bucket should return bucket info') + @test + async 'get bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const { data: gotBucket, error } = await supabase.storage.getBucket(bucket.id) + expect(error).toBeNull() + expect(gotBucket).toEqual(bucket) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('update bucket should change bucket both public->private and back') + @params({ public: true }) + @params({ public: false }) + async 'update bucket'(params: { public: boolean }) { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket(params.public) + + const { + data: { message }, + error, + } = await supabase.storage.updateBucket(bucket.id, { + public: !params.public, + }) + expect(error).toBeNull() + expect(message).toBe('Successfully updated') + + bucket.public = !params.public + const { data: gotBucket } = await supabase.storage.getBucket(bucket.id) + expect(gotBucket).toEqual(bucket) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('get bucket should return bucket info') + @test + async 'delete bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const { + data: { message }, + error, + } = await supabase.storage.deleteBucket(bucket.id) + expect(error).toBeNull() + expect(message).toBe('Successfully deleted') + + const { data: buckets } = await supabase.storage.listBuckets() + expect( + buckets.map((b) => { + return { name: b.name, id: b.id } + }) + ).not.toContain({ name: bucket.name, id: bucket.id }) + + Storage.buckets = Storage.buckets.filter((b) => b.name != bucket.name) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('upload to bucket') + @test + async 'upload to bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + const { + data: { path }, + error, + } = await supabase.storage.from(bucket.name).upload(file.path, file.data) + expect(error).toBeNull() + expect(path).toEqual(file.path) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('list files in bucket') + @test + async 'list files'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const files: { path: string; data: string }[] = [] + const requests: Promise[] = [] + for (let i = 0; i < 3; i++) { + requests.push( + new Promise((resolve) => { + const dir = faker.random.word() + const file = { + path: dir + '/' + faker.random.word() + '.txt', + data: faker.lorem.sentence(), + } + files.push(file) + const p1 = supabase.storage.from(bucket.name).upload(file.path, file.data) + const file2 = { + path: dir + '/' + faker.random.word() + '.txt', + data: faker.lorem.sentence(), + } + files.push(file2) + const p2 = supabase.storage.from(bucket.name).upload(file2.path, file2.data) + Promise.all([p1, p2]).finally(() => resolve()) + }) + ) + } + await Promise.all(requests) + + const { data: dirs, error: error1 } = await supabase.storage.from(bucket.name).list() + expect(error1).toBeNull() + expect(dirs.map((d) => d.name)).toEqual( + expect.arrayContaining([...new Set(files.map((f) => f.path.split('/')[0]))]) + ) + + const dir = files[0].path.split('/')[0] + const { data: filesInDir, error: error2 } = await supabase.storage.from(bucket.name).list(dir) + expect(error2).toBeNull() + expect(filesInDir.map((d) => d.name)).toEqual( + expect.arrayContaining( + files.filter((f) => f.path.split('/')[0] === dir).map((f) => f.path.split('/')[1]) + ) + ) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('download file') + @test + async 'download file from bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { data, error } = await supabase.storage.from(bucket.name).download(file.path) + expect(error).toBeNull() + expect(await data.text()).toEqual(file.data) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('move file') + @test + async '[skip-local] move file in bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + newPath: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { error } = await supabase.storage.from(bucket.name).move(file.path, file.newPath) + expect(error).toBeNull() + const { data: filesRoot } = await supabase.storage.from(bucket.name).list() + expect(filesRoot.map((f) => f.name)).not.toContain(file.path.split('/')[0]) + const { data: files } = await supabase.storage + .from(bucket.name) + .list(file.newPath.split('/')[0]) + expect(files.map((f) => f.name)).toEqual(expect.arrayContaining([file.newPath.split('/')[1]])) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('copy file') + @test + async '[skip-local] copy file in bucket'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + newPath: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { error } = await supabase.storage.from(bucket.name).copy(file.path, file.newPath) + expect(error).toBeNull() + + const { data: filesRoot } = await supabase.storage.from(bucket.name).list() + expect(filesRoot.map((f) => f.name)).toEqual( + expect.arrayContaining([file.path.split('/')[0], file.newPath.split('/')[0]]) + ) + const { data: filesNew } = await supabase.storage + .from(bucket.name) + .list(file.newPath.split('/')[0]) + expect(filesNew.map((f) => f.name)).toEqual( + expect.arrayContaining([file.newPath.split('/')[1]]) + ) + const { data: files } = await supabase.storage.from(bucket.name).list(file.path.split('/')[0]) + expect(files.map((f) => f.name)).toEqual(expect.arrayContaining([file.path.split('/')[1]])) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('get public link to file in the public bucket') + @test + async 'get public link to file'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { + data: { publicUrl }, + } = supabase.storage.from(bucket.name).getPublicUrl(file.path) + expect(publicUrl).toBeDefined() + expect(publicUrl.length).toBeGreaterThan(1) + expect(publicUrl).toMatch(new RegExp(`^http[s]?://.*storage.*/${bucket.name}/.*`)) + + const resp = await fetch(publicUrl) + expect(resp.status).toEqual(200) + expect(await resp.text()).toEqual(file.data) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('get public link to file in the private bucket') + @test + async 'get public link to private file'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket(false) + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { + data: { publicUrl }, + } = supabase.storage.from(bucket.name).getPublicUrl(file.path) + expect(publicUrl).toBeDefined() + expect(publicUrl.length).toBeGreaterThan(1) + expect(publicUrl).toMatch(new RegExp(`^http[s]?://.*storage.*/${bucket.name}/.*`)) + + const resp = await fetch(publicUrl) + expect(resp.status).toEqual(400) // todo 400 should be 404 too I guess + const error = await resp.json() + expect(error.statusCode).toBe('404') // todo should be a number? + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('get signed link to file in the bucket') + @params({ public: true }) + @params({ public: false }) + async 'get signed link to file'(params: { public: boolean }) { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket(params.public) + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { + data: { signedUrl }, + error, + } = await supabase.storage.from(bucket.name).createSignedUrl(file.path, 10000) + expect(error).toBeNull() + expect(signedUrl).toBeDefined() + expect(signedUrl.length).toBeGreaterThan(1) + expect(signedUrl).toMatch(new RegExp(`^http[s]?://.*storage.*/${bucket.name}/.*`)) + + const resp = await fetch(signedUrl) + expect(resp.status).toEqual(200) + expect(await resp.text()).toEqual(file.data) + } + + @feature(FEATURE.STORAGE) + @severity(Severity.BLOCKER) + @description('update file check if it will change') + @test + async 'update file'() { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucket = await this.createBucket() + + const file = { + path: faker.random.word() + '/' + faker.random.word() + '/' + faker.random.word() + '.txt', + data: faker.lorem.paragraph(), + } + await supabase.storage.from(bucket.name).upload(file.path, file.data) + + const { + data: { publicUrl }, + } = supabase.storage.from(bucket.name).getPublicUrl(file.path) + expect(publicUrl).toBeDefined() + expect(publicUrl.length).toBeGreaterThan(1) + expect(publicUrl).toMatch(new RegExp(`^http[s]?://.*storage.*/${bucket.name}/.*`)) + + const newFile = { + path: file.path, + data: faker.lorem.paragraph(10), + } + await supabase.storage.from(bucket.name).update(newFile.path, newFile.data) + + const resp = await fetch(publicUrl) + expect(resp.status).toEqual(200) + expect(await resp.text()).toEqual(newFile.data) + } + + @step('create bucket') + async createBucket(pub = true) { + const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY_ADMIN) + const bucketName = faker.unique(faker.random.word) + + const { data: bucket, error } = await supabase.storage.createBucket(bucketName, { + public: pub, + }) + expect(error).toBeNull() + expect(bucket).toBeDefined() + expect(bucket.name).toBe(bucketName) + Storage.buckets.push(bucket) + + const { data: buckets } = await supabase.storage.listBuckets() + return buckets.find((b) => b.name === bucketName) } } diff --git a/tests/features/templates/enums.ts b/tests/features/templates/enums.ts index 3ce59684e85..fed746cf37a 100644 --- a/tests/features/templates/enums.ts +++ b/tests/features/templates/enums.ts @@ -1,10 +1,11 @@ export enum FEATURE { AUTHENTICATION = 'authentication', + AUTH_ADMIN = 'auth_admin', REALTIME = 'realtime', STORAGE = 'storage', FUNCTIONS = 'functions', RPC = 'rpc', - DB = 'database', - Filters = 'database.filters', - Modifiers = 'database.modifiers', + POSTGREST = 'postgrest', + FILTERS = 'postgrest_filters', + MODIFIERS = 'postgrest_modifiers', } diff --git a/tests/package-lock.json b/tests/package-lock.json index e0dfc319b5b..16b0e91e8a7 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "devDependencies": { "@faker-js/faker": "^6.1.2", - "@supabase/supabase-js": "^1.35.6", + "@supabase/supabase-js": "^2.0.0", "@testdeck/jest": "^0.2.0", "@types/jest": "^27.4.0", "@types/node": "^17.0.24", @@ -1023,36 +1023,36 @@ } }, "node_modules/@supabase/functions-js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.4.tgz", - "integrity": "sha512-yYVgkECjv7IZEBKBI3EB5Q7R1p0FJ10g8Q9N7SWKIHUU6i6DnbEGHIMFLyQRm1hmiNWD8fL7bRVEYacmTRJhHw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.0.0.tgz", + "integrity": "sha512-ozb7bds2yvf5k7NM2ZzUkxvsx4S4i2eRKFSJetdTADV91T65g4gCzEs9L3LUXSrghcGIkUaon03VPzOrFredqg==", "dev": true, "dependencies": { "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/gotrue-js": { - "version": "1.22.22", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.22.tgz", - "integrity": "sha512-exbCopLo4tLawKZR25wEdipm+IRVujuqFRR4h2wiXd11YA+N8rfgg/4S4L6BTFHmzV+KMjy0kDF3yMbsjIR/xA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.0.1.tgz", + "integrity": "sha512-PCzHeFhz33lPWLZT+UiVDpGkZQI3K1wwhkhgAjvJdkyCbwK5u+LWWUzz0AX66bGGLpg1IYQIP7oz8V68XVjzMA==", "dev": true, "dependencies": { - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/postgrest-js": { - "version": "0.37.4", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-0.37.4.tgz", - "integrity": "sha512-x+c2rk1fz9s6f1PrGxCJ0QTUgXPDI0G3ngIqD5sSiXhhCyfl8Q5V92mXl2EYtlDhkiUkjFNrOZFhXVbXOHgvDw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.0.0.tgz", + "integrity": "sha512-MRR4g8dThgSnlnjkL2Jvc6gS73W68hLq48/Q245CLvUPt3KtOh3WXpvo4rZDEDHYVkyVga+L9IQixo6YTsjMQg==", "dev": true, "dependencies": { "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/realtime-js": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.4.tgz", - "integrity": "sha512-h/Jk3PCLkIVonsNavof/LvHbvF41UD+D+mWcA3m8yHzJ2TLbV3S4XDYId+A3AkvFOAork7Ns/9O8rK0uY4F4zw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.0.0.tgz", + "integrity": "sha512-DJVt5Z76pik3Fxt1es4fmquq6GrwR3nLqcb2Q6VpHXSudtixML0Cyxsu0DPzxa6ruSQrO9s08s0OQgwaIA1C0Q==", "dev": true, "dependencies": { "@types/phoenix": "^1.5.4", @@ -1060,25 +1060,26 @@ } }, "node_modules/@supabase/storage-js": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.3.tgz", - "integrity": "sha512-jnIZWqOc9TGclOozgX9v/RWGFCgJAyW/yvmauexgRZhWknUXoA4b2i8tj7vfwE0WTvNRuA5JpXID98rfJeSG7Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.0.0.tgz", + "integrity": "sha512-7kXThdRt/xqnOOvZZxBqNkeX1CFNUWc0hYBJtNN/Uvt8ok9hD14foYmroWrHn046wEYFqUrB9U35JYsfTrvltA==", "dev": true, "dependencies": { - "cross-fetch": "^3.1.0" + "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/supabase-js": { - "version": "1.35.6", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.35.6.tgz", - "integrity": "sha512-KDRXRr+kdGwruIUizZPALbe5YccMYFVyZJf1sFWKLncaLYSFiM6iKFnqCrNeQ4JFoZZiICkiTl1FUuai62jVpg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.0.0.tgz", + "integrity": "sha512-8nNMXYzBlQVSDONFaNgyyxsAdUY+C9Q/7vEDYDdTFTyC3i+luXTDOSjHMLcjIky/+x5rIAcNQqqIJ48HJUluXA==", "dev": true, "dependencies": { - "@supabase/functions-js": "^1.3.4", - "@supabase/gotrue-js": "^1.22.21", - "@supabase/postgrest-js": "^0.37.4", - "@supabase/realtime-js": "^1.7.4", - "@supabase/storage-js": "^1.7.2" + "@supabase/functions-js": "^2.0.0", + "@supabase/gotrue-js": "^2.0.1", + "@supabase/postgrest-js": "^1.0.0", + "@supabase/realtime-js": "^2.0.0", + "@supabase/storage-js": "^2.0.0", + "cross-fetch": "^3.1.5" } }, "node_modules/@testdeck/core": { @@ -2652,12 +2653,12 @@ } }, "node_modules/ext": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "dev": true, "dependencies": { - "type": "^2.5.0" + "type": "^2.7.2" } }, "node_modules/ext/node_modules/type": { @@ -6349,36 +6350,36 @@ } }, "@supabase/functions-js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.4.tgz", - "integrity": "sha512-yYVgkECjv7IZEBKBI3EB5Q7R1p0FJ10g8Q9N7SWKIHUU6i6DnbEGHIMFLyQRm1hmiNWD8fL7bRVEYacmTRJhHw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.0.0.tgz", + "integrity": "sha512-ozb7bds2yvf5k7NM2ZzUkxvsx4S4i2eRKFSJetdTADV91T65g4gCzEs9L3LUXSrghcGIkUaon03VPzOrFredqg==", "dev": true, "requires": { "cross-fetch": "^3.1.5" } }, "@supabase/gotrue-js": { - "version": "1.22.22", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-1.22.22.tgz", - "integrity": "sha512-exbCopLo4tLawKZR25wEdipm+IRVujuqFRR4h2wiXd11YA+N8rfgg/4S4L6BTFHmzV+KMjy0kDF3yMbsjIR/xA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.0.1.tgz", + "integrity": "sha512-PCzHeFhz33lPWLZT+UiVDpGkZQI3K1wwhkhgAjvJdkyCbwK5u+LWWUzz0AX66bGGLpg1IYQIP7oz8V68XVjzMA==", "dev": true, "requires": { - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" } }, "@supabase/postgrest-js": { - "version": "0.37.4", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-0.37.4.tgz", - "integrity": "sha512-x+c2rk1fz9s6f1PrGxCJ0QTUgXPDI0G3ngIqD5sSiXhhCyfl8Q5V92mXl2EYtlDhkiUkjFNrOZFhXVbXOHgvDw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.0.0.tgz", + "integrity": "sha512-MRR4g8dThgSnlnjkL2Jvc6gS73W68hLq48/Q245CLvUPt3KtOh3WXpvo4rZDEDHYVkyVga+L9IQixo6YTsjMQg==", "dev": true, "requires": { "cross-fetch": "^3.1.5" } }, "@supabase/realtime-js": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-1.7.4.tgz", - "integrity": "sha512-h/Jk3PCLkIVonsNavof/LvHbvF41UD+D+mWcA3m8yHzJ2TLbV3S4XDYId+A3AkvFOAork7Ns/9O8rK0uY4F4zw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.0.0.tgz", + "integrity": "sha512-DJVt5Z76pik3Fxt1es4fmquq6GrwR3nLqcb2Q6VpHXSudtixML0Cyxsu0DPzxa6ruSQrO9s08s0OQgwaIA1C0Q==", "dev": true, "requires": { "@types/phoenix": "^1.5.4", @@ -6386,25 +6387,26 @@ } }, "@supabase/storage-js": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-1.7.3.tgz", - "integrity": "sha512-jnIZWqOc9TGclOozgX9v/RWGFCgJAyW/yvmauexgRZhWknUXoA4b2i8tj7vfwE0WTvNRuA5JpXID98rfJeSG7Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.0.0.tgz", + "integrity": "sha512-7kXThdRt/xqnOOvZZxBqNkeX1CFNUWc0hYBJtNN/Uvt8ok9hD14foYmroWrHn046wEYFqUrB9U35JYsfTrvltA==", "dev": true, "requires": { - "cross-fetch": "^3.1.0" + "cross-fetch": "^3.1.5" } }, "@supabase/supabase-js": { - "version": "1.35.6", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-1.35.6.tgz", - "integrity": "sha512-KDRXRr+kdGwruIUizZPALbe5YccMYFVyZJf1sFWKLncaLYSFiM6iKFnqCrNeQ4JFoZZiICkiTl1FUuai62jVpg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.0.0.tgz", + "integrity": "sha512-8nNMXYzBlQVSDONFaNgyyxsAdUY+C9Q/7vEDYDdTFTyC3i+luXTDOSjHMLcjIky/+x5rIAcNQqqIJ48HJUluXA==", "dev": true, "requires": { - "@supabase/functions-js": "^1.3.4", - "@supabase/gotrue-js": "^1.22.21", - "@supabase/postgrest-js": "^0.37.4", - "@supabase/realtime-js": "^1.7.4", - "@supabase/storage-js": "^1.7.2" + "@supabase/functions-js": "^2.0.0", + "@supabase/gotrue-js": "^2.0.1", + "@supabase/postgrest-js": "^1.0.0", + "@supabase/realtime-js": "^2.0.0", + "@supabase/storage-js": "^2.0.0", + "cross-fetch": "^3.1.5" } }, "@testdeck/core": { @@ -7602,12 +7604,12 @@ } }, "ext": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "dev": true, "requires": { - "type": "^2.5.0" + "type": "^2.7.2" }, "dependencies": { "type": { diff --git a/tests/package.json b/tests/package.json index a85bd06ff62..1633462bdbc 100644 --- a/tests/package.json +++ b/tests/package.json @@ -6,6 +6,9 @@ "docker:up": "cd ../docker && ENABLE_EMAIL_AUTOCONFIRM=true docker-compose --env-file ./.env -f docker-compose.yml -f dev/docker-compose.dev.yml up -d", "docker:down": "cd ../docker && docker-compose --env-file ./.env -f docker-compose.yml -f dev/docker-compose.dev.yml down", "test": "jest", + "test:local": "jest --testNamePattern '^((?!\\[skip-local\\]).)*$' --testPathPattern '^((?!realtime).)*$' --detectOpenHandles", + "test:stage": "jest --testNamePattern '^((?!\\[skip-stage\\]).)*$' --detectOpenHandles", + "test:prod": "jest --detectOpenHandles", "allure:generate": "rm -rf allure-report && node_modules/allure-commandline/bin/allure generate", "allure:serve": "node_modules/allure-commandline/bin/allure serve", "test:report": "npm run allure:generate && npm run allure:serve", @@ -15,7 +18,7 @@ "license": "MIT", "devDependencies": { "@faker-js/faker": "^6.1.2", - "@supabase/supabase-js": "^1.35.6", + "@supabase/supabase-js": "^2.0.0", "@testdeck/jest": "^0.2.0", "@types/jest": "^27.4.0", "@types/node": "^17.0.24", diff --git a/tests/supabase/.gitignore b/tests/supabase/.gitignore new file mode 100644 index 00000000000..773c7c3e0a1 --- /dev/null +++ b/tests/supabase/.gitignore @@ -0,0 +1,3 @@ +# Supabase +.branches +.temp diff --git a/tests/supabase/config.toml b/tests/supabase/config.toml new file mode 100644 index 00000000000..2d4498ba196 --- /dev/null +++ b/tests/supabase/config.toml @@ -0,0 +1,71 @@ +# A string used to distinguish different Supabase projects on the same host. Defaults to the working +# directory name when running `supabase init`. +project_id = "tests" + +[api] +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. public and storage are always included. +schemas = [] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[db] +# Port to use for the local database URL. +port = 54322 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 14 + +[studio] +# Port to use for Supabase Studio. +port = 54323 + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +# Port to use for the email testing server web interface. +port = 54324 +smtp_port = 54325 +pop3_port = 54326 + +[auth] +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://localhost:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://localhost:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one +# week). +jwt_expiry = 3600 +# Allow/disallow new user signups to your project. +enable_signup = true + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false + +[auth.phone] +# Allow/disallow new user signups via phone to your project. +enable_signup = true +double_confirm_changes = true +enable_confirmations = false + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `twitch`, `twitter`, `slack`, `spotify`. +[auth.external.apple] +enabled = false +client_id = "" +secret = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" diff --git a/tests/supabase/migrations/data.sql b/tests/supabase/migrations/data.sql new file mode 100644 index 00000000000..88b84570933 --- /dev/null +++ b/tests/supabase/migrations/data.sql @@ -0,0 +1,50 @@ +create table if not exists profiles ( + id uuid references auth.users not null, + updated_at timestamp with time zone, + username text unique, + avatar_url text, + website text, + + primary key (id), + unique(username), + constraint username_length check (char_length(username) >= 3) +); + +alter table profiles enable row level security; + +create policy "Public profiles are viewable by the owner." + on profiles for select + using ( 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 delete own profile." + on profiles for delete + using ( auth.uid() = id ); + +-- Set up Realtime +drop publication if exists supabase_realtime; +create publication supabase_realtime; +alter publication supabase_realtime add table profiles; + +-- Set up Storage +insert into storage.buckets (id, name) +values ('avatars', 'avatars') on conflict do nothing; + +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 update an avatar." + on storage.objects for update + with check ( bucket_id = 'avatars' );