Merge pull request #9515 from supabase/master

Prod studio deploy
This commit is contained in:
Alaister Young authored and GitHub committed 2022-10-13 19:46:48 +11:00
commit da8cf6f5ef
109 files changed
+3167 -2216

No files matched your search

+16 -16
View File
@@ -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 }}
File diff suppressed because it is too large. Load diff
-17
View File
@@ -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)
-321
View File
@@ -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 (
<UserProvider supabaseClient={supabaseClient}>
<Component {...pageProps} />
</UserProvider>
)
}
```
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 && <p>{error.message}</p>}
<Auth
supabaseClient={supabaseClient}
providers={['google', 'github']}
socialLayout="horizontal"
socialButtonSize="xlarge"
/>
</>
)
return (
<>
<button onClick={() => supabaseClient.auth.signOut()}>Sign out</button>
<p>user:</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
<p>client-side data fetching with RLS</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
</>
)
}
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 <div>Hello {user.name}</div>
}
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 <div>Protected content</div>
}
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 (
<>
<div>Protected content for {user.email}</div>
<pre>{JSON.stringify(data, null, 2)}</pre>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
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 <div>Protected content</div>
}
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`.
-303
View File
@@ -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
<script>
import { session } from '$app/stores'
import { supabaseClient } from '$lib/db'
import { SupaAuthHelper } from '@supabase/auth-helpers-svelte'
</script>
<SupaAuthHelper {supabaseClient} {session}>
<slot />
</SupaAuthHelper>
```
### 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
/// <reference types="@sveltejs/kit" />
// 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
<a href="/api/auth/logout">Sign out</a>
```
### 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
<script>
import { session } from '$app/stores'
</script>
{#if !$session.user}
<h1>I am not logged in</h1>
{:else}
<h1>Welcome {$session.user.email}</h1>
<p>I am logged in!</p>
{/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
<script>
import Auth from 'supabase-ui-svelte';
import { error, isLoading } from '@supabase/auth-helpers-svelte';
import { supabaseClient } from '$lib/db';
import { session } from '$app/stores';
let loadedData = [];
async function loadData() {
const { data } = await supabaseClient.from('test').select('*').single();
loadedData = data
}
$: {
if ($session.user && $session.user.id) {
loadData();
}
}
</script>
{#if !$session.user}
{#if $error}
<p>{$error.message}</p>
{/if}
<h1>{$isLoading ? `Loading...` : `Loaded!`}</h1>
<Auth
supabaseClient={supabaseClient}
providers={['google', 'github']}
/>
{:else}
<a href=="/api/auth/logout">Sign out</a>
<p>user:</p>
<pre>{JSON.stringify($session.user, null, 2)}</pre>
<p>client-side data fetching with RLS</p>
<pre>{JSON.stringify(loadedData, null, 2)}</pre>
{/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
<!-- src/routes/profile.svelte -->
<script>
export let user
export let data
</script>
<div>Protected content for {user.email}</div>
<pre>{JSON.stringify(data, null, 2)}</pre>
<pre>{JSON.stringify(user, null, 2)}</pre>
```
```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<GetOutput> = async ({ locals }) =>
withApiAuth(
{
redirectTo: '/',
user: locals.user,
},
async () => {
const { data } = await supabaseServerClient(session.accessToken)
.from<TestTable>('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<GetOutput> = 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.
+10 -6
View File
@@ -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`.
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY -->
## 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`.
<ul>
<li>
Required: <code>true</code>
</li>
<li>
Default: <code>None</code>
</li>
<li>Required: <code>true</code></li>
<li>Default: <code>None</code></li>
</ul>
<br />
+5 -2
View File
@@ -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';
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
Documentation of the gotrue API.
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
+1 -9
View File
@@ -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)
+2 -2
View File
@@ -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
@@ -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:
+1 -1
View File
@@ -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) |
+8 -10
View File
@@ -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.
<video width="99%" muted playsInline controls="true">
<source
@@ -187,12 +194,3 @@ Supabase makes it simple to manage your users.
playsInline
/>
</video>
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)
+12 -22
View File
@@ -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:
/>
</video>
### 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() {
+10 -20
View File
@@ -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)
@@ -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:
/>
</video>
### 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() {
@@ -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:
/>
</video>
### 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() {
+9 -43
View File
@@ -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
<Tabs
defaultValue="js"
@@ -45,44 +40,11 @@ Add logins using our client libraries:
<TabItem value="js">
```js
const { user, error } = await supabase.auth.signIn({
email: 'example@email.com',
password: 'example-password',
})
```
</TabItem>
<TabItem value="dart">
```dart
final res = await supabase.auth.signIn(
email: 'example@email.com',
password: 'example-password'
);
final user = res.data?.user;
final error = res.error;
```
</TabItem>
</Tabs>
Add this function which you can call from a button, link, or UI element.
<Tabs
defaultValue="js"
groupId="guides/auth"
values={[{ label: 'JavaScript', value: 'js' }, { label: 'Dart', value: 'dart' }]}
>
<TabItem value="js">
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() {
<TabItem value="dart">
When your user signs in, call [signIn()](/docs/reference/dart/auth-signin) with their email address and password:
```dart
Future<void> signInWithEmail() async {
await supabase.auth.signIn(
@@ -106,8 +70,6 @@ Future<void> signInWithEmail() async {
</Tabs>
To log out:
<Tabs
defaultValue="js"
groupId="guides/auth"
@@ -116,6 +78,8 @@ To log out:
<TabItem value="js">
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() {
<TabItem value="dart">
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<void> signOut() async {
await supabase.auth.signOut();
@@ -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:
/>
</video>
### 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() {
+10 -20
View File
@@ -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:
/>
</video>
### 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() {
@@ -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:
/>
</video>
### 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() {
+11 -21
View File
@@ -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:
/>
</video>
### 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() {
@@ -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
@@ -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):
<Tabs
groupId="install"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'Yarn', value: 'yarn'},
]}>
<TabItem value="npm">
```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
</TabItem>
<TabItem value="yarn">
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
```
</TabItem>
</Tabs>
## 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 (
<UserProvider supabaseClient={supabaseClient}>
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<button
onClick={async () => {
await supabaseClient.auth.signOut()
router.push('/')
}}
>
Logout
</button>
<Component {...pageProps} />
</UserProvider>
</SessionContextProvider>
)
}
```
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<Database>()
);
```
```js
// Retrieving a supabase client object from the SessionContext:
import { useSupabaseClient } from '@supabase/auth-helpers-react';
import { Database } from '../db_types';
const supabaseClient = useSupabaseClient<Database>();
```
## 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 && <p>{error.message}</p>}
<Auth
redirectTo="http://localhost:3000/"
appearance={{ theme: ThemeSupa }}
supabaseClient={supabaseClient}
providers={['google', 'github']}
socialLayout="horizontal"
socialButtonSize="xlarge"
/>
</>
)
@@ -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 <div>Protected content</div>
@@ -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
<button
onClick={async () => {
await supabaseClient.auth.signOut()
router.push('/')
}}
>
Logout
</button>
```
- 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<Database>()
);
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<Database>({ 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<Database>()
);
```
```js
// Retrieving a supabase client object from the SessionContext:
import { useSupabaseClient } from '@supabase/auth-helpers-react';
import { Database } from '../db_types';
const supabaseClient = useSupabaseClient<Database>();
```
@@ -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() {
@@ -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:
/>
</video>
### 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() {
@@ -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
<Tabs
defaultValue="js"
@@ -40,39 +36,11 @@ Add logins using our client libraries:
<TabItem value="js">
```js
const { user, error } = await supabase.auth.signIn({
email: 'example@email.com',
})
```
</TabItem>
<TabItem value="dart">
```dart
final res = await supabase.auth.signIn(email: 'example@email.com');
final error = res.error;
```
</TabItem>
</Tabs>
Add this function which you can call from a button, link, or UI element.
<Tabs
defaultValue="js"
groupId="guides/auth"
values={[{ label: 'JavaScript', value: 'js' }, { label: 'Dart', value: 'dart' }]}
>
<TabItem value="js">
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() {
<TabItem value="dart">
When your user signs in, call [signIn()](/docs/reference/dart/auth-signin) with their email address:
```dart
Future<void> signInWithEmail() async {
await supabase.auth.signIn(email: 'example@email.com');
@@ -92,8 +62,6 @@ Future<void> signInWithEmail() async {
</Tabs>
To log out:
<Tabs
defaultValue="js"
groupId="guides/auth"
@@ -102,6 +70,8 @@ To log out:
<TabItem value="js">
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() {
<TabItem value="dart">
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<void> signOut() async {
await supabase.auth.signOut();
@@ -172,7 +172,7 @@ values={[
<TabItem value="JS">
```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={[
<TabItem value="JS">
```js
let { user, error } = await supabase.auth.signIn({
let { user, error } = await supabase.auth.signInWithOtp({
phone: '+13334445555',
})
```
@@ -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://<project-ref>.supabas
/>
</video>
### 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://<project-ref>.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() {
+8 -18
View File
@@ -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:
/>
</video>
### 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() {
@@ -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:
/>
</video>
### 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() {
@@ -189,7 +189,7 @@ values={[
<TabItem value="JS">
```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={[
<TabItem value="JS">
```js
let { user, error } = await supabase.auth.signIn({
let { user, error } = await supabase.auth.signInWithOtp({
phone: '+13334445555',
})
```
@@ -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:
/>
</video>
### 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() {
@@ -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:
/>
</video>
### 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() {
@@ -163,7 +163,7 @@ values={[
<TabItem value="JS">
```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={[
<TabItem value="JS">
```js
let { user, error } = await supabase.auth.signIn({
let { user, error } = await supabase.auth.signInWithOtp({
phone: '491512223334444',
})
```
+11 -27
View File
@@ -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://<project-ref>.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: '<your_connection>',
organization: '<your_organization',
provider: '<your_provider>',
}
)
```
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() {
+8 -18
View File
@@ -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:
/>
</video>
### 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() {
+2 -45
View File
@@ -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.
<div class="container" style={{ padding: 0 }}>
<div class="row is-multiline">
{examples.map((x) => (
<div class="col col--6">
<ButtonCard
class="card"
to={x.href}
title={x.name}
description={x.description}
style={{ height: '100%' }}
/>
</div>
))}
</div>
</div>
## 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`).
@@ -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).
@@ -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.
<div class="container" style={{ padding: 0 }}>
<div class="row is-multiline">
{examples.map((x) => (
<div class="col col--6">
<ButtonCard
class="card"
to={x.href}
title={x.name}
description={x.description}
style={{ height: '100%' }}
/>
</div>
))}
</div>
</div>
+26 -1
View File
@@ -34,7 +34,32 @@ To update the screenshots, ensure that at least one log line is selected to disp
</TabItem>
<TabItem value="postgres" label="Postgres">
[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)
@@ -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):
@@ -235,7 +235,7 @@ const roomId = 'room1'
const userId = 'user1'
channel.on(
'postgres-changes',
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -272,7 +272,7 @@ You can access the storage API directly with the same endpoint. See the <a href=
Supabase Storage is integrated with your [Postgres Database](/docs/guides/database).
This means that you can use the same [Row Level Security Policies](/docs/guides/auth#policies)
for managing access to your files. Supabase Storage stores metadata in the `objects` and `buckets` table in the storage schema. To allow read access to files, the RLS policy must allow users to `SELECT` the `objects` table and for uploading a new object, the RLS policy must grant users access to `INSERT` into the `objects` table and so on. The mapping between the different API calls and the database permissions required is documented in the [Reference docs](/docs/reference/javascript/next/storage-createbucket).
for managing access to your files. Supabase Storage stores metadata in the `objects` and `buckets` table in the storage schema. To allow read access to files, the RLS policy must allow users to `SELECT` the `objects` table and for uploading a new object, the RLS policy must grant users access to `INSERT` into the `objects` table and so on. The mapping between the different API calls and the database permissions required is documented in the [Reference docs](/docs/reference/javascript/storage-createbucket).
:::note
+1 -7
View File
@@ -8,12 +8,6 @@ sidebar_label: Expo
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
:::note
This guide uses supabase-js v2.0.0 Release Candidate (RC).
:::
## Intro
This example provides the steps to build a simple user management app (from scratch!) using Supabase and React Native. It includes:
@@ -155,7 +149,7 @@ cd expo-user-management
Then let's install the additional dependencies: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npm install @supabase/supabase-js@rc
npm install @supabase/supabase-js
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
```
+1 -7
View File
@@ -8,12 +8,6 @@ sidebar_label: 'Next.js'
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
:::note
This guide uses supabase-js v2.0.0 Release Candidate (RC).
:::
## Intro
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Next.js. It includes:
@@ -177,7 +171,7 @@ cd supabase-nextjs
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npm install @supabase/supabase-js@rc
npm install @supabase/supabase-js
```
And finally we want to save the environment variables in a `.env.local`.
+2 -8
View File
@@ -8,12 +8,6 @@ sidebar_label: React
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
:::note
This guide uses supabase-js v2.0.0 Release Candidate (RC).
:::
## Intro
This example provides the steps to build a simple user management app (from scratch!) using Supabase and React. It includes:
@@ -174,10 +168,10 @@ npx create-react-app supabase-react
cd supabase-react
```
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js). We're going to install the "rc" (release candidate) version until this is released for general use.
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js).
```bash
npm install @supabase/supabase-js@rc
npm install @supabase/supabase-js
```
And finally we want to save the environment variables in a `.env`.
+1 -7
View File
@@ -8,12 +8,6 @@ sidebar_label: SolidJS
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
:::note
This guide uses supabase-js v2.0.0 Release Candidate (RC).
:::
## Intro
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Solid JS. It includes:
@@ -176,7 +170,7 @@ cd supabase-solid
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npm install @supabase/supabase-js@rc
npm install @supabase/supabase-js
```
And finally we want to save the environment variables in a `.env`.
+1 -7
View File
@@ -8,12 +8,6 @@ sidebar_label: Svelte
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
:::note
This guide uses supabase-js v2.0.0 Release Candidate (RC).
:::
## Intro
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Svelte. It includes:
@@ -160,7 +154,7 @@ npm install
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npm install @supabase/supabase-js@rc
npm install @supabase/supabase-js
```
And finally we want to save the environment variables in a `.env`.
@@ -82,13 +82,13 @@ For the curious, try heading to the SQL editor and querying:
select * from auth.users;
```
If supabase-js is loaded on your site (in this case http://localhost:3000) then it will automatically pluck the access_token out of the URL and initiate a session. You can check the [session()](../../reference/javascript/auth-session) method to see if there is a valid session:
If supabase-js is loaded on your site (in this case http://localhost:3000) it automatically plucks the `access_token` out of the URL and initiates a session. You can retrieve the [session](/docs/reference/javascript/auth-getsession) to see if there is a valid session:
```jsx
console.log(supabase.auth.session())
console.log(supabase.auth.getSession())
```
Now that we can use methods like `supabase.auth.signIn({ email, password})` to issue JWTs to users we want to start fetching resources specific to that user. So let's make some. Go to the SQL editor and run:
Now that we can use methods to issue JWTs to users, we want to start fetching resources specific to that user. So let's make some. Go to the SQL editor and run:
```sql
create table my_scores (
-20
View File
@@ -41,26 +41,6 @@ Reference documentation for the official Supabase client libraries, APIs, and to
</div>
</div>
## Community
Reference documentation for community-contributed utilities.
<div class="container" style={{ padding: 0 }}>
<div class="row is-multiline">
{/* Auth */}
<div class="col col--6">
<ButtonCard
class="card"
to={useBaseUrl('/reference/auth-helpers')}
style={{ height: '100%' }}
title="Auth Helpers"
description="Collection of framework-specific auth utilities for working with Supabase."
/>
</div>
{/* */}
</div>
</div>
## Self-hosting
Reference documentation for self-hosting Supabase features.
+2 -2
View File
@@ -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.
+2 -15
View File
@@ -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: [
+20 -14
View File
@@ -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',
@@ -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
@@ -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)
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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'
@@ -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'
+2 -2
View File
@@ -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."
}
]
+17 -2
View File
@@ -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
// {
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -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 ;)
@@ -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'
+3
View File
@@ -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',
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
@@ -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",
+2 -2
View File
@@ -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({
+9 -9
View File
@@ -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:
<Tabs>
@@ -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).
<Tabs>
<TabItem value="schema" label="Schema">
@@ -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
@@ -112,12 +112,13 @@ const RowHeader: FC<RowHeaderProps> = ({ 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 } =
@@ -27,10 +27,10 @@ const PolicyRow: FC<Props> = ({
'w-full space-x-4 border-b py-4 lg:items-center',
].join(' ')}
>
<div className="flex grow flex-col space-y-1 truncate">
<div className="flex grow flex-col space-y-1">
<div className="flex items-center space-x-4">
<p className="font-mono text-xs text-scale-1000">{policy.command}</p>
<p className="max-w-xs truncate text-sm text-scale-1200">{policy.name}</p>
<p className="text-sm text-scale-1200">{policy.name}</p>
</div>
<div className="flex items-center space-x-2">
<p className="text-scale-1000 text-sm">Applied to:</p>
@@ -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<Props> = ({ limit }) => {
const FreeProjectLimitWarning: FC<Props> = ({ membersExceededLimit }) => {
return (
<div className="mt-4">
<InformationBox
icon={<IconAlertCircle className="text-scale-1200" size="large" strokeWidth={1.5} />}
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={
<div className="space-y-3">
<p className="text-sm leading-normal">
{`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:
</p>
<ul className="list-disc pl-5">
{membersExceededLimit.map((member, idx: number) => (
<li key={`member-${idx}`}>
{member.username || member.primary_email} (Limit: {member.free_project_limit} free
projects)
</li>
))}
</ul>
<p className="text-sm leading-normal">
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.
</p>
</div>
}
@@ -13,9 +13,10 @@ interface Props {
const DatePickers: React.FC<Props> = ({ to, from, onChange, helpers }) => {
const defaultHelper = getDefaultHelper(helpers)
const [helperValue, setHelperValue] = useState<string>(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<Props> = ({ to, from, onChange, helpers }) => {
return false
}
})
useEffect(() => {
if (selectedHelper && helperValue !== selectedHelper.text) {
setHelperValue(selectedHelper.text)
@@ -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,
@@ -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
@@ -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<Props> = ({
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<Props> = ({
{ 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<Props> = ({
}
}, [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<Props> = ({
},
})
} 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<Props> = ({
<Button onClick={loadOlder} icon={<IconRewind />} type="default">
Load older
</Button>
<UpgradePrompt projectRef={projectRef} from={params.iso_timestamp_start || ''} />
<div className="mt-2 flex flex-row justify-end">
<UpgradePrompt show={showUpgradePrompt} setShowUpgradePrompt={setShowUpgradePrompt} />
</div>
</div>
)}
</div>
@@ -51,11 +51,7 @@ const LogsQueryPanel: React.FC<Props> = ({
return (
<div
className="
rounded
rounded-bl-none rounded-br-none border border-panel-border-light bg-panel-header-light
dark:border-panel-border-dark dark:bg-panel-header-dark
className=" rounded rounded-bl-none rounded-br-none border border-panel-border-light bg-panel-header-light dark:border-panel-border-dark dark:bg-panel-header-dark
"
>
<div className="flex w-full items-center justify-between px-5 py-2">
@@ -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<Props> = ({ projectRef, from }) => {
const [showHelperModal, setShowHelperModal] = useState(false)
const { subscription, isLoading, isError } = useProjectSubscription(projectRef)
if (isLoading) return <IconLoader size={16} className="animate-spin" />
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<Props> = ({ show, setShowUpgradePrompt }) => {
const router = useRouter()
const { ref } = router.query
return (
<>
<div
className={`flex flex-row items-center gap-3 px-2 py-1 text-xs transition-all ${
isExceedingLimit
? 'rounded border border-yellow-700 bg-yellow-200 font-semibold text-yellow-1100'
: ''
}`}
>
<span className="text-scale-1100">{`${queryLimit.text} retention`}</span>
<IconHelpCircle
size={16}
strokeWidth={1.5}
className="cursor-pointer text-scale-1100 transition hover:text-scale-1200"
onClick={() => setShowHelperModal(true)}
/>
{queryLimit.promptUpgrade && (
<Link href={`/project/${projectRef}/settings/billing`}>
<Button size="tiny">Upgrade</Button>
</Link>
)}
</div>
<Modal
hideFooter
visible={showHelperModal}
size="medium"
header="Log retention"
onCancel={() => setShowHelperModal(false)}
>
<div className="space-y-4 py-4">
<Modal.Content>
<div className="space-y-4">
<p className="text-sm">
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.
</p>
<div className="rounded border border-scale-600 bg-scale-500">
<div className="flex items-center px-4 pt-2 pb-1">
<p className="w-[40%] text-sm text-scale-1100">Plan</p>
<p className="w-[60%] text-sm text-scale-1100">Retention duration</p>
<Modal
hideFooter
visible={show}
closable
size="medium"
header="Log retention"
onCancel={() => setShowUpgradePrompt(false)}
>
<div className="space-y-4 py-4">
<Modal.Content>
<div className="space-y-4">
<p className="text-sm">
Logs can be retained up to a duration of 3 months depending on the plan that your
project is on.
</p>
<div className="border-scale-600 bg-scale-500 rounded border">
<div className="flex items-center px-4 pt-2 pb-1">
<p className="text-scale-1100 w-[40%] text-sm">Plan</p>
<p className="text-scale-1100 w-[60%] text-sm">Retention duration</p>
</div>
<div className="py-1">
<div className="flex items-center px-4 py-1">
<p className="w-[40%] text-sm">Free</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.FREE.text}</p>
</div>
<div className="py-1">
<div className="flex items-center px-4 py-1">
<p className="w-[40%] text-sm">Free</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.FREE.text}</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[40%] text-sm">Pro</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.PRO.text}</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[40%] text-sm">Enterprise</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.ENTERPRISE.text}</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[40%] text-sm">Pro</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.PRO.text}</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[40%] text-sm">Enterprise</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.ENTERPRISE.text}</p>
</div>
</div>
</div>
</Modal.Content>
<Modal.Seperator />
<Modal.Content>
<div className="flex items-center gap-2">
<Button block type="primary" onClick={() => setShowHelperModal(false)}>
Understood
</Button>
</div>
</Modal.Content>
</div>
</Modal>
</>
</div>
</Modal.Content>
<Modal.Seperator />
<Modal.Content>
<div className="flex justify-between">
<Button type="default" onClick={() => setShowUpgradePrompt(false)}>
Close
</Button>
<Link href={`/project/${ref}/settings/billing`}>
<Button size="tiny">Upgrade</Button>
</Link>
</div>
</Modal.Content>
</div>
</Modal>
)
}
@@ -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')
@@ -27,6 +27,7 @@ const AccountLayout: FC<Props> = ({ children, title, breadcrumbs }) => {
const onClickLogout = async () => {
await auth.signOut()
localStorage.clear();
router.reload()
}
@@ -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: <IconUsers size={18} strokeWidth={2} />,
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: <IconArchive size={18} strokeWidth={2} />,
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: <IconDatabase size={18} strokeWidth={2} />,
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: <IconCode size={18} strokeWidth={2} />,
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: <IconList size={18} strokeWidth={2} />,
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: <IconBarChart size={18} strokeWidth={2} />,
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: <IconFileText size={18} strokeWidth={2} />,
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/api`),
link:
ref &&
(isProjectPaused ? homeUrl : isProjectBuilding ? buildingUrl : `/project/${ref}/api`),
},
...(IS_PLATFORM
? [
@@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ project }) => {
return (
<>
<div className="space-y-4">
{hasExceedActiveFreeProjectsLimit && (
<div className="mx-6">
<Alert
withIcon
variant="warning"
title={`Your account can only have ${freeProjectsLimit} active free projects`}
>
To restore this project you'll need to pause or delete an existing free project.
</Alert>
</div>
)}
<div className="mx-auto mb-16 w-full max-w-7xl">
<div className="mx-6 flex h-[500px] items-center justify-center rounded border border-scale-400 bg-scale-300 p-8">
<div className="grid w-[420px] gap-4">
@@ -76,7 +66,7 @@ const ProjectPausedState: FC<Props> = ({ project }) => {
<Button
size="tiny"
type="primary"
disabled={hasExceedActiveFreeProjectsLimit || !canResumeProject}
disabled={!canResumeProject}
onClick={onSelectRestore}
>
Restore project
@@ -101,11 +91,9 @@ const ProjectPausedState: FC<Props> = ({ project }) => {
<DeleteProjectButton type="default" />
</div>
{!hasExceedActiveFreeProjectsLimit && (
<p className="mt-4 text-sm text-scale-1000">
Restore this project and get back to building the next big thing!
</p>
)}
<p className="mt-4 text-sm text-scale-1000">
Restore this project and get back to building the next big thing!
</p>
</div>
</div>
</div>
@@ -119,6 +107,49 @@ const ProjectPausedState: FC<Props> = ({ project }) => {
onSelectCancel={() => setShowConfirmRestore(false)}
onSelectConfirm={onConfirmRestore}
/>
<Modal
hideFooter
visible={showFreeProjectLimitWarning}
size="medium"
header="Your organization has members who have exceeded their free project limits"
onCancel={() => setShowFreeProjectLimitWarning(false)}
>
<div className="space-y-4 py-4">
<Modal.Content>
<div className="space-y-2">
<p className="text-sm text-scale-1100">
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:
</p>
<ul className="list-disc pl-5 text-sm text-scale-1100">
{(membersExceededLimit || []).map((member, idx: number) => (
<li key={`member-${idx}`}>
{member.username || member.primary_email} (Limit: {member.free_project_limit}{' '}
free projects)
</li>
))}
</ul>
<p className="text-sm text-scale-1100">
These members will need to either delete, pause, or upgrade one or more of these
projects before you're able to unpause this project.
</p>
</div>
</Modal.Content>
<Modal.Seperator />
<Modal.Content>
<div className="flex items-center gap-2">
<Button
htmlType="button"
type="default"
onClick={() => setShowFreeProjectLimitWarning(false)}
block
>
Understood
</Button>
</div>
</Modal.Content>
</div>
</Modal>
</>
)
}
+23
View File
@@ -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 }
}
+1
View File
@@ -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'
@@ -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<any>(slug ? url : null, get)
const anyError = data?.error || error
return {
membersExceededLimit: anyError ? undefined : (data as MemberWithFreeProjectLimit[]),
error: anyError,
isLoading: !anyError && !data,
isError: !!anyError,
}
}
+38 -23
View File
@@ -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 (
<Panel
hideHeaderStyling
loading={!app.organizations.isInitialized}
loading={
!app.organizations.isInitialized ||
subscriptionStats.isLoading ||
isLoadingFreeProjectLimitCheck
}
title={
<div key="panel-title">
<h3>Create a new project</h3>
@@ -243,7 +253,12 @@ const Wizard: NextPageWithLayout = () => {
</Panel.Content>
) : (
<>
<Panel.Content className="Form section-block--body has-inputs-centered space-y-4 border-t border-b border-panel-border-interior-light dark:border-panel-border-interior-dark">
<Panel.Content
className={[
'Form section-block--body has-inputs-centered space-y-4 border-t border-b',
'border-panel-border-interior-light dark:border-panel-border-interior-dark',
].join(' ')}
>
{organizations.length > 0 && (
<Listbox
label="Organization"
@@ -266,9 +281,15 @@ const Wizard: NextPageWithLayout = () => {
{!isAdmin && <NotOrganizationOwnerWarning />}
</Panel.Content>
{canCreateProject && (
<>
<Panel.Content className="Form section-block--body has-inputs-centered border-t border-b border-panel-border-interior-light dark:border-panel-border-interior-dark">
<Panel.Content
className={[
'Form section-block--body has-inputs-centered border-t border-b',
'border-panel-border-interior-light dark:border-panel-border-interior-dark',
].join(' ')}
>
<Input
id="project-name"
layout="horizontal"
@@ -335,6 +356,7 @@ const Wizard: NextPageWithLayout = () => {
</Panel.Content>
</>
)}
{isAdmin && (
<Panel.Content className="Form section-block--body has-inputs-centered ">
<Listbox
@@ -376,8 +398,8 @@ const Wizard: NextPageWithLayout = () => {
})}
</Listbox>
{isSelectFreeTier && isOverFreeProjectLimit && (
<FreeProjectLimitWarning limit={freeProjectsLimit} />
{isSelectFreeTier && hasMembersExceedingFreeTierLimit && (
<FreeProjectLimitWarning membersExceededLimit={membersExceededLimit || []} />
)}
{!isSelectFreeTier && isEmptyPaymentMethod && (
@@ -385,13 +407,6 @@ const Wizard: NextPageWithLayout = () => {
)}
</Panel.Content>
)}
{subscriptionStats.isLoading && (
<Panel.Content>
<div className="flex items-center justify-center py-10">
<IconLoader size={16} className="animate-spin" />
</div>
</Panel.Content>
)}
</>
)}
</>
@@ -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<boolean>(false)
const [warnings, setWarnings] = useState<LogsWarning[]>([])
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}
/>
<div className="h-48 min-h-[7rem]">
<ShimmerLine active={isLoading} />
<CodeEditor
@@ -158,7 +182,7 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
</div>
</LoadingOpacity>
<div className="mt-2 flex flex-row justify-end">
<UpgradePrompt projectRef={ref as string} from={params.iso_timestamp_start || ''} />
<UpgradePrompt show={showUpgradePrompt} setShowUpgradePrompt={setShowUpgradePrompt} />
</div>
</div>
</div>
@@ -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 = () => {
<Modal
hideFooter
visible={showDowngradeError}
size="small"
header="Free tier limit exceeded"
size="medium"
header="Your organization has members who have exceeded their free project limits"
onCancel={() => setShowDowngradeError(false)}
>
<div className="space-y-4 py-4">
<Modal.Content>
<p className="text-sm text-scale-1100">
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.
</p>
<div className="space-y-2">
<p className="text-sm text-scale-1100">
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:
</p>
<ul className="list-disc pl-5 text-sm text-scale-1100">
{(membersExceededLimit || []).map((member, idx: number) => (
<li key={`member-${idx}`}>
{member.username || member.primary_email} (Limit: {member.free_project_limit}{' '}
free projects)
</li>
))}
</ul>
<p className="text-sm text-scale-1100">
These members will need to either delete, pause, or upgrade one or more of these
projects before you're able to downgrade this project.
</p>
</div>
</Modal.Content>
<Modal.Seperator />
<Modal.Content>
@@ -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(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
await screen.findByText('Log retention') // assert modal title is present
})
})
+41 -15
View File
@@ -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(<LogsExplorerPage />)
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(<LogsExplorerPage />)
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(<LogsExplorerPage />)
await screen.findByText(/Log retention/) // assert modal title is present
})
test('based on datepicker helpers', async () => {
render(<LogsExplorerPage />)
// 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()
}
})
})
+10 -9
View File
@@ -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<UserResponse>[] = []
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)
}
+12
View File
@@ -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' } })
})
+319
View File
@@ -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
}
}
+129 -116
View File
@@ -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')
}
}
@@ -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
}
}
+23 -1
View File
@@ -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')
}
}
+98 -17
View File
@@ -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<AuthResponse> {
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<AuthResponse> {
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<UserResponse> {
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 }
}
}
@@ -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()
}
}
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff. Show more