Merge branch 'master' into patch-1

This commit is contained in:
Copple authored and GitHub committed 2023-09-06 18:07:17 +02:00
commit fca89fed88
105 files changed
+1656 -325

No files matched your search

+2 -2
View File
@@ -1,7 +1,7 @@
import Link from 'next/link'
import React, { useState } from 'react'
import { GlassPanel, IconLink, IconX, Input } from 'ui'
import extensions from '../data/extensions.json'
import { extensions } from 'shared-data'
import { GlassPanel, IconX, Input } from 'ui'
type Extension = {
name: string
+33 -11
View File
@@ -47,12 +47,12 @@ You can access the Supabase Dashboard through the API gateway on port `8000`. Fo
You will be prompted for a username and password. By default, the credentials are:
- Username: `supabase`
- Username: `supabase`
- Password: `this_password_is_insecure_and_should_be_updated`
You should change these credentials as soon as possible using the [instructions](#dashboard-authentication) below.
### Accessing the APIs
### Accessing the APIs
Each of the APIs are available through the the same API gateway:
@@ -61,9 +61,9 @@ Each of the APIs are available through the the same API gateway:
- Storage: `http://<your-domain>:8000/storage/v1/`
- Realtime: `http://<your-domain>:8000/realtime/v1/`
### Accessing your Edge Functions
### Accessing your Edge Functions
Edge Functions are stored in `volumes/functions`. The default setup has a `hello` Function that you can invoke on `http://<your-domain>:8000/functions/v1/hello`.
Edge Functions are stored in `volumes/functions`. The default setup has a `hello` Function that you can invoke on `http://<your-domain>:8000/functions/v1/hello`.
You can add new Functions as `volumes/functions/<FUNCTION_NAME>/index.ts`. Restart the `functions` service to pick up the changes: `docker compose restart functions --no-deps`
@@ -84,7 +84,7 @@ While we provided you with some example secrets for getting started, you should
### Generate API Keys
Create a new `JWT_SECRET` and store it securely.
Create a new `JWT_SECRET` and store it securely.
We can use your JWT Secret to generate new `anon` and `service` API keys using the form below. Update the "JWT Secret" and then run "Generate JWT" once for the `SERVICE_KEY` and once for the `ANON_KEY`:
@@ -114,8 +114,8 @@ You will need to [restart](#restarting-all-services) the services for the change
The dashboard is protected with Basic Authentication. The default user and password MUST be updated before using Supabase in production.
Update the following values in the `.env` file:
- `DASHBOARD_USERNAME`: The default username for the Dashboard
- `DASHBOARD_PASSWORD`: The default password for the Dashboard
- `DASHBOARD_USERNAME`: The default username for the Dashboard
- `DASHBOARD_PASSWORD`: The default password for the Dashboard
You can also add more credentials in `./docker/volumes/api/kong.yml`. For example:
@@ -149,13 +149,13 @@ You can stop Supabase by running `docker compose stop` in same directory as your
You can stop Supabase by running the following in same directory as your `docker-compose.yml` file:
```sh
```sh
# Stop docker and remove volumes:
docker compose down -v
# Remove Postgres data:
rm -rf volumes/db/data/
```
```
This will destroy all data in the database and storage volumes, so be careful!
@@ -163,7 +163,7 @@ This will destroy all data in the database and storage volumes, so be careful!
Each system can be [configured](../self-hosting#configuration) independently. Some of the most common configuration options are listed below.
### Configuring an email server
### Configuring an email server
You will need to use a production-ready SMTP server for sending emails. You can configure the SMTP server by updating the the following environment variables:
@@ -178,7 +178,7 @@ SMTP_SENDER_NAME=
We recommend using [AWS SES](https://aws.amazon.com/ses/). It's extremely cheap and reliable. Restart all services to pick up the new configuration.
### Configuring S3 Storage
### Configuring S3 Storage
By default all files are stored locally on the server. You can connfigure the Storage service to use S3 by updating the following environment variables:
@@ -216,7 +216,29 @@ By default, Storage backend is set to `file`, which is to use local files as the
Additional configuration is required for self-hosting the Analytics server. For the full setup instructions, see [Self Hosting Analytics](https://supabase.com/docs/reference/self-hosting-analytics/introduction#getting-started).
### Upgrading Analytics
Due to the changes in the Analytics server, you will need to run the following commands to upgrade your Analytics server:
<Admonition type="warning">
- All data in analytics will be deleted when you run the above commands.
</Admonition>
```sh
### Destroy analytics to transition to postgres self hosted solution without other data loss
# Enter the container and use your .env POSTGRES_PASSWORD value to login
docker exec -it $(docker ps | grep supabase-db | awk '{print $1}') psql -U supabase_admin --password
# Drop all the data in the _analytics schema
DROP PUBLICATION logflare_pub; DROP SCHEMA _analytics CASCADE; CREATE SCHEMA _analytics;\q
# Drop the analytics container
docker rm supabase-db
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -387,4 +387,4 @@ The [pgvector extension](https://supabase.com/docs/guides/ai/vector-columns) is
- [How to build ChatGPT Plugin from scratch with Supabase Edge Runtime](https://supabase.com/blog/building-chatgpt-plugins-template)
- [Docs pgvector: Embeddings and vector similarity](https://supabase.com/docs/guides/database/extensions/pgvector)
- [Choosing Compute Add-on for AI workloads](https://supabase.com/docs/guides/ai/choosing-compute-addon)
- [pgvector v0.5.0: Faster semantic search with HNSW indexes](https://supabase.com/blog/pgvector-v0-5-0-hnsw)
- [pgvector v0.5.0: Faster semantic search with HNSW indexes](https://supabase.com/blog/increase-performance-pgvector-hnsw)
@@ -123,7 +123,7 @@ Want to try Supabase Clippy? It's a hidden feature while in MVP - visit [supabas
## More pgvector and ChatGPT resources
- [pgvector docs]([https://supabase.com/blog/chatgpt-supabase-docs](https://supabase.com/docs/guides/getting-started/openai/vector-search)
- [AI docs](https://supabase.com/docs/guides/ai)
- [A ChatGPT Plugins Template built with Supabase Edge Runtime](https://supabase.com/blog/building-chatgpt-plugins-template)
- [Template for building your own custom ChatGPT style doc search](https://github.com/supabase-community/nextjs-openai-doc-search)
- [Supabase + LangChain Starter Template](https://blog.langchain.dev/langchain-x-supabase/)
@@ -1,6 +1,6 @@
---
title: 'pgvector 0.4.0 performance'
description: There's been lot of talk about pgvector performance lately, so we took some datasets and pushed pgvector to the limits to find out its strengths and limitations.
description: There's been a lot of talk about pgvector performance lately, so we took some datasets and pushed pgvector to the limits to find out its strengths and limitations.
categories:
- engineering
tags:
@@ -17,11 +17,10 @@ thumb: 2023-07-13-pgvector-performance/vector-benchmarks-thumb.jpeg
<div className="bg-gray-300 rounded-lg px-6 py-2 bold">
🚀 The incorporation of the HNSW index in pgvector v0.5.0 ensures lightning-fast vector searches. We tested it and benchmarked and share everything.
[Read the new post](https://supabase.com/blog/pgvector-v0-5-0-hnsw)
🚀 The incorporation of the HNSW index in pgvector v0.5.0 ensures lightning-fast vector searches. We tested it, benchmarked it, and shared everything.
[Read the new post](https://supabase.com/blog/increase-performance-pgvector-hnsw)
</div>
pgvector v0.5.0: Faster semantic search with HNSW indexes
There are a few pgvector benchmarks floating around the internet, most recently a [pgvector vs Qdrant](https://nirantk.com/writing/pgvector-vs-qdrant/) comparison by NirantK. We wanted to reproduce (or improve!) the results.
@@ -31,7 +30,7 @@ Our goals in this article are:
1. To show the strengths and limitations of the _current version_ of pgvector.
2. Highlight some improvements that are coming to pgvector.
3. Prove to you that it's completely viable for production workloads and give you some tips on using it at scale. We'll show you how to run 1 million Open AI embeddings at ~1800 requests per second with 91% precision, or 670 requests per second with 98% precision.
3. Prove to you that it's completely viable for production workloads and give you some tips on using it at scale. We'll show you how to run 1 million Open AI embeddings at ~1800 queries per second with 91% accuracy, or 670 queries per second with 98% accuracy.
## Benchmark Methodology
@@ -40,7 +39,7 @@ We've used the [ANN Benchmarks](https://github.com/erikbern/ann-benchmarks) meth
The key elements are:
- **Helper scripts:** a Python test runner which is responsible for data upload, index creation, and query execution. This uses [qdrant's vector-db-benchmark](https://github.com/qdrant/vector-db-benchmark) repo. The “engine” in this repo uses [Vecs](https://github.com/supabase/vecs), a Python client for pgvector.
- **Runtime:** Each test runs for at least 30-40 minutes and included a series of experiments executed at various concurrency levels. This process allowed us to gauge the engine's performance under different load types. Subsequently, we averaged the results.
- **Runtime:** Each test runs for at least 30-40 minutes and includes a series of experiments executed at various concurrency levels. This process allowed us to gauge the engine's performance under different load types. Subsequently, we averaged the results.
- **Pre-warming RAM:** We executed 10,000 to 50,000 “warm-up” queries before each benchmark, matching the number of probes as the benchmark. Additionally, we executed about 1,000 queries with probes ranging from three to ten times the benchmark's probes. Both of these help with RAM utilization.
<div>
@@ -119,8 +118,8 @@ The resulting figures were significantly different after these changes.
With the changes above and probes set to 10, pgvector was faster and more accurate:
- precision@10 of 0.91
- RPS (requests per second) of 380
- accuracy@10 of 0.91
- QPS (queries per second) of 380
<div>
<img
@@ -137,10 +136,10 @@ With the changes above and probes set to 10, pgvector was faster and more accura
### PROBES = 40
If we increase the probes from 10 to 40, pgvector was not just substantially faster but also boasted almost the same precision as Qdrant:
If we increase the probes from 10 to 40, pgvector was not just substantially faster but also boasted almost the same accuracy as Qdrant:
- precision@10 of 0.98
- RPS of 140
- accuracy@10 of 0.98
- QPS of 140
<div>
<img
@@ -157,7 +156,7 @@ If we increase the probes from 10 to 40, pgvector was not just substantially fas
### Scaling the database
Another key takeaway is that the performance scales predictably with the size of the database. For instance, a 4XL instance achieves precision@10 of 0.98 and RPS of 270 with probes set to 40. Moreover, an 8XL compute add-on analogously obtains precision@10 of 0.98 and an RPS of 470, surpassing the results of Qdrant.
Another key takeaway is that the performance scales predictably with the size of the database. For instance, a 4XL instance achieves accuracy@10 of 0.98 and QPS of 270 with probes set to 40. Moreover, an 8XL compute add-on analogously obtains accuracy@10 of 0.98 and an QPS of 470, surpassing the results of Qdrant.
<div className="bg-gray-300 rounded-lg px-6 py-2 italic">
@@ -178,13 +177,13 @@ The Qdrant benchmark uses “default” configuration and is in not indicative o
/>
</div>
Although more compute is required to match Qdrant's precision and RPS levels concurrently, this is still a satisfying outcome. It means that it's not a _necessity_ to use another vector database. You can put everything in Postgres to lower your operational complexity.
Although more compute is required to match Qdrant's accuracy and QPS levels concurrently, this is still a satisfying outcome. It means that it's not a _necessity_ to use another vector database. You can put everything in Postgres to lower your operational complexity.
### Final results: pgvector performance
Putting it all together, we find that we can predictably scale our database to match the performance we need.
With a 64-core, 256 GB server we achieve ~1800 RPS and 0.91 precision. This is for pgvector 0.4.0, and we've heard that the latest version (0.4.4) already has significant improvements. We'll release those benchmarks as soon as we have them.
With a 64-core, 256 GB server we achieve ~1800 QPS and 0.91 accuracy. This is for pgvector 0.4.0, and we've heard that the latest version (0.4.4) already has significant improvements. We'll release those benchmarks as soon as we have them.
<div>
<img
@@ -209,7 +208,7 @@ Another way to improve performance without throwing more compute would be to inc
We ran a test to measure the impact of list size: we uploaded 90,000 vectors from the Wikipedia dataset and then queried 10,000 vectors from the same dataset. The documentation recommends to use `lists` constant of `number of vectors / 1000`. In this case, it would be 90.
But as our experiment shows, we can improve RPS if we increase `lists` (i.e. with more lists in the index we need to get less index data to get the same precision). So for 95% precision, we can take any of:
But as our experiment shows, we can improve QPS if we increase `lists` (i.e. with more lists in the index we need to get less index data to get the same accuracy). So for 95% accuracy, we can take any of:
- 3% of index data = 270 lists
- 6% of index data = 90 lists
@@ -257,11 +256,11 @@ SET maintenance_work_mem TO '7168 MB';
Keeping in mind that the overall index size is almost the same, and only index build time increases, we can say that it's better to use more lists for better select queries speed.
### Real data has higher precision than random data
### Real data has higher accuracy than random data
Embeddings created from “real” data are more likely to be clustered together, whereas random embeddings are more likely to be scattered. In other words, real embeddings are very far from being randomly distributed. This might seem obvious, but it's an important call-out for benchmarks.
Embeddings generated for similarity search using “real world data” will be more correlated, so the precision will be higher as well. You can see the difference in this chart using 10,000 Wikipedia embeddings, vs 10,000 randomly-generated embeddings:
Embeddings generated for similarity search using “real world data” will be more correlated, so the accuracy will be higher as well. You can see the difference in this chart using 10,000 Wikipedia embeddings, vs 10,000 randomly-generated embeddings:
<div>
<img
@@ -288,7 +287,7 @@ First, a few generic tips which you can pick and choose from:
2. Prefer `inner-product` to `L2` or `Cosine` distances if your vectors are normalized (like `text-embedding-ada-002`). If embeddings are not normalized, `Cosine` distance should give the best results with an index.
3. **Pre-warm your database.** Implement the warm-up technique we described earlier before transitioning to production.
4. **Establish your workload.** Increasing the lists constant for the pgvector index can accelerate your queries (at the expense of a slower build). For instance, for benchmarks with OpenAI embeddings, we employed a `lists` constant of 2000 (`number of vectors / 500`) as opposed to the suggested 1000 (`number of vectors / 1000`).
5. **Benchmark your own specific workloads.** Doing this during cache warm-up helps gauge the best value for the `probes` constant, balancing precision with RPS.
5. **Benchmark your own specific workloads.** Doing this during cache warm-up helps gauge the best value for the `probes` constant, balancing accuracy with QPS.
### Going into production
@@ -296,12 +295,12 @@ Before running your pgvector workload in production, here are a few steps you ca
1. Over-provision RAM during preparation. You can scale down in step `5`, but it's better to start with a larger size to get the best results for RAM requirements. (We'd recommend at least 8XL if you're using Supabase.)
2. Upload your data to the database. If you use [`vecs`](https://supabase.com/docs/guides/ai/python/api) library, it will automatically generate an index with default parameters.
3. Run a benchmark using randomly generated queries and see the results. Again, you can use `vecs` library with the `ann-benchmarks` tool. Do it with probes set to 10 (default) and then with probes set to 100 or more, so RPS will be lower than 10.
3. Run a benchmark using randomly generated queries and see the results. Again, you can use `vecs` library with the `ann-benchmarks` tool. Do it with probes set to 10 (default) and then with probes set to 100 or more, so QPS will be lower than 10.
4. Take a look at the RAM usage, and save it as a note for yourself. You would likely want to use compute add-on in the future that would have the same amount of RAM as used at the moment (both actual RAM usage and RAM used for cache and buffers).
5. Scale down your compute add-on to the one that would have the same amount of RAM as used at the moment.
6. Repeat step 3. to load the data into RAM. You should see that RPS is increased on subsequent runs, and stop when it no longer increases. Then repeat the benchmark with probes set to a higher value as well if you didn't do it before on that compute add-on size.
7. Run a benchmark using real queries and see the results. You can use `vecs` library for that as well with `ann-benchmarks` tool. Do it with probes set to 10 (default) and then gradually increase/decrease probes value until you see that both precision and RPS match your requirements.
8. If you want higher RPS and you don't expect to have frequent inserts and reindexing, you can increase `lists` constantly. You have to rebuild the index with higher lists value and repeat steps 6-7 to find the best combination of `lists` and `probes` constants to achieve the best RPS and precision values. Higher `lists` mean that index will build slower, but you can achieve better RPS and precision. Higher probes mean that select queries will be slower, but you can achieve better precision.
6. Repeat step 3. to load the data into RAM. You should see that QPS is increased on subsequent runs, and stop when it no longer increases. Then repeat the benchmark with probes set to a higher value as well if you didn't do it before on that compute add-on size.
7. Run a benchmark using real queries and see the results. You can use `vecs` library for that as well with `ann-benchmarks` tool. Do it with probes set to 10 (default) and then gradually increase/decrease probes value until you see that both accuracy and QPS match your requirements.
8. If you want higher QPS and you don't expect to have frequent inserts and reindexing, you can increase `lists` constantly. You have to rebuild the index with higher lists value and repeat steps 6-7 to find the best combination of `lists` and `probes` constants to achieve the best QPS and accuracy values. Higher `lists` mean that index will build slower, but you can achieve better QPS and accuracy. Higher probes mean that select queries will be slower, but you can achieve better accuracy.
## The pgvector roadmap
@@ -309,7 +308,7 @@ pgvector is still early in development. As with any open source tool, it needs t
What's next on the roadmap? Andrew has an impressive list of features [planned for v0.5.0](https://github.com/pgvector/pgvector/issues/27):
✅ Adding HNSW: an index with better speed & precision than IVFFlat (at a higher memory cost)
✅ [Adding HNSW](https://supabase.com/blog/increase-performance-pgvector-hnsw): an index with better speed & accuracy than IVFFlat (at a higher memory cost)
- Product quantization: better storage for IVFFLAT, improving speed and recall
- Parallel index builds: building your IVFFLAT indexes will be much faster
@@ -25,7 +25,7 @@ At Supabase, we support storing embeddings in Postgres using the [pgvector](http
## Challenges with pgvector
Without indexes, pgvector performs a full table scan when you run a similarity query. This means distance has to be computed against every row in your table. This is manageable at a small scale, but becomes problematic as your table grows.
Without indexes, pgvector performs a full table scan when you run a similarity query. This means distance has to be computed against every row in your table. This is manageable at a small scale but becomes problematic as your table grows.
To solve this, pgvector offers indexes. Indexes reorganize the data into data structures that exploit internal structure and enable approximate similarity search without referring to every record. Currently, pgvector supports an IVF index, with HNSW expected in the next release.
@@ -35,7 +35,7 @@ IVF [indexes](https://supabase.com/docs/guides/ai/managing-indexes) work by clus
98% of our customers are generating text embeddings using OpenAI's `text-embedding-ada-002` model. At an initial glance, there's good reason for this - these embeddings perform quite well for information retrieval and are economical to produce. `text-embedding-ada-002` produces vectors with 1536 dimensions which is among the largest in the industry. IVF indexes help address some scaling challenges, but there are still some pitfalls.
First, vectors are large. A 1536 dimensional vector is ~12.3 kilobytes. Scaling that up to 1M vectors, the raw data tops 11 gigabytes. Experienced SQL users know that for best performance, indexes should fit within system memory. Moreover unlike traditional workloads, there is a significant compute component when performing vector similarity queries.
First, vectors are large. A 1536 dimensional vector is ~12.3 kilobytes. Scaling that up to 1M vectors, the raw data tops 11 gigabytes. Experienced SQL users know that for best performance, indexes should fit within system memory. Moreover, unlike traditional workloads, there is a significant compute component when performing vector similarity queries.
In the real-world it's common for an index to reduce the number of distance computations needed to estimate nearest neighbors from 100% of the dataset to 5-20%. At 1M records, that's still 50k-200k distance calculations being performed for a single query. Given how different the [resource requirements](https://supabase.com/docs/guides/ai/choosing-compute-addon) are to support heavy vector workloads, it's not surprising that one of the most common issues we see is significant under provisioning of hardware.
@@ -54,7 +54,7 @@ Text embeddings are one of the most common types of embeddings today. Our friend
Models are ranked per task by taking their average score produced over each task's datasets. Each model also has an overall (general purpose) score, calculated by taking the average score produced across all datasets. You can find the results on their [MTEB leaderboard](https://huggingface.co/spaces/mteb/leaderboard).
It's worth pointing out that each model's dimension size has little-to-no correlation with its performance. In fact there are a number of models that perform comparably with`text-embedding-ada-002`, all of which produce embeddings with fewer dimensions than 1536.
It's worth pointing out that each model's dimension size has little-to-no correlation with its performance. In fact, there are a number of models that perform comparably with`text-embedding-ada-002`, all of which produce embeddings with fewer dimensions than 1536.
| Rank | Model | Dimensions | Average | Model Size (GB) |
| ---- | -------------------------------------------------------------------------------------------------- | ---------- | ------- | --------------- |
@@ -68,7 +68,7 @@ It's worth pointing out that each model's dimension size has little-to-no correl
## Benefits of fewer dimensions
What specifically do we gain when we have fewer dimensions? Faster queries and less RAM: Fewer dimensions means less computation while more of the dataset or index is able to fit in memory.
What specifically do we gain when we have fewer dimensions? Faster queries and less RAM: Fewer dimensions mean less computation while more of the dataset or index is able to fit in memory.
Take a look at dot product for example:
@@ -85,9 +85,9 @@ Take a look at dot product for example:
/>
</div>
Dot product is the product of each vector element pair summed together into a single result. Fewer dimensions in the vector means fewer calculations for every computed distance .
Dot product is the product of each vector element pair summed together into a single result. Fewer dimensions in the vector means fewer calculations for every computed distance.
We compared the performance of `text-embedding-ada-002` from OpenAI (1536 dimensions) with open-source `all-MiniLM-L6-v2` (384 dimensions) by measuring requests per second at a constant precision and configuration:
We compared the performance of `text-embedding-ada-002` from OpenAI (1536 dimensions) with open-source `all-MiniLM-L6-v2` (384 dimensions) by measuring queries per second at a constant accuracy and configuration:
- **Database size:** with 2vCPU (ARM) and 8GB RAM - `large` add-on for Supabase project.
- **Version:** Postgres v15 and pgvector v0.4.0
@@ -95,7 +95,7 @@ We compared the performance of `text-embedding-ada-002` from OpenAI (1536 dimens
- **Index:** The index was generated for `inner-product` (dot product) distance function with `lists=1000`.
- **Process:** We followed [our optimization guide and tips](https://supabase.com/docs/guides/ai/going-to-prod#performance-tips-when-using-indexes).
We observed pgvector with `all-MiniLM-L6-v2` outperforming `text-embedding-ada-002` by 78% when holding the precision@10 constant at 0.99. This gap increases as you lower the precision. Postgres was using just 4GB of RAM with 384d vectors generated by `all-MiniLM-L6-v2` compared to 7.5GB with `text-embedding-ada-002`.
We observed pgvector with `all-MiniLM-L6-v2` outperforming `text-embedding-ada-002` by 78% when holding the accuracy@10 constant at 0.99. This gap increases as you lower the accuracy. Postgres was using just 4GB of RAM with 384d vectors generated by `all-MiniLM-L6-v2` compared to 7.5GB with `text-embedding-ada-002`.
<div>
<img
@@ -123,7 +123,7 @@ We observed pgvector with `all-MiniLM-L6-v2` outperforming `text-embedding-ada-0
/>
</div>
After that, we decided to try the recently published [gte-small](https://huggingface.co/thenlper/gte-small) (also 384 dimensions), and the results were even more astonishing. With `gte-small`, we could set `probes=10` to achieve the same level of `precision@10 = 0.99`. Consequently, we observed more than a 200% improvement in requests per second for pgvector with embeddings generated by `gte-small` compared to `all-MiniLM-L6-v2`.
After that, we decided to try the recently published [gte-small](https://huggingface.co/thenlper/gte-small) (also 384 dimensions), and the results were even more astonishing. With `gte-small`, we could set `probes=10` to achieve the same level of `accuracy@10 = 0.99`. Consequently, we observed more than a 200% improvement in queries per second for pgvector with embeddings generated by `gte-small` compared to `all-MiniLM-L6-v2`.
<div>
<img
@@ -176,4 +176,4 @@ At Supabase we are working on ways to make it easy to generate embeddings using
- [How to build ChatGPT Plugin from scratch with Supabase Edge Runtime](https://supabase.com/blog/building-chatgpt-plugins-template)
- [Docs pgvector: Embeddings and vector similarity](https://supabase.com/docs/guides/database/extensions/pgvector)
- [Choosing Compute Add-on for AI workloads](https://supabase.com/docs/guides/ai/choosing-compute-addon)
- [pgvector v0.5.0: Faster semantic search with HNSW indexes](https://supabase.com/blog/pgvector-v0-5-0-hnsw)
- [pgvector v0.5.0: Faster semantic search with HNSW indexes](https://supabase.com/blog/increase-performance-pgvector-hnsw)
@@ -0,0 +1,266 @@
---
title: 'pgvector v0.5.0: Faster semantic search with HNSW indexes'
description: Increase performance in pgvector using HNSW indexes
author: gregnr
categories:
- engineering
tags:
- AI
- performance
- postgres
- planetpg
date: '2023-09-06'
toc_depth: 3
image: 2023-09-06-increase-performance-pgvector-hnsw/pgvector-v0-5-0-og.png
thumb: 2023-09-06-increase-performance-pgvector-hnsw/pgvector-v0-5-0-thumb.png
---
_Contributed by: [Egor Romanov](https://github.com/egor-romanov)_
[Supabase Vector](https://supabase.com/vector) is about to get a lot faster. Starting today, new Supabase databases will ship with pgvector v0.5.0 which adds a new type of index: Hierarchical Navigable Small World (HNSW).
HNSW is an algorithm for approximate nearest neighbor search, often used in high-dimensional spaces like those found in embeddings.
With this update, you can take advantage of the new HNSW index on your column using the following:
```sql
-- Add a HNSW index for the inner product distance function
CREATE INDEX ON documents
USING hnsw (embedding vector_ip_ops);
```
<aside className="bg-gray-300 rounded-lg px-6 py-2 bold">
💡 If you have an existing database that was created before this release, please reach out to [support](https://supabase.com/dashboard/support/new) and we'll assist you in an upgrade to pgvector v0.5.0.
</aside>
## How does HNSW work?
Compared to inverted file (IVF) indexes which use [clusters](https://supabase.com/docs/guides/ai/managing-indexes#ivfflat-indexes) to approximate nearest-neighbor search, HNSW uses proximity graphs (graphs connecting nodes based on distance between them). To understand HNSW, we can break it down into 2 parts:
- **Hierarchical (H):** The algorithm operates over multiple layers
- **Navigable Small World (NSW):** Each vector is a node within a graph and is connected to several other nodes
### Hierarchical
The hierarchical aspect of HNSW builds off of the idea of skip lists.
Skip lists are multi-layer linked lists. The bottom layer is a regular linked list connecting an ordered sequence of elements. Each new layer above removes some elements from the underlying layer (based on a fixed probability), producing a sparser subsequence that “skips” over elements.
<div>
<img
alt="visual of an example skip list (light)"
className="dark:hidden"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/pgvector-v0-5-0-skip-list--light.png"
/>
<img
alt="visual of an example skip list (dark)"
className="hidden dark:block"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/pgvector-v0-5-0-skip-list--dark.png"
/>
</div>
When searching for an element, the algorithm begins at the top layer and traverses its linked list horizontally. If the target element is found, the algorithm stops and returns it. Otherwise if the next element in the list is greater than the target (or NIL), the algorithm drops down to the next layer below. Since each layer below is less sparse than the layer above (with the bottom layer connecting all elements), the target will eventually be found. Skip lists offer O(log n) average complexity for both search and insertion/deletion.
### Navigable Small World
A navigable small world (NSW) is a special type of proximity graph that also includes long-range connections between nodes. These long-range connections support the “small world” property of the graph, meaning almost every node can be reached from any other node within a few hops. Without these additional long-range connections, many hops would be required to reach a far-away node.
<Img
alt="visual of an example navigable small world graph"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/pgvector-v0-5-0-nsw.png"
/>
The “navigable” part of NSW specifically refers to the ability to logarithmically scale the greedy search algorithm on the graph, an algorithm that attempts to make only the locally optimal choice at each hop. Without this property, the graph may still be considered a small world with short paths between far-away nodes, but the greedy algorithm tends to miss them. Greedy search is ideal for NSW because it is quick to navigate and has low computational costs.
### **Hierarchical +** Navigable Small World
HNSW combines these two concepts. From the hierarchical perspective, the bottom layer consists of a NSW made up of short links between nodes. Each layer above “skips” elements and creates longer links between nodes further away from each other.
Just like skip lists, search starts at the top layer and works its way down until it finds the target element. However instead of comparing a scalar value at each layer to determine whether or not to descend to the layer below, a multi-dimensional distance measure (such as Euclidean distance) is used.
## HNSW performance: 1536 dimensions
To understand the performance improvements that HNSW offers, we decided to expand upon our previous benchmarks and include results for the HNSW index in addition to IVF and compare the queries per second (QPS) for both.
### wikipedia-en-embeddings
In the first test, we used [224,482 vectors by OpenAI](https://huggingface.co/datasets/Supabase/wikipedia-en-embeddings) (1536 dimensions). You can find our previous benchmark with additional information on how vector dimensions may affect performance in [pgvector: Fewer dimensions are better](https://supabase.com/blog/pgvector-performance).
In this test, we used a Supabase project with a large compute add-on (2-core CPU and 8GB RAM) and built the HNSW index with the following parameters:
- `m`: 32
- `ef_construction`: 64
<div>
<img
alt="wikipedia embeddings comparing ivfflat and hnsw queries-per-second (light)"
className="dark:hidden"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/wikipedia-ivfflat-vs-hnsw--light.png"
/>
<img
alt="wikipedia embeddings comparing ivfflat and hnsw queries-per-second (dark)"
className="hidden dark:block"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/wikipedia-ivfflat-vs-hnsw--dark.png"
/>
</div>
HNSW has 3 times better performance than IVFFlat and with better accuracy.
### dbpedia-entities-openai-1M
Next, we took the setup from our benchmarks with [1,000,000 vectors by OpenAI](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) (1536 dimensions). If you want to find out more about pgvector 0.4.0 IVFFlat performance and our load testing methodology, check out [pgvector 0.4.0 performance](https://supabase.com/blog/pgvector-performance) blogpost.
Here we used the Supabase project with a 2XL compute add-on (8-core CPU and 32GB RAM) and built the HNSW index with the same parameters:
- `m`: 24
- `ef_construction`: 56
<div>
<img
alt="dbpedia embeddings comparing ivfflat and hnsw queries-per-second (light)"
className="dark:hidden"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-ivfflat-vs-hnsw--light.png"
/>
<img
alt="dbpedia embeddings comparing ivfflat and hnsw queries-per-second (dark)"
className="hidden dark:block"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-ivfflat-vs-hnsw--dark.png"
/>
</div>
When we maintain fixed HNSW build parameters, we can adjust the select query parameter `ef_search` to balance query speed and accuracy. To achieve accuracy@10 of 0.98, we increased it from the default 40 to 100. For accuracy@10 of 0.99, we further raised it to 250. Remarkably, HNSW demonstrated over six times better performance while maintaining the same level of accuracy. With higher accuracy@10 of 0.99 HNSW even outperforms [qdrant](https://nirantk.com/writing/pgvector-vs-qdrant/) on equivalent compute resources.
<div>
<img
alt="dbpedia embeddings comparing qdrant and hnsw queries-per-second (light)"
className="dark:hidden"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-qdrant-vs-hnsw--light.png"
/>
<img
alt="dbpedia embeddings comparing qdrant and hnsw queries-per-second (dark)"
className="hidden dark:block"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-qdrant-vs-hnsw--dark.png"
/>
</div>
### Scaling the database
Performance scales predictably with the size of the database for the HNSW index, just as [it does for the IVFFlat index](https://supabase.com/blog/pgvector-performance#scaling-the-database). The difference in performance between IVFFlat and HNSW remains consistent after a compute upgrade.
<div>
<img
alt="dbpedia embeddings comparing ivfflat and hnsw queries-per-second using the 4XL compute addon (light)"
className="dark:hidden"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-ivfflat-vs-hnsw-4xl--light.png"
/>
<img
alt="dbpedia embeddings comparing ivfflat and hnsw queries-per-second using the 4XL compute addon (dark)"
className="hidden dark:block"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-ivfflat-vs-hnsw-4xl--dark.png"
/>
</div>
Switching to a 4XL compute add-on (with a 16-core CPU and 64GB of RAM) resulted in a 69% increase in QPS for an accuracy@10 of 0.99 compared to the 2XL instance.
<div>
<img
alt="dbpedia embeddings comparing hnsw queries-per-second using the 2XL vs 4XL compute addon (light)"
className="dark:hidden"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-hnsw-2xl-vs-4xl--light.png"
/>
<img
alt="dbpedia embeddings comparing hnsw queries-per-second using the 2XL vs 4XL compute addon (dark)"
className="hidden dark:block"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-hnsw-2xl-vs-4xl--dark.png"
/>
</div>
Furthermore, having 64GB of RAM is better suited for this dataset because Postgres uses approximately 30-35GB of RAM to achieve optimal performance. With a 4XL setup, you not only have the capacity to store 1,000,000 vectors but also the flexibility to accommodate additional data or increase the number of vectors as needed.
### HNSW build parameters
We conducted a small experiment to assess the impact of HNSW index parameters on accuracy and QPS. In this experiment, we utilized the same 4XL instance as in the previous test but modified the building parameters:
- `m`: 32
- `ef_construction`: 80
This adjustment enabled us to achieve the same level of accuracy@10 of 0.99 with an `ef_search` = 100 instead of the previous 250, resulting in a 35% increase in QPS.
<div>
<img
alt="dbpedia embeddings comparing hnsw queries-per-second using different build parameters (light)"
className="dark:hidden"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-hnsw-build-parameters--light.png"
/>
<img
alt="dbpedia embeddings comparing hnsw queries-per-second using different build parameters (dark)"
className="hidden dark:block"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-hnsw-build-parameters--dark.png"
/>
</div>
## Other HNSW features
In addition to the above query performance improvements, HNSW offers another advantage: you don't need to fully fill your table before building the index.
With IVFFlat indexes, the clusters (lists) are constructed based on the distribution of existing data in the table. This means that IVF indexes built on an empty table would produce completely suboptimal centers. This is why pgvector recommends building IVF indexes only once sufficient data exists in the table and rebuilding them any time the distribution of data changes significantly.
HNSW indexes use graphs which inherently are not affected by this limitation, so you are safe to create your HNSW index immediately after the table is created. As new data is added to the table, the index will be filled automatically and the index structure will remain optimal.
## Improvements to IVF indexes
IVFFlat indexes also saw some improvements in v0.5.0. Index build times are now significantly faster for large datasets thanks to 2 new improvements:
- Parallelization during the assignment build step (assigning records to lists)
- Switching from [double to float](https://github.com/pgvector/pgvector/pull/180) accuracy for select distance calculations which unlocks the [fused multiply-add](https://en.wikipedia.org/wiki/Multiply%E2%80%93accumulate_operation#Fused_multiply%E2%80%93add) instruction on CPUs
Below we compare the index build times between v0.4.0 and v0.5.0 for the inner product distance measure over 1,000,000 vectors on a Supabase project with 4XL compute add-on (with a 16-core CPU and 64GB of RAM).
- `lists = 1000`
<div>
<img
alt="dbpedia embeddings comparing ivfflat index build time between v0.4.0 and v0.5.0 using 1000 lists (light)"
className="dark:hidden"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-ivfflat-0-4-0-vs-0-5-0-lists-1000--light.png"
/>
<img
alt="dbpedia embeddings comparing ivfflat index build time between v0.4.0 and v0.5.0 using 1000 lists (dark)"
className="hidden dark:block"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-ivfflat-0-4-0-vs-0-5-0-lists-1000--dark.png"
/>
</div>
- `lists = 5000`
<div>
<img
alt="dbpedia embeddings comparing ivfflat index build time between v0.4.0 and v0.5.0 using 5000 lists (light)"
className="dark:hidden"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-ivfflat-0-4-0-vs-0-5-0-lists-5000--light.png"
/>
<img
alt="dbpedia embeddings comparing ivfflat index build time between v0.4.0 and v0.5.0 using 5000 lists (dark)"
className="hidden dark:block"
src="/images/blog/2023-09-06-increase-performance-pgvector-hnsw/dbpedia-ivfflat-0-4-0-vs-0-5-0-lists-5000--dark.png"
/>
</div>
The index build time has decreased by over 50%, and this ratio remains consistent when you adjust the index build parameters ([specifically, the `lists` value](https://supabase.com/blog/pgvector-performance#other-performance-factors)).
## When should you use HNSW vs IVF?
In most cases today, HNSW offers a more performant and robust index over IVFFlat. It's worth noting though that HNSW indexes will almost always be slower to build and use more memory than IVFFlat, so if your system is memory-constrained and you don't foresee the need to rebuild your index often, you may find IVFFlat to be more suitable. It's also worth noting that product quantization (compressing index entries for vectors) is expected for IVF in the next versions of pgvector which should significantly improve performance and lower resource requirements. Regardless of the type of index you use, we still recommend [reducing the number of dimensions](https://supabase.com/blog/fewer-dimensions-are-better-pgvector) in your embeddings when possible.
## Start using HNSW
All [new Supabase databases](https://database.new/) will automatically ship with pgvector v0.5.0 which includes the new HNSW indexes. If you have an existing database, please reach out to [support](https://supabase.com/dashboard/support/new) and we'll be more than happy to assist you with an upgrade.
## More pgvector and AI resources
- [How to build ChatGPT Plugin from scratch with Supabase Edge Runtime](https://supabase.com/blog/building-chatgpt-plugins-template)
- [Docs pgvector: Embeddings and vector similarity](https://supabase.com/docs/guides/database/extensions/pgvector)
- [Choosing Compute Add-on for AI workloads](https://supabase.com/docs/guides/ai/choosing-compute-addon)
- [pgvector: Fewer dimensions are better](https://supabase.com/blog/fewer-dimensions-are-better-pgvector)
@@ -57,6 +57,7 @@ function GithubExamples() {
<div className={'lg:-mr-32 lg:-ml-32'}>
<Swiper
style={{ overflow: 'visible' }}
loop={true}
initialSlide={3}
spaceBetween={0}
slidesPerView={4}
+1 -1
View File
@@ -2291,6 +2291,6 @@ module.exports = [
{
permanent: true,
source: '/blog/pgvector-v0-5-0-hnsw',
destination: '/blog',
destination: '/blog/increase-performance-pgvector-hnsw',
},
]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+8 -1
View File
@@ -5,9 +5,16 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Greg Richardson at Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 02 Aug 2023 06:00:00 GMT</lastBuildDate>
<lastBuildDate>Tue, 05 Sep 2023 06:00:00 GMT</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-gregnr-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/increase-performance-pgvector-hnsw</guid>
<title>pgvector v0.5.0: Faster semantic search with HNSW indexes</title>
<link>https://supabase.com/blog/increase-performance-pgvector-hnsw</link>
<description>Increase performance in pgvector using HNSW indexes</description>
<pubDate>Tue, 05 Sep 2023 06:00:00 GMT</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/fewer-dimensions-are-better-pgvector</guid>
<title>pgvector: Fewer dimensions are better</title>
<link>https://supabase.com/blog/fewer-dimensions-are-better-pgvector</link>
+8 -1
View File
@@ -5,9 +5,16 @@
<link>https://supabase.com</link>
<description>Latest news from Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 30 Aug 2023 06:00:00 GMT</lastBuildDate>
<lastBuildDate>Tue, 05 Sep 2023 06:00:00 GMT</lastBuildDate>
<atom:link href="https://supabase.com/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/increase-performance-pgvector-hnsw</guid>
<title>pgvector v0.5.0: Faster semantic search with HNSW indexes</title>
<link>https://supabase.com/blog/increase-performance-pgvector-hnsw</link>
<description>Increase performance in pgvector using HNSW indexes</description>
<pubDate>Tue, 05 Sep 2023 06:00:00 GMT</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/organization-based-billing</guid>
<title>Organization-based Billing, Project Transfers, Team plan</title>
<link>https://supabase.com/blog/organization-based-billing</link>
+6 -6
View File
@@ -14210,6 +14210,11 @@
"websocket": "^1.0.34"
}
},
"node_modules/@supabase/shared-types": {
"version": "0.1.47",
"resolved": "https://registry.npmjs.org/@supabase/shared-types/-/shared-types-0.1.47.tgz",
"integrity": "sha512-OsXFgKd7yID/5mHuQ8k7I+dCT++ctEEj4YqV8soAik2EP0oiqqRzPwJ8kdmpwK+YyB0Z+AfOxex4dVa7J9J6eA=="
},
"node_modules/@supabase/storage-js": {
"version": "2.5.1",
"license": "MIT",
@@ -42491,7 +42496,7 @@
"@supabase/auth-helpers-nextjs": "^0.5.6",
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/react-data-grid": "^7.1.0-beta.7",
"@supabase/shared-types": "^0.1.46",
"@supabase/shared-types": "0.1.47",
"@supabase/supabase-js": "^2.23.0",
"@tanstack/react-query": "^4.22.0",
"@tanstack/react-query-devtools": "^4.22.0",
@@ -42812,11 +42817,6 @@
"url": "https://opencollective.com/storybook"
}
},
"studio/node_modules/@supabase/shared-types": {
"version": "0.1.46",
"resolved": "https://registry.npmjs.org/@supabase/shared-types/-/shared-types-0.1.46.tgz",
"integrity": "sha512-DV3VIQcyWdkVhQJNkH2IzHTiDS37AM5DnI/kQZc6YlzS93cFHERkwueKDFFIl85Q0UddG6ANZuoTne3w7BjWOQ=="
},
"studio/node_modules/@tanstack/match-sorter-utils": {
"version": "8.7.6",
"license": "MIT",
File renamed without changes.
+2 -1
View File
@@ -3,6 +3,7 @@ import logConstants from './logConstants'
import { plans, PricingInformation } from './plans'
import { pricing } from './pricing'
import { products } from './products'
import extensions from './extensions.json'
export { tweets, logConstants, plans, pricing, products }
export { tweets, logConstants, plans, pricing, products, extensions }
export type { PricingInformation }
+4 -1
View File
@@ -1,5 +1,8 @@
{
"extends": "tsconfig/react-library.json",
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
"exclude": ["dist", "build", "node_modules"],
"compilerOptions": {
"resolveJsonModule": true
}
}
+7
View File
@@ -296,6 +296,13 @@ functions:
```js
const { data, error } = await supabase.auth.verifyOtp({ email, token, type: 'signup'})
```
- id: verify-email-auth(tokenhash)
name: Verify Email Auth (Token Hash)
isSpotlight: false
code: |
```js
const { data, error } = await supabase.auth.verifyOtp({ token_hash: tokenHash, type: 'email'})
```
- id: auth.getSession()
title: 'getSession()'
$ref: '@supabase/gotrue-js.GoTrueClient.getSession'
+8
View File
@@ -415,6 +415,7 @@ functions:
notes: |
- The `verifyOtp` method takes in different verification types. If a phone number is used, the type can either be `sms` or `phone_change`. If an email address is used, the type can be one of the following: `email`, `recovery`, `invite` or `email_change` (`signup` and `magiclink` types are deprecated).
- The verification type used should be determined based on the corresponding auth method called before `verifyOtp` to sign up / sign-in a user.
- The `TokenHash` is contained in the [email templates](/docs/guides/auth/auth-email-templates) and can be used to sign in. You may wish to use the hash with Magic Links for the PKCE flow for Server Side Auth. See [this guide](/docs/guides/auth/server-side/email-based-auth-with-pkce-flow-for-ssr) for more details.
examples:
- id: verify-sms-one-time-password(otp)
name: Verify Sms One-Time Password (OTP)
@@ -430,6 +431,13 @@ functions:
```js
const { data, error } = await supabase.auth.verifyOtp({ email, token, type: 'email'})
```
- id: verify-email-auth(tokenhash)
name: Verify Email Auth (Token Hash)
isSpotlight: false
code: |
```js
const { data, error } = await supabase.auth.verifyOtp({ token_hash: tokenHash, type: 'email'})
```
- id: get-session
title: 'getSession()'
$ref: '@supabase/gotrue-js.GoTrueClient.getSession'
@@ -0,0 +1,226 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import { Input, Modal } from 'ui'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import InformationBox from 'components/ui/InformationBox'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useMfaChallengeAndVerifyMutation } from 'data/profile/mfa-challenge-and-verify-mutation'
import { useMfaEnrollMutation } from 'data/profile/mfa-enroll-mutation'
import { useMfaUnenrollMutation } from 'data/profile/mfa-unenroll-mutation'
import { useStore } from 'hooks'
interface AddNewFactorModalProps {
visible: boolean
onClose: () => void
}
const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps) => {
// Generate a name with a number between 0 and 1000
const [name, setName] = useState(`App ${Math.floor(Math.random() * 1000)}`)
const { data, mutate: enroll, isLoading: isEnrolling, reset } = useMfaEnrollMutation()
useEffect(() => {
// reset has to be called because the state is kept between if the process is canceled during
// the second step.
if (!visible) {
setName(`App ${Math.floor(Math.random() * 1000)}`)
reset()
}
}, [reset, visible])
return (
<>
<FirstStep
visible={visible && !Boolean(data)}
name={name}
setName={setName}
enroll={enroll}
isEnrolling={isEnrolling}
onClose={onClose}
/>
<SecondStep
visible={visible && Boolean(data)}
factorName={name}
factor={data!}
isLoading={isEnrolling}
onSuccess={() => onClose()}
onClose={onClose}
/>
</>
)
}
interface FirstStepProps {
visible: boolean
name: string
setName: Dispatch<SetStateAction<string>>
enroll: (params: { factorType: 'totp'; friendlyName?: string }) => void
isEnrolling: boolean
onClose: () => void
}
const FirstStep = ({ visible, name, enroll, setName, isEnrolling, onClose }: FirstStepProps) => {
return (
<ConfirmationModal
size="medium"
visible={visible}
header="Add a new authenticator app as a factor"
buttonLabel="Generate QR"
buttonLoadingLabel="Generating QR"
buttonDisabled={name.length === 0}
loading={isEnrolling}
onSelectCancel={onClose}
onSelectConfirm={() => {
enroll({
factorType: 'totp',
friendlyName: name,
})
}}
>
<Modal.Content>
<div className="pt-6 pb-5">
<Input
label="Provide a name to identify this app"
descriptionText="A string will be randomly generated if a name is not provided"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
</Modal.Content>
</ConfirmationModal>
)
}
interface SecondStepProps {
visible: boolean
factorName: string
factor?: {
id: string
type: 'totp'
totp: {
qr_code: string
secret: string
uri: string
}
}
isLoading: boolean
onSuccess: () => void
onClose: () => void
}
const SecondStep = ({
visible,
factorName,
factor: outerFactor,
isLoading,
onSuccess,
onClose,
}: SecondStepProps) => {
const { ui } = useStore()
const [code, setCode] = useState('')
const { mutate: unenroll } = useMfaUnenrollMutation({ onSuccess: () => onClose() })
const { mutate: challengeAndVerify, isLoading: isVerifying } = useMfaChallengeAndVerifyMutation({
onError: (error) => {
ui.setNotification({
category: 'error',
message: `Failed to add a second factor authentication: ${error?.message}`,
})
},
onSuccess: () => {
ui.setNotification({
category: 'success',
message: `Successfully added a second factor authentication`,
})
onSuccess()
},
})
const [factor, setFactor] = useState<{
id: string
type: 'totp'
totp: {
qr_code: string
secret: string
uri: string
}
} | null>(null)
// this useEffect is to keep the factor until a new one comes. This is a fix to an issue which
// happens when closing the modal, the outer factor is reset to null too soon and the modal
// removes a big div mid transition.
useEffect(() => {
if (outerFactor && factor?.id !== outerFactor.id) {
setFactor(outerFactor)
}
}, [outerFactor])
return (
<ConfirmationModal
size="medium"
visible={visible}
header={`Verify new factor ${factorName}`}
buttonLabel="Confirm"
buttonLoadingLabel="Confirming"
loading={isVerifying}
onSelectCancel={() => {
// If a factor has been created (but not verified), unenroll it. This will be run as a
// side effect so that it's not confusing to the user why the modal stays open while
// unenrolling.
if (factor) unenroll({ factorId: factor.id })
}}
onSelectConfirm={() => factor && challengeAndVerify({ factorId: factor.id, code })}
>
<Modal.Content>
<>
<div className="py-4 pb-0 text-sm">
<span>
Use an authenticator app to scan the following QR code, and provide the code from the
app to complete the enrolment.
</span>
</div>
{isLoading && (
<div className="pb-4 px-4">
<GenericSkeletonLoader />
</div>
)}
{factor && (
<>
<div className="flex justify-center py-6">
<div className="h-48 w-48 bg-white rounded">
<img width={190} height={190} src={factor.totp.qr_code} alt={factor.totp.uri} />
</div>
</div>
<div>
<InformationBox
title="Unable to scan?"
description={
<Input
copy
disabled
id="ref"
size="small"
label="You can also enter this secret key into your authenticator app"
value={factor.totp.secret}
/>
}
/>
</div>
<div className="pt-2 pb-4">
<Input
label="Authentication code"
value={code}
placeholder="XXXXXX"
onChange={(e) => setCode(e.target.value)}
/>
</div>
</>
)}
</>
</Modal.Content>
</ConfirmationModal>
)
}
export default AddNewFactorModal
@@ -0,0 +1,81 @@
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
IconAlertTriangle,
Modal,
} from 'ui'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { useMfaUnenrollMutation } from 'data/profile/mfa-unenroll-mutation'
import { useStore } from 'hooks'
interface DeleteFactorModalProps {
visible: boolean
factorId: string | null
lastFactorToBeDeleted: boolean
onClose: () => void
}
const DeleteFactorModal = ({
visible,
factorId,
lastFactorToBeDeleted,
onClose,
}: DeleteFactorModalProps) => {
const { ui } = useStore()
const { mutate: unenroll, isLoading } = useMfaUnenrollMutation({
onSuccess: () => {
ui.setNotification({ category: 'success', message: `Successfully deleted factor` })
onClose()
},
})
return (
<ConfirmationModal
size="medium"
visible={visible}
danger
header="Confirm to delete factor"
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
loading={isLoading}
onSelectCancel={onClose}
onSelectConfirm={() => factorId && unenroll({ factorId })}
>
<Modal.Content className="py-6">
<Alert_Shadcn_ variant="warning">
<IconAlertTriangle strokeWidth={2} />
<AlertTitle_Shadcn_>
{lastFactorToBeDeleted
? 'Multi-factor authentication will be disabled'
: 'This action cannot be undone'}
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
{lastFactorToBeDeleted
? 'There are no other factors that are set up once you delete this factor, as such your account will no longer be guarded by multi-factor authentication'
: 'You will no longer be able to use this authenticator app for multi-factor authentication when signing in to the dashboard'}
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
<div className="text-sm px-1 pt-4">
<p>Before deleting this factor, consider:</p>
<ul className="text-light py-1 list-disc mx-4 space-y-1">
{lastFactorToBeDeleted ? (
<>
<li>Adding another authenticator app as a factor prior to deleting</li>
<li>Ensure that your account does not need multi-factor authentication</li>
</>
) : (
<>
<li>Your backup authenticator app is still available to use</li>
<li>Adding another authenticator app thereafter as a backup</li>
</>
)}
</ul>
</div>
</Modal.Content>
</ConfirmationModal>
)
}
export default DeleteFactorModal
@@ -0,0 +1,99 @@
import dayjs from 'dayjs'
import { useState } from 'react'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
IconAlertCircle,
} from 'ui'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useMfaListFactorsQuery } from 'data/profile/mfa-list-factors-query'
import { DATETIME_FORMAT } from 'lib/constants'
import AddNewFactorModal from './AddNewFactorModal'
import DeleteFactorModal from './DeleteFactorModal'
const TOTPFactors = () => {
const [isAddNewFactorOpen, setIsAddNewFactorOpen] = useState(false)
const [factorToBeDeleted, setFactorToBeDeleted] = useState<string | null>(null)
const { data, isLoading, isError, isSuccess, error } = useMfaListFactorsQuery()
return (
<>
<section className="space-y-3">
<p className="text-sm text-light">
Generate one-time passwords via authenticator apps like 1Password, Authy, etc. as a second
factor to verify your identity during sign-in.
</p>
<div>
{isLoading && <GenericSkeletonLoader />}
{isError && (
<AlertError error={error} subject="Failed to retrieve account security information" />
)}
{isSuccess && (
<>
{data.totp.length === 1 && (
<Alert_Shadcn_ variant="default" className="mb-2">
<IconAlertCircle className="h-4 w-4" />
<AlertTitle_Shadcn_>
We recommend configuring two authenticator apps across different devices
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex flex-col gap-3">
The two authenticator apps will serve as a backup for each other.
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
<div>
{data.totp.map((factor) => {
return (
<div key={factor.id} className="flex flex-row justify-between py-2">
<p className="text-sm text flex items-center space-x-2">
<span className="text-light">Name:</span>{' '}
<span>{factor.friendly_name ?? 'No name provided'}</span>
</p>
<div className="flex items-center gap-4">
<p className="text-sm text-light">
Added on {dayjs(factor.updated_at).format(DATETIME_FORMAT)}
</p>
<Button
size="tiny"
type="default"
// className="px-1"
// icon={<IconTrash />}
onClick={() => setFactorToBeDeleted(factor.id)}
>
Remove
</Button>
</div>
</div>
)
})}
</div>
{data.totp.length < 2 ? (
<>
<div className="pt-2">
<Button onClick={() => setIsAddNewFactorOpen(true)}>Add new app</Button>
</div>
</>
) : null}
</>
)}
</div>
</section>
<AddNewFactorModal
visible={isAddNewFactorOpen}
onClose={() => setIsAddNewFactorOpen(false)}
/>
<DeleteFactorModal
visible={factorToBeDeleted !== null}
factorId={factorToBeDeleted}
lastFactorToBeDeleted={data?.totp.length === 1}
onClose={() => setFactorToBeDeleted(null)}
/>
</>
)
}
export default TOTPFactors
@@ -1,5 +1,6 @@
import AccessTokenList from './AccessTokenList'
import NewTokenBanner from './NewTokenBanner'
import NewAccessTokenButton from './NewAccessTokenButton'
import NewTokenBanner from './NewTokenBanner'
import TOTPFactors from './TOTPFactors'
export { AccessTokenList, NewTokenBanner, NewAccessTokenButton }
export { AccessTokenList, NewTokenBanner, NewAccessTokenButton, TOTPFactors }
@@ -1,12 +1,14 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import { Badge, IconLoader, Toggle } from 'ui'
import { Badge, IconExternalLink, IconLoader, Toggle } from 'ui'
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
import { useCheckPermissions, useStore } from 'hooks'
import { isResponseOk } from 'lib/common/fetch'
import EnableExtensionModal from './EnableExtensionModal'
import Link from 'next/link'
import { extensions } from 'shared-data'
interface ExtensionCardProps {
extension: any
@@ -18,6 +20,7 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
const isOn = extension.installed_version !== null
const [loading, setLoading] = useState(false)
const [showConfirmEnableModal, setShowConfirmEnableModal] = useState(false)
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || ''
const canUpdateExtensions = useCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
@@ -70,15 +73,38 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
<div
className={[
'border-panel-border-light bg-panel-header-light dark:bg-panel-header-dark',
'flex border-b p-4 px-6 dark:border-panel-border-dark',
'flex justify-between w-full border-b p-4 px-6 dark:border-panel-border-dark',
].join(' ')}
>
<h3
title={extension.name}
className="m-0 h-5 flex-1 truncate text-base uppercase text-scale-1200"
>
{extension.name}
</h3>
<div className="flex items-center gap-1">
<h3
title={extension.name}
className="h-5 m-0 text-base uppercase truncate cursor-pointer text-scale-1200"
>
{extension.name}
</h3>
{extensions.find((item: any) => item.name === extension.name) ? (
<Link
href={
extensions
.find((item: any) => item.name === extension.name)
?.link.startsWith('/guides')
? siteUrl === 'http://localhost:8082'
? `http://localhost:3001/docs${
extensions.find((item: any) => item.name === extension.name)?.link
}`
: `https://supabase.com/docs${
extensions.find((item: any) => item.name === extension.name)?.link
}`
: extensions.find((item: any) => item.name === extension.name)?.link ?? ''
}
>
<a className="max-w-[85%] cursor-default zans" target="_blank" rel="noreferrer">
<IconExternalLink className="ml-2.5 cursor-pointer" size={14} />
</a>
</Link>
) : null}
</div>
{loading ? (
<IconLoader className="animate-spin" size={16} />
) : (
@@ -101,7 +127,7 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
</div>
{isOn && extension.schema && (
<div className="p-4 px-6">
<div className="flex flex-grow items-center space-x-2 text-sm text-scale-1100">
<div className="flex items-center flex-grow space-x-2 text-sm text-scale-1100">
<span>Schema:</span>
<Badge>{`${extension.schema}`}</Badge>
</div>
@@ -1,14 +1,17 @@
import HCaptcha from '@hcaptcha/react-hcaptcha'
import * as Sentry from '@sentry/nextjs'
import { AuthError } from '@supabase/supabase-js'
import { useQueryClient } from '@tanstack/react-query'
import { getMfaAuthenticatorAssuranceLevel } from 'data/profile/mfa-authenticator-assurance-level-query'
import { useStore } from 'hooks'
import { usePushNext } from 'hooks/misc/useAutoAuthRedirect'
import { auth } from 'lib/gotrue'
import { auth, buildPathWithParams } from 'lib/gotrue'
import { incrementSignInClicks } from 'lib/local-storage'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useRef, useState } from 'react'
import { Button, Form, Input } from 'ui'
import { object, string } from 'yup'
import * as Sentry from '@sentry/nextjs'
const signInSchema = object({
email: string().email('Must be a valid email').required('Email is required'),
@@ -17,6 +20,7 @@ const signInSchema = object({
const SignInForm = () => {
const { ui } = useStore()
const router = useRouter()
const pushNext = usePushNext()
const queryClient = useQueryClient()
@@ -47,15 +51,37 @@ const SignInForm = () => {
Sentry.captureMessage('Sign in without previous sign out detected')
}
ui.setNotification({
id: toastId,
category: 'success',
message: `Signed in successfully!`,
})
try {
const data = await getMfaAuthenticatorAssuranceLevel()
if (data) {
if (data.currentLevel !== data.nextLevel) {
ui.setNotification({
id: toastId,
category: 'success',
message: `You need to provide your second factor authentication.`,
})
const url = buildPathWithParams('/sign-in-mfa')
router.replace(url)
return
}
}
await queryClient.resetQueries()
ui.setNotification({
id: toastId,
category: 'success',
message: `Signed in successfully!`,
})
await pushNext()
await queryClient.resetQueries()
await pushNext()
} catch (error) {
ui.setNotification({
id: toastId,
category: 'error',
message: (error as AuthError).message,
})
}
} else {
setCaptchaToken(null)
captchaRef.current?.resetCaptcha()
@@ -0,0 +1,193 @@
import { AuthError } from '@supabase/gotrue-js'
import { Factor } from '@supabase/supabase-js'
import { useQueryClient } from '@tanstack/react-query'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { Button, Form, IconLock, Input } from 'ui'
import { object, string } from 'yup'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useMfaChallengeAndVerifyMutation } from 'data/profile/mfa-challenge-and-verify-mutation'
import { useMfaListFactorsQuery } from 'data/profile/mfa-list-factors-query'
import { useStore } from 'hooks'
import { usePushNext } from 'hooks/misc/useAutoAuthRedirect'
import { useSignOut } from 'lib/auth'
const signInSchema = object({
code: string().required('MFA Code is required'),
})
const SignInMfaForm = () => {
const { ui } = useStore()
const pushNext = usePushNext()
const queryClient = useQueryClient()
const router = useRouter()
const {
data: factors,
error: factorsError,
isError: isErrorFactors,
isSuccess: isSuccessFactors,
isInitialLoading: isInitialLoadingFactors,
isLoading: isLoadingFactors,
} = useMfaListFactorsQuery()
const {
mutateAsync: mfaChallengeAndVerify,
isLoading,
isSuccess,
} = useMfaChallengeAndVerifyMutation()
const [selectedFactor, setSelectedFactor] = useState<Factor | null>(null)
const signOut = useSignOut()
const onClickLogout = async () => {
await signOut()
await router.replace('/sign-in')
}
useEffect(() => {
if (isSuccessFactors) {
// if the user wanders into this page and he has no MFA setup, send the user to the next screen
if (factors.totp.length === 0) {
queryClient.resetQueries().then(() => pushNext())
}
if (factors.totp.length > 0) {
setSelectedFactor(factors.totp[0])
}
}
}, [factors?.totp, isSuccessFactors, pushNext, queryClient])
const onSignIn = async ({ code }: { code: string }) => {
const toastId = ui.setNotification({
category: 'loading',
message: `Signing in...`,
})
if (selectedFactor) {
await mfaChallengeAndVerify(
{ factorId: selectedFactor.id, code, refreshFactors: false },
{
onSuccess: async () => {
ui.setNotification({
id: toastId,
category: 'success',
message: `Signed in successfully!`,
})
await queryClient.resetQueries()
await pushNext()
},
onError: (error) => {
ui.setNotification({
id: toastId,
category: 'error',
message: (error as AuthError).message,
})
},
}
)
}
}
return (
<>
{isLoadingFactors && <GenericSkeletonLoader />}
{isErrorFactors && <AlertError error={factorsError} subject="Failed to retrieve factors" />}
{isSuccessFactors && (
<Form
validateOnBlur
id="sign-in-mfa-form"
initialValues={{ code: '' }}
validationSchema={signInSchema}
onSubmit={onSignIn}
>
{() => (
<>
<div className="flex flex-col gap-4">
<Input
id="code"
name="code"
type="text"
autoFocus
icon={<IconLock />}
placeholder="XXXXXX"
disabled={isLoading}
autoComplete="off"
spellCheck="false"
autoCapitalize="none"
autoCorrect="off"
label={
selectedFactor && factors?.totp.length === 2
? `Code generated by ${selectedFactor.friendly_name}`
: null
}
/>
<div className="flex items-center justify-between space-x-2">
<Button
block
type="outline"
size="large"
disabled={isLoading || isSuccess}
onClick={onClickLogout}
className="opacity-80 hover:opacity-100 transition"
>
Cancel
</Button>
<Button
block
form="sign-in-mfa-form"
htmlType="submit"
size="large"
disabled={isLoading || isSuccess}
loading={isLoading || isSuccess}
>
{isLoading ? 'Verifying' : isSuccess ? 'Signing in' : 'Verify'}
</Button>
</div>
</div>
</>
)}
</Form>
)}
<div className="my-8">
<div className="text-sm">
<span className="text-scale-1000">Unable to sign in?</span>{' '}
</div>
<ul className="list-disc pl-6">
{factors?.totp.length === 2 && (
<li>
<a
className="text-sm text-scale-1100 hover:text-scale-1200 cursor-pointer"
onClick={() =>
setSelectedFactor(factors.totp.find((f) => f.id !== selectedFactor?.id)!)
}
>{`Authenticate using ${
factors.totp.find((f) => f.id !== selectedFactor?.id)?.friendly_name
}?`}</a>
</li>
)}
<li>
<Link
passHref
href="/support/new?subject=Unable+to+sign+in+via+MFA&category=Login_issues"
>
<a
target="_blank"
rel="noreferrer"
className="text-sm transition text-scale-1100 hover:text-scale-1200"
>
Reach out to us via support
</a>
</Link>
</li>
</ul>
</div>
</>
)
}
export default SignInMfaForm
@@ -2,7 +2,7 @@ import HCaptcha from '@hcaptcha/react-hcaptcha'
import { useQueryClient } from '@tanstack/react-query'
import { useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
import { auth, getReturnToPath } from 'lib/gotrue'
import { auth, buildPathWithParams } from 'lib/gotrue'
import { useRef, useState } from 'react'
import { Button, Form, Input } from 'ui'
import { object, string } from 'yup'
@@ -30,15 +30,20 @@ const SignInSSOForm = () => {
token = captchaResponse?.response ?? null
}
// redirects to /sign-in to check if the user has MFA setup (handled in SignInLayout.tsx)
const redirectTo = buildPathWithParams(
`${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? location.origin
: process.env.NEXT_PUBLIC_SITE_URL
}${BASE_PATH}/sign-in-mfa`
)
const { data, error } = await auth.signInWithSSO({
domain: email.split('@')[1],
options: {
captchaToken: token ?? undefined,
redirectTo: `${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? location.origin
: process.env.NEXT_PUBLIC_SITE_URL
}${BASE_PATH}${getReturnToPath()}`,
redirectTo,
},
})
@@ -1,9 +1,9 @@
import { useState } from 'react'
import { BASE_PATH } from 'lib/constants'
import { auth, getReturnToPath } from 'lib/gotrue'
import { incrementSignInClicks } from 'lib/local-storage'
import { Button, IconGitHub } from 'ui'
import * as Sentry from '@sentry/nextjs'
import { BASE_PATH } from 'lib/constants'
import { auth, buildPathWithParams } from 'lib/gotrue'
import { incrementSignInClicks } from 'lib/local-storage'
import { useState } from 'react'
import { Button, IconGitHub } from 'ui'
const SignInWithGitHub = () => {
const [loading, setLoading] = useState(false)
@@ -17,14 +17,19 @@ const SignInWithGitHub = () => {
Sentry.captureMessage('Sign in without previous sign out detected')
}
// redirects to /sign-in to check if the user has MFA setup (handled in SignInLayout.tsx)
const redirectTo = buildPathWithParams(
`${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? location.origin
: process.env.NEXT_PUBLIC_SITE_URL
}${BASE_PATH}/sign-in-mfa`
)
const { error } = await auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? location.origin
: process.env.NEXT_PUBLIC_SITE_URL
}${BASE_PATH}${getReturnToPath()}`,
redirectTo,
},
})
if (error) throw error
@@ -70,9 +70,9 @@ const Success = ({ sentCategory = '' }: SuccessProps) => {
<Divider light />
</div>
<div className="w-full pb-4 px-4 flex items-center justify-end">
<Link href="/projects">
<Link href="/">
<a>
<Button>Go back to dashboard</Button>
<Button>Go back</Button>
</a>
</Link>
</div>
@@ -49,6 +49,12 @@ export const CATEGORY_OPTIONS = [
description: 'Formal enquiry form for requesting refunds',
query: undefined,
},
{
value: SupportCategories.LOGIN_ISSUES,
label: 'Issues with logging in',
description: 'Issues with logging in and MFA',
query: undefined,
},
]
export const SEVERITY_OPTIONS = [
@@ -2,6 +2,7 @@ import { useParams } from 'common'
import { CLIENT_LIBRARIES } from 'common/constants'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ChangeEvent, useEffect, useRef, useState } from 'react'
import {
Button,
@@ -21,22 +22,19 @@ import Divider from 'components/ui/Divider'
import InformationBox from 'components/ui/InformationBox'
import MultiSelect from 'components/ui/MultiSelect'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { getProjectAuthConfig } from 'data/auth/auth-config-query'
import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { useFlag, useStore } from 'hooks'
import useLatest from 'hooks/misc/useLatest'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { detectBrowser } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { Project } from 'types'
import DisabledStateForFreeTier from './DisabledStateForFreeTier'
import { CATEGORY_OPTIONS, SERVICE_OPTIONS, SEVERITY_OPTIONS } from './Support.constants'
import { formatMessage, uploadAttachments } from './SupportForm.utils'
import { useRouter } from 'next/router'
import { getProjectAuthConfig } from 'data/auth/auth-config-query'
const MAX_ATTACHMENTS = 5
const INCLUDE_DISCUSSIONS = ['Problem', 'Database_unresponsive']
@@ -286,6 +284,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
resetForm({ values: updatedValues, initialValues: updatedValues })
}
}, [isReady])
console.log(values)
return (
<div className="space-y-8 w-[620px]">
@@ -314,121 +313,125 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
</Listbox>
</div>
<div className="px-6">
<div className="grid grid-cols-2 gap-4">
{isLoadingProjects && (
<div className="space-y-2">
<p className="text-sm prose">Which project is affected?</p>
<ShimmeringLoader className="!py-[19px]" />
</div>
)}
{isErrorProjects && (
<div className="space-y-2">
<p className="text-sm prose">Which project is affected?</p>
<div className="border rounded-md px-4 py-2 flex items-center space-x-2">
<IconAlertCircle strokeWidth={2} className="text-scale-1000" />
<p className="text-sm prose">Failed to retrieve projects</p>
</div>
</div>
)}
{isSuccessProjects && (
<Listbox
id="projectRef"
layout="vertical"
label="Which project is affected?"
onChange={(val) => {
setSelectedProjectRef(val)
}}
>
{projects.map((option) => {
const organization = organizations?.find(
(x) => x.id === option.organization_id
)
return (
<Listbox.Option
key={`option-${option.ref}`}
label={option.name || ''}
value={option.ref}
>
<span>{option.name}</span>
<span className="block text-xs opacity-50">{organization?.name}</span>
</Listbox.Option>
)
})}
</Listbox>
)}
<Listbox id="severity" layout="vertical" label="Severity">
{SEVERITY_OPTIONS.map((option: any) => {
return (
<Listbox.Option
key={`option-${option.value}`}
label={option.label}
value={option.value}
disabled={option.value === 'Critical' && isFreeProject}
>
<span>{option.label}</span>
<span className="block text-xs opacity-50">{option.description}</span>
</Listbox.Option>
)
})}
</Listbox>
</div>
{values.projectRef !== 'no-project' && subscription && isSuccessProjects ? (
<p className="text-sm text-scale-1000 mt-2">
This project is on the{' '}
<span className="text-scale-1100">{subscription.plan.name} plan</span>
</p>
) : isLoadingSubscription && selectedProjectRef !== 'no-project' ? (
<div className="flex items-center space-x-2 mt-2">
<IconLoader size={14} className="animate-spin" />
<p className="text-sm text-scale-1000">Checking project's plan</p>
</div>
) : (
<></>
)}
</div>
{isSuccessProjects && values.projectRef === 'no-project' && (
{values.category !== 'Login_issues' && (
<div className="px-6">
{isLoadingOrganizations && (
<div className="space-y-2">
<p className="text-sm prose">Which organization is affected?</p>
<ShimmeringLoader className="!py-[19px]" />
</div>
)}
{isErrorOrganizations && (
<div className="space-y-2">
<p className="text-sm prose">Which organization is affected?</p>
<div className="border rounded-md px-4 py-2 flex items-center space-x-2">
<IconAlertCircle strokeWidth={2} className="text-scale-1000" />
<p className="text-sm prose">Failed to retrieve organizations</p>
<div className="grid grid-cols-2 gap-4">
{isLoadingProjects && (
<div className="space-y-2">
<p className="text-sm prose">Which project is affected?</p>
<ShimmeringLoader className="!py-[19px]" />
</div>
</div>
)}
{isSuccessOrganizations && (
<Listbox
id="organizationSlug"
layout="vertical"
label="Which organization is affected?"
>
{organizations?.map((option) => {
)}
{isErrorProjects && (
<div className="space-y-2">
<p className="text-sm prose">Which project is affected?</p>
<div className="border rounded-md px-4 py-2 flex items-center space-x-2">
<IconAlertCircle strokeWidth={2} className="text-scale-1000" />
<p className="text-sm prose">Failed to retrieve projects</p>
</div>
</div>
)}
{isSuccessProjects && (
<Listbox
id="projectRef"
layout="vertical"
label="Which project is affected?"
onChange={(val) => {
setSelectedProjectRef(val)
}}
>
{projects.map((option) => {
const organization = organizations?.find(
(x) => x.id === option.organization_id
)
return (
<Listbox.Option
key={`option-${option.ref}`}
label={option.name || ''}
value={option.ref}
>
<span>{option.name}</span>
<span className="block text-xs opacity-50">{organization?.name}</span>
</Listbox.Option>
)
})}
</Listbox>
)}
<Listbox id="severity" layout="vertical" label="Severity">
{SEVERITY_OPTIONS.map((option: any) => {
return (
<Listbox.Option
key={`option-${option.slug}`}
label={option.name || ''}
value={option.slug}
key={`option-${option.value}`}
label={option.label}
value={option.value}
disabled={option.value === 'Critical' && isFreeProject}
>
<span>{option.name}</span>
<span>{option.label}</span>
<span className="block text-xs opacity-50">{option.description}</span>
</Listbox.Option>
)
})}
</Listbox>
</div>
{values.projectRef !== 'no-project' && subscription && isSuccessProjects ? (
<p className="text-sm text-scale-1000 mt-2">
This project is on the{' '}
<span className="text-scale-1100">{subscription.plan.name} plan</span>
</p>
) : isLoadingSubscription && selectedProjectRef !== 'no-project' ? (
<div className="flex items-center space-x-2 mt-2">
<IconLoader size={14} className="animate-spin" />
<p className="text-sm text-scale-1000">Checking project's plan</p>
</div>
) : (
<></>
)}
</div>
)}
{subscription?.plan.id === 'free' && (
{isSuccessProjects &&
values.projectRef === 'no-project' &&
values.category !== 'Login_issues' && (
<div className="px-6">
{isLoadingOrganizations && (
<div className="space-y-2">
<p className="text-sm prose">Which organization is affected?</p>
<ShimmeringLoader className="!py-[19px]" />
</div>
)}
{isErrorOrganizations && (
<div className="space-y-2">
<p className="text-sm prose">Which organization is affected?</p>
<div className="border rounded-md px-4 py-2 flex items-center space-x-2">
<IconAlertCircle strokeWidth={2} className="text-scale-1000" />
<p className="text-sm prose">Failed to retrieve organizations</p>
</div>
</div>
)}
{isSuccessOrganizations && (
<Listbox
id="organizationSlug"
layout="vertical"
label="Which organization is affected?"
>
{organizations?.map((option) => {
return (
<Listbox.Option
key={`option-${option.slug}`}
label={option.name || ''}
value={option.slug}
>
<span>{option.name}</span>
</Listbox.Option>
)
})}
</Listbox>
)}
</div>
)}
{subscription?.plan.id === 'free' && values.category !== 'Login_issues' && (
<div className="px-6">
<InformationBox
icon={<IconAlertCircle strokeWidth={2} />}
@@ -603,16 +606,18 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
</div>
)}
<div className="px-6 space-y-2">
<p className="text-sm text-scale-1100">Which services are affected?</p>
<MultiSelect
options={SERVICE_OPTIONS}
value={selectedServices}
placeholder="No particular service"
searchPlaceholder="Search for a service"
onChange={setSelectedServices}
/>
</div>
{values.category !== 'Login_issues' && (
<div className="px-6 space-y-2">
<p className="text-sm text-scale-1100">Which services are affected?</p>
<MultiSelect
options={SERVICE_OPTIONS}
value={selectedServices}
placeholder="No particular service"
searchPlaceholder="Search for a service"
onChange={setSelectedServices}
/>
</div>
)}
<div className="text-area-text-sm px-6">
<Input.TextArea
id="message"
@@ -3,12 +3,13 @@ import { useRouter } from 'next/router'
import { PropsWithChildren } from 'react'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useMfaListFactorsQuery } from 'data/profile/mfa-list-factors-query'
import { useFlag, useSelectedOrganization, withAuth } from 'hooks'
import { useSignOut } from 'lib/auth'
import { IS_PLATFORM } from 'lib/constants'
import SettingsLayout from '../SettingsLayout/SettingsLayout'
import { SidebarSection } from './AccountLayout.types'
import WithSidebar from './WithSidebar'
import SettingsLayout from '../SettingsLayout/SettingsLayout'
export interface AccountLayoutProps {
title: string
@@ -22,9 +23,11 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
const router = useRouter()
const { data: organizations } = useOrganizationsQuery()
const selectedOrganization = useSelectedOrganization()
const { data: factors } = useMfaListFactorsQuery()
const ongoingIncident = useFlag('ongoingIncident')
const navLayoutV2 = useFlag('navigationLayoutV2')
const mfaSetup = useFlag('mfaSetup')
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
const signOut = useSignOut()
@@ -85,6 +88,19 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
href: `/account/tokens`,
key: `/account/tokens`,
},
// show the MFA page only if the feature flag is set or the user has already MFA setup.
// He should be able to edit/revoke his MFA even if MFA feature flag is disabled.
...(mfaSetup || (factors?.all || []).length > 0
? [
{
isActive: router.pathname === `/account/security`,
icon: `${router.basePath}/img/user.svg`,
label: 'Security',
href: `/account/security`,
key: `/account/security`,
},
]
: []),
],
},
]
@@ -6,6 +6,7 @@ const AccountSettingsMenu = () => {
const accountSettings = [
{ label: 'Preferences', pathname: `/account/me` },
{ label: 'Access Tokens', pathname: `/account/tokens` },
{ label: 'Security', pathname: `/account/security` },
]
return (
@@ -1,13 +1,13 @@
import { useQueryClient } from '@tanstack/react-query'
import { useTheme } from 'common'
import { useFlag, useStore } from 'hooks'
import { useFlag } from 'hooks'
import { usePushNext } from 'hooks/misc/useAutoAuthRedirect'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { auth, getReturnToPath, STORAGE_KEY } from 'lib/gotrue'
import { BASE_PATH } from 'lib/constants'
import { auth, buildPathWithParams, getAccessToken } from 'lib/gotrue'
import { observer } from 'mobx-react-lite'
import Head from 'next/head'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect, useState } from 'react'
import { tweets } from 'shared-data'
import { Button, IconFileText } from 'ui'
@@ -26,40 +26,53 @@ const SignInLayout = ({
logoLinkToMarketingSite = false,
children,
}: PropsWithChildren<SignInLayoutProps>) => {
const router = useRouter()
const pushNext = usePushNext()
const queryClient = useQueryClient()
const { isDarkMode } = useTheme()
const ongoingIncident = useFlag('ongoingIncident')
// This useEffect redirects the user to MFA if they're already halfway signed in
useEffect(() => {
const searchParams = new URLSearchParams(location.search)
const hasReturnTo = searchParams.has('returnTo')
const hasNext = searchParams.has('next')
const shouldRedirect = !hasReturnTo && !hasNext
auth
.initialize()
.then(async ({ error }) => {
if (error) {
// if there was a problem signing in via the url, don't redirect
return
}
if (!shouldRedirect) {
// If there's a returnTo or next, then this redirect will be handled by useAutoAuthRedirect() in _app.tsx
return
}
const token = await getAccessToken()
;(async () => {
const { error } = await auth.initialize()
if (token) {
const { data, error } = await auth.mfa.getAuthenticatorAssuranceLevel()
if (error) {
// if there was a problem signing in via the url, don't redirect
return
}
if (error) {
// if there was a problem signing in via the url, don't redirect
return
}
if (data) {
// we're already where we need to be
if (router.pathname === '/sign-in-mfa') {
return
}
if (data.currentLevel !== data.nextLevel) {
const redirectTo = buildPathWithParams('/sign-in-mfa')
router.replace(redirectTo)
return
}
}
const {
data: { session },
} = await auth.getSession()
if (session) {
await queryClient.resetQueries()
await pushNext()
}
})()
await queryClient.resetQueries()
await pushNext()
} else {
// if the user doesn't have a token, he needs to go back to the sign-in page
const redirectTo = buildPathWithParams('/sign-in')
router.replace(redirectTo)
return
}
})
.catch(() => {}) // catch all errors thrown by auth methods
}, [])
const [quote, setQuote] = useState<{
@@ -77,20 +90,12 @@ const SignInLayout = ({
return (
<>
{IS_PLATFORM && (
<Head>
<script
dangerouslySetInnerHTML={{
__html: `window._getReturnToPath = ${getReturnToPath.toString()};if (localStorage.getItem('${STORAGE_KEY}') && !(new URLSearchParams(location.search).has('next'))) {location.replace('${
BASE_PATH ?? ''
}' + window._getReturnToPath())}`,
}}
/>
</Head>
)}
<div className="flex flex-col flex-1 bg-scale-100">
<div className={`absolute top-0 w-full px-8 mx-auto sm:px-6 lg:px-8 ${ongoingIncident ? 'pt-16' : 'pt-6'}`}>
<div
className={`absolute top-0 w-full px-8 mx-auto sm:px-6 lg:px-8 ${
ongoingIncident ? 'pt-16' : 'pt-6'
}`}
>
<nav className="relative flex items-center justify-between sm:h-10">
<div className="flex items-center flex-grow flex-shrink-0 lg:flex-grow-0">
<div className="flex items-center justify-between w-full md:w-auto">
+3 -1
View File
@@ -10,6 +10,7 @@ export interface ConfirmationModalProps {
size?: 'small' | 'tiny' | 'medium' | 'large'
buttonLabel: string
buttonLoadingLabel?: string
buttonDisabled?: boolean
onSelectCancel: () => void
onSelectConfirm: () => void
}
@@ -23,6 +24,7 @@ const ConfirmationModal = ({
size = 'small',
buttonLabel = '',
buttonLoadingLabel = '',
buttonDisabled = false,
onSelectCancel = () => {},
onSelectConfirm = () => {},
children,
@@ -62,7 +64,7 @@ const ConfirmationModal = ({
<Button
type={danger ? 'danger' : 'primary'}
loading={loading}
disabled={loading}
disabled={loading || buttonDisabled}
onClick={onConfirm}
>
{loading ? buttonLoadingLabel : buttonLabel}
+2
View File
@@ -1,3 +1,5 @@
export const profileKeys = {
profile: () => ['profile'] as const,
mfaFactors: () => ['mfa', 'factors'] as const,
aaLevel: () => ['mfa', 'aaLevel'] as const,
}
@@ -0,0 +1,41 @@
import { AuthMFAGetAuthenticatorAssuranceLevelResponse } from '@supabase/supabase-js'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { auth } from 'lib/gotrue'
import { profileKeys } from './keys'
import { Profile } from './types'
export type ProfileResponse = Profile
export async function getMfaAuthenticatorAssuranceLevel() {
const { error, data } = await auth.mfa.getAuthenticatorAssuranceLevel()
if (error) throw error
return data
}
type CustomAuthMFAGetAuthenticatorAssuranceLevelData = NonNullable<
AuthMFAGetAuthenticatorAssuranceLevelResponse['data']
>
type CustomAuthMFAGetAuthenticatorAssuranceLevelError = NonNullable<
AuthMFAGetAuthenticatorAssuranceLevelResponse['error']
>
export const useAuthenticatorAssuranceLevelQuery = <
TData = CustomAuthMFAGetAuthenticatorAssuranceLevelData
>({
enabled = true,
...options
}: UseQueryOptions<
CustomAuthMFAGetAuthenticatorAssuranceLevelData,
CustomAuthMFAGetAuthenticatorAssuranceLevelError,
TData
> = {}) => {
return useQuery<
CustomAuthMFAGetAuthenticatorAssuranceLevelData,
CustomAuthMFAGetAuthenticatorAssuranceLevelError,
TData
>(profileKeys.aaLevel(), () => getMfaAuthenticatorAssuranceLevel(), {
staleTime: 1000 * 60 * 30, // default good for 30 mins
...options,
})
}
@@ -0,0 +1,48 @@
import { AuthMFAVerifyResponse, MFAChallengeAndVerifyParams } from '@supabase/supabase-js'
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { auth } from 'lib/gotrue'
import { profileKeys } from './keys'
interface MFAChallengeAndVerifyVariables extends MFAChallengeAndVerifyParams {
refreshFactors?: boolean
}
export const mfaChallengeAndVerify = async (params: MFAChallengeAndVerifyParams) => {
const { error, data } = await auth.mfa.challengeAndVerify(params)
if (error) throw error
return data
}
type CustomMFAVerifyResponse = NonNullable<AuthMFAVerifyResponse['data']>
type CustomMFAVerifyError = NonNullable<AuthMFAVerifyResponse['error']>
export const useMfaChallengeAndVerifyMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<CustomMFAVerifyResponse, CustomMFAVerifyError, MFAChallengeAndVerifyVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation(
(vars) => {
const { refreshFactors, ...params } = vars
return mfaChallengeAndVerify(params)
},
{
async onSuccess(data, variables, context) {
// when a MFA is added, the aaLevel is bumped up
const refreshFactors = variables.refreshFactors ?? true
await Promise.all([
...(refreshFactors ? [queryClient.invalidateQueries(profileKeys.mfaFactors())] : []),
queryClient.invalidateQueries(profileKeys.aaLevel()),
])
await onSuccess?.(data, variables, context)
},
...options,
}
)
}
@@ -0,0 +1,37 @@
import { AuthMFAEnrollResponse, MFAEnrollParams } from '@supabase/supabase-js'
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { auth } from 'lib/gotrue'
import { toast } from 'react-hot-toast'
const mfaEnroll = async (params: MFAEnrollParams) => {
const { error, data } = await auth.mfa.enroll(params)
if (error) throw error
return data
}
type CustomMFAEnrollResponse = NonNullable<AuthMFAEnrollResponse['data']>
type CustomMFAEnrollError = NonNullable<AuthMFAEnrollResponse['error']>
export const useMfaEnrollMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<CustomMFAEnrollResponse, CustomMFAEnrollError, MFAEnrollParams>,
'mutationFn'
> = {}) => {
return useMutation((vars) => mfaEnroll(vars), {
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to enroll factor: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}
@@ -0,0 +1,29 @@
import { AuthMFAListFactorsResponse, Factor } from '@supabase/supabase-js'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { auth } from 'lib/gotrue'
import { profileKeys } from './keys'
export async function getMfaListFactors() {
const { error, data } = await auth.mfa.listFactors()
if (error) throw error
return data
}
export type MFAFactor = Factor
type CustomMFAListFactorsData = NonNullable<AuthMFAListFactorsResponse['data']>
type CustomMFAListFactorsError = NonNullable<AuthMFAListFactorsResponse['error']>
export const useMfaListFactorsQuery = <TData = CustomMFAListFactorsData>({
enabled = true,
...options
}: UseQueryOptions<CustomMFAListFactorsData, CustomMFAListFactorsError, TData> = {}) => {
return useQuery<CustomMFAListFactorsData, CustomMFAListFactorsError, TData>(
profileKeys.mfaFactors(),
() => getMfaListFactors(),
{
staleTime: 1000 * 60 * 30, // default good for 30 mins
...options,
}
)
}
@@ -0,0 +1,46 @@
import { AuthMFAUnenrollResponse, MFAUnenrollParams } from '@supabase/supabase-js'
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { auth } from 'lib/gotrue'
import { profileKeys } from './keys'
import { toast } from 'react-hot-toast'
const mfaUnenroll = async (params: MFAUnenrollParams) => {
const { error, data } = await auth.mfa.unenroll(params)
if (error) throw error
return data
}
type CustomMFAUnenrollResponse = NonNullable<AuthMFAUnenrollResponse['data']>
type CustomMFAUnenrollError = NonNullable<AuthMFAUnenrollResponse['error']>
export const useMfaUnenrollMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<CustomMFAUnenrollResponse, CustomMFAUnenrollError, MFAUnenrollParams>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation((vars) => mfaUnenroll(vars), {
async onSuccess(data, variables, context) {
// when a factor is unenrolled, the aaLevel is bumped down if it's the last factor
await Promise.all([
queryClient.invalidateQueries(profileKeys.mfaFactors()),
queryClient.invalidateQueries(profileKeys.aaLevel()),
])
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to delete factor: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}
+24 -15
View File
@@ -1,5 +1,5 @@
import { QueryClient } from '@tanstack/react-query'
import { auth, getReturnToPath } from 'lib/gotrue'
import { auth, getAccessToken, getReturnToPath } from 'lib/gotrue'
import { useRouter } from 'next/router'
import { useCallback, useEffect } from 'react'
@@ -59,24 +59,33 @@ function useAutoAuthRedirect(queryClient: QueryClient) {
return
}
;(async () => {
const { error } = await auth.initialize()
auth
.initialize()
.then(async ({ error }) => {
if (error) {
// if there was a problem signing in via the url, don't redirect
return
}
if (error) {
// if there was a problem signing in via the url, don't redirect
return
}
const token = await getAccessToken()
const {
data: { session },
} = await auth.getSession()
if (token) {
const { data, error } = await auth.mfa.getAuthenticatorAssuranceLevel()
if (error) {
// if there was a problem fetching the user AAL levels, don't redirect
return
}
if (session) {
await queryClient.resetQueries()
// if the user doesn't have the appropriate AAL level, don't redirect
if (data.currentLevel !== data.nextLevel) {
return
}
await pushNext()
}
})()
await queryClient.resetQueries()
await pushNext()
}
})
.catch(() => {}) // catch all errors thrown by auth methods
}, [])
}
+35 -19
View File
@@ -4,11 +4,12 @@ import { ComponentType, useEffect } from 'react'
import { useParams } from 'common/hooks'
import { usePermissionsQuery } from 'data/permissions/permissions-query'
import { useAuthenticatorAssuranceLevelQuery } from 'data/profile/mfa-authenticator-assurance-level-query'
import { useSelectedProject, useStore } from 'hooks'
import { useAuth } from 'lib/auth'
import { IS_PLATFORM } from 'lib/constants'
import { STORAGE_KEY, getReturnToPath } from 'lib/gotrue'
import { NextPageWithLayout, isNextPageWithLayout } from 'types'
import { getReturnToPath, STORAGE_KEY } from 'lib/gotrue'
import { isNextPageWithLayout, NextPageWithLayout } from 'types'
import Error500 from '../../pages/500'
const PLATFORM_ONLY_PAGES = [
@@ -21,24 +22,30 @@ const PLATFORM_ONLY_PAGES = [
export function withAuth<T>(
WrappedComponent: ComponentType<T> | NextPageWithLayout<T, T>,
options?: {
redirectTo: string
/* run the redirect if the user is logged in */
redirectIfFound?: boolean
}
options: {
/**
* The auth level used to check the user credentials. In most cases, if the user has MFA enabled
* we want the highest level (which is 2) for all pages. For certain pages, the user should be
* able to access them even if he didn't finished his login (typed in his MFA code), for example
* the support page: We want the user to be able to submit a ticket even if he's not fully
* signed in.
* @default true
*/
useHighestAAL: boolean
} = { useHighestAAL: true }
) {
const WithAuthHOC: ComponentType<T> = (props: any) => {
const WithAuthHOC: ComponentType<T> = (props) => {
const router = useRouter()
const { basePath } = router
const { ref } = useParams()
const rootStore = useStore()
const { isLoading, session } = useAuth()
const { isLoading: isAALLoading, data: aalData } = useAuthenticatorAssuranceLevelQuery()
const { ui } = rootStore
const page = router.pathname.split('/').slice(3).join('/')
const redirectTo = options?.redirectTo ?? defaultRedirectTo(ref)
const redirectIfFound = options?.redirectIfFound
const redirectTo = defaultRedirectTo(ref)
usePermissionsQuery({
onError(error: any) {
@@ -51,18 +58,30 @@ export function withAuth<T>(
})
const isLoggedIn = Boolean(session)
const isCorrectLevel = options.useHighestAAL
? aalData?.currentLevel === aalData?.nextLevel
: true
const isAccessingBlockedPage =
!IS_PLATFORM &&
PLATFORM_ONLY_PAGES.some((platformOnlyPage) => page.startsWith(platformOnlyPage))
const isRedirecting =
isAccessingBlockedPage ||
checkRedirectTo(isLoading, router.pathname, isLoggedIn, redirectTo, redirectIfFound)
checkRedirectTo(
isLoading || isAALLoading,
router.pathname,
isLoggedIn,
isCorrectLevel,
redirectTo
)
useEffect(() => {
// This should run after setting store data
if (isRedirecting) {
router.push(redirectTo)
const searchParams = new URLSearchParams(location.search)
searchParams.set('returnTo', location.pathname)
const url = `${redirectTo}?${searchParams.toString()}`
router.push(url)
}
}, [isRedirecting, redirectTo])
@@ -114,17 +133,14 @@ function checkRedirectTo(
loading: boolean,
pathname: string,
isLoggedIn: boolean,
redirectTo: string,
redirectIfFound?: boolean
isCorrectLevel: boolean,
redirectTo: string
) {
if (loading) return false
if (pathname === redirectTo) return false
// If redirectTo is set, redirect if the user is not logged in.
if (redirectTo && !redirectIfFound && !isLoggedIn) return true
// If redirectIfFound is also set, redirect if the user was found
if (redirectIfFound && isLoggedIn) return true
// If redirectTo is set, redirect if the user is not logged in or logged in with MFA.
if (redirectTo && (!isLoggedIn || !isCorrectLevel)) return true
return false
}
+1 -1
View File
@@ -66,7 +66,7 @@ export function useSignOut() {
const result = await gotrueClient.signOut()
clearLocalStorage()
await queryClient.resetQueries()
await queryClient.clear()
return result
}, [queryClient])
+6
View File
@@ -10,8 +10,14 @@ export const PG_META_URL = IS_PLATFORM
: process.env.STUDIO_PG_META_URL
export const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
/**
* @deprecated use DATETIME_FORMAT
*/
export const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ'
// should be used for all dayjs formattings shown to the user. Includes timezone info.
export const DATETIME_FORMAT = 'DD MMM YYYY, HH:mm:ss (ZZ)'
// Keyboard Shortcuts Related
export const SHORTCUT_KEYS = {
VIEW_ALL_SHORTCUTS: 'VIEW_ALL_SHORTCUTS',
+9 -4
View File
@@ -1,9 +1,6 @@
import { IS_PLATFORM } from './constants'
import { User, Session } from '@supabase/gotrue-js'
import { Session, User } from '@supabase/gotrue-js'
import { gotrueClient } from 'common'
import { getFlags } from './configcat'
export { STORAGE_KEY } from 'common'
export const auth = gotrueClient
@@ -70,6 +67,14 @@ export const getIdentity = (gotrueUser: User) => {
}
}
/**
* Transfers the search params from the current location path to a newly built path
*/
export const buildPathWithParams = (pathname: string) => {
const searchParams = new URLSearchParams(location.search)
return `${pathname}?${searchParams.toString()}`
}
// NOTE: do not use any imports in this function as it is used standalone in the documents head
// [Joshen] Potentially can remove after full move over to /dashboard
export const getReturnToPath = (fallback = '/projects') => {
+1 -1
View File
@@ -35,7 +35,7 @@
"@supabase/auth-helpers-nextjs": "^0.5.6",
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/react-data-grid": "^7.1.0-beta.7",
"@supabase/shared-types": "^0.1.46",
"@supabase/shared-types": "0.1.47",
"@supabase/supabase-js": "^2.23.0",
"@tanstack/react-query": "^4.22.0",
"@tanstack/react-query-devtools": "^4.22.0",
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more