diff --git a/apps/docs/components/Extensions.tsx b/apps/docs/components/Extensions.tsx index b49c5ad83d3..8740be67870 100644 --- a/apps/docs/components/Extensions.tsx +++ b/apps/docs/components/Extensions.tsx @@ -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 diff --git a/apps/docs/pages/guides/self-hosting/docker.mdx b/apps/docs/pages/guides/self-hosting/docker.mdx index 649bb5e2d83..53f2c44be70 100644 --- a/apps/docs/pages/guides/self-hosting/docker.mdx +++ b/apps/docs/pages/guides/self-hosting/docker.mdx @@ -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://:8000/storage/v1/` - Realtime: `http://: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://:8000/functions/v1/hello`. +Edge Functions are stored in `volumes/functions`. The default setup has a `hello` Function that you can invoke on `http://:8000/functions/v1/hello`. You can add new Functions as `volumes/functions//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: + + + +- All data in analytics will be deleted when you run the above commands. + + + + +```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 }) => export default Page + diff --git a/apps/www/_blog/2023-02-03-openai-embeddings-postgres-vector.mdx b/apps/www/_blog/2023-02-03-openai-embeddings-postgres-vector.mdx index 6055988c8bb..7184cd66f41 100644 --- a/apps/www/_blog/2023-02-03-openai-embeddings-postgres-vector.mdx +++ b/apps/www/_blog/2023-02-03-openai-embeddings-postgres-vector.mdx @@ -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) diff --git a/apps/www/_blog/2023-02-07-chatgpt-supabase-docs.mdx b/apps/www/_blog/2023-02-07-chatgpt-supabase-docs.mdx index 2a7a46d93fb..fe8f61cf7df 100644 --- a/apps/www/_blog/2023-02-07-chatgpt-supabase-docs.mdx +++ b/apps/www/_blog/2023-02-07-chatgpt-supabase-docs.mdx @@ -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/) diff --git a/apps/www/_blog/2023-07-13-pgvector-performance.mdx b/apps/www/_blog/2023-07-13-pgvector-performance.mdx index 43f52d92072..4a2df4293a0 100644 --- a/apps/www/_blog/2023-07-13-pgvector-performance.mdx +++ b/apps/www/_blog/2023-07-13-pgvector-performance.mdx @@ -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
-🚀 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)
-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.
@@ -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
@@ -178,13 +177,13 @@ The Qdrant benchmark uses “default” configuration and is in not indicative o />
-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.
-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`.
-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`.
+ +💡 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. + + + +## 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. + +
+ visual of an example skip list (light) + visual of an example skip list (dark) +
+ +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. + +visual of an example navigable small world graph + +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 + +
+ wikipedia embeddings comparing ivfflat and hnsw queries-per-second (light) + wikipedia embeddings comparing ivfflat and hnsw queries-per-second (dark) +
+ +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 + +
+ dbpedia embeddings comparing ivfflat and hnsw queries-per-second (light) + dbpedia embeddings comparing ivfflat and hnsw queries-per-second (dark) +
+ +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. + +
+ dbpedia embeddings comparing qdrant and hnsw queries-per-second (light) + dbpedia embeddings comparing qdrant and hnsw queries-per-second (dark) +
+ +### 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. + +
+ dbpedia embeddings comparing ivfflat and hnsw queries-per-second using the 4XL compute addon (light) + dbpedia embeddings comparing ivfflat and hnsw queries-per-second using the 4XL compute addon (dark) +
+ +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. + +
+ dbpedia embeddings comparing hnsw queries-per-second using the 2XL vs 4XL compute addon (light) + dbpedia embeddings comparing hnsw queries-per-second using the 2XL vs 4XL compute addon (dark) +
+ +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. + +
+ dbpedia embeddings comparing hnsw queries-per-second using different build parameters (light) + dbpedia embeddings comparing hnsw queries-per-second using different build parameters (dark) +
+ +## 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` + +
+ dbpedia embeddings comparing ivfflat index build time between v0.4.0 and v0.5.0 using 1000 lists (light) + dbpedia embeddings comparing ivfflat index build time between v0.4.0 and v0.5.0 using 1000 lists (dark) +
+ +- `lists = 5000` + +
+ dbpedia embeddings comparing ivfflat index build time between v0.4.0 and v0.5.0 using 5000 lists (light) + dbpedia embeddings comparing ivfflat index build time between v0.4.0 and v0.5.0 using 5000 lists (dark) +
+ +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) diff --git a/apps/www/components/Sections/GithubExamples.tsx b/apps/www/components/Sections/GithubExamples.tsx index 85e3cb4e279..ff3a5027626 100644 --- a/apps/www/components/Sections/GithubExamples.tsx +++ b/apps/www/components/Sections/GithubExamples.tsx @@ -57,6 +57,7 @@ function GithubExamples() {
https://supabase.com/blog Latest Postgres news from Greg Richardson at Supabase en - Wed, 02 Aug 2023 06:00:00 GMT + Tue, 05 Sep 2023 06:00:00 GMT + https://supabase.com/blog/increase-performance-pgvector-hnsw + pgvector v0.5.0: Faster semantic search with HNSW indexes + https://supabase.com/blog/increase-performance-pgvector-hnsw + Increase performance in pgvector using HNSW indexes + Tue, 05 Sep 2023 06:00:00 GMT + + https://supabase.com/blog/fewer-dimensions-are-better-pgvector pgvector: Fewer dimensions are better https://supabase.com/blog/fewer-dimensions-are-better-pgvector diff --git a/apps/www/public/rss.xml b/apps/www/public/rss.xml index 6e12b951821..32b4e7bec05 100644 --- a/apps/www/public/rss.xml +++ b/apps/www/public/rss.xml @@ -5,9 +5,16 @@ https://supabase.com Latest news from Supabase en - Wed, 30 Aug 2023 06:00:00 GMT + Tue, 05 Sep 2023 06:00:00 GMT + https://supabase.com/blog/increase-performance-pgvector-hnsw + pgvector v0.5.0: Faster semantic search with HNSW indexes + https://supabase.com/blog/increase-performance-pgvector-hnsw + Increase performance in pgvector using HNSW indexes + Tue, 05 Sep 2023 06:00:00 GMT + + https://supabase.com/blog/organization-based-billing Organization-based Billing, Project Transfers, Team plan https://supabase.com/blog/organization-based-billing diff --git a/package-lock.json b/package-lock.json index 5fd3f5787a3..19ad31b2ede 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/apps/docs/data/extensions.json b/packages/shared-data/extensions.json similarity index 100% rename from apps/docs/data/extensions.json rename to packages/shared-data/extensions.json diff --git a/packages/shared-data/index.ts b/packages/shared-data/index.ts index 07592bab736..bee5b4e8389 100644 --- a/packages/shared-data/index.ts +++ b/packages/shared-data/index.ts @@ -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 } diff --git a/packages/shared-data/tsconfig.json b/packages/shared-data/tsconfig.json index cd6c94d6e8b..6bfd004e4e6 100644 --- a/packages/shared-data/tsconfig.json +++ b/packages/shared-data/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "tsconfig/react-library.json", "include": ["."], - "exclude": ["dist", "build", "node_modules"] + "exclude": ["dist", "build", "node_modules"], + "compilerOptions": { + "resolveJsonModule": true + } } diff --git a/spec/examples/examples.yml b/spec/examples/examples.yml index bd034b94223..4960b1f29a5 100644 --- a/spec/examples/examples.yml +++ b/spec/examples/examples.yml @@ -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' diff --git a/spec/supabase_js_v2.yml b/spec/supabase_js_v2.yml index aa2337b47b5..601b892c97b 100644 --- a/spec/supabase_js_v2.yml +++ b/spec/supabase_js_v2.yml @@ -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' diff --git a/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx b/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx new file mode 100644 index 00000000000..5eeff45f7e8 --- /dev/null +++ b/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx @@ -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 ( + <> + + onClose()} + onClose={onClose} + /> + + ) +} + +interface FirstStepProps { + visible: boolean + name: string + setName: Dispatch> + enroll: (params: { factorType: 'totp'; friendlyName?: string }) => void + isEnrolling: boolean + onClose: () => void +} + +const FirstStep = ({ visible, name, enroll, setName, isEnrolling, onClose }: FirstStepProps) => { + return ( + { + enroll({ + factorType: 'totp', + friendlyName: name, + }) + }} + > + +
+ setName(e.target.value)} + /> +
+
+
+ ) +} + +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 ( + { + // 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 })} + > + + <> +
+ + Use an authenticator app to scan the following QR code, and provide the code from the + app to complete the enrolment. + +
+ {isLoading && ( +
+ +
+ )} + {factor && ( + <> +
+
+ {factor.totp.uri} +
+
+
+ + } + /> +
+
+ setCode(e.target.value)} + /> +
+ + )} + +
+
+ ) +} + +export default AddNewFactorModal diff --git a/studio/components/interfaces/Account/TOTPFactors/DeleteFactorModal.tsx b/studio/components/interfaces/Account/TOTPFactors/DeleteFactorModal.tsx new file mode 100644 index 00000000000..63e4dccc634 --- /dev/null +++ b/studio/components/interfaces/Account/TOTPFactors/DeleteFactorModal.tsx @@ -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 ( + factorId && unenroll({ factorId })} + > + + + + + {lastFactorToBeDeleted + ? 'Multi-factor authentication will be disabled' + : 'This action cannot be undone'} + + + {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'} + + +
+

Before deleting this factor, consider:

+
    + {lastFactorToBeDeleted ? ( + <> +
  • Adding another authenticator app as a factor prior to deleting
  • +
  • Ensure that your account does not need multi-factor authentication
  • + + ) : ( + <> +
  • Your backup authenticator app is still available to use
  • +
  • Adding another authenticator app thereafter as a backup
  • + + )} +
+
+
+
+ ) +} + +export default DeleteFactorModal diff --git a/studio/components/interfaces/Account/TOTPFactors/index.tsx b/studio/components/interfaces/Account/TOTPFactors/index.tsx new file mode 100644 index 00000000000..b974a805403 --- /dev/null +++ b/studio/components/interfaces/Account/TOTPFactors/index.tsx @@ -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(null) + const { data, isLoading, isError, isSuccess, error } = useMfaListFactorsQuery() + + return ( + <> +
+

+ Generate one-time passwords via authenticator apps like 1Password, Authy, etc. as a second + factor to verify your identity during sign-in. +

+
+ {isLoading && } + {isError && ( + + )} + {isSuccess && ( + <> + {data.totp.length === 1 && ( + + + + We recommend configuring two authenticator apps across different devices + + + The two authenticator apps will serve as a backup for each other. + + + )} +
+ {data.totp.map((factor) => { + return ( +
+

+ Name:{' '} + {factor.friendly_name ?? 'No name provided'} +

+
+

+ Added on {dayjs(factor.updated_at).format(DATETIME_FORMAT)} +

+ +
+
+ ) + })} +
+ {data.totp.length < 2 ? ( + <> +
+ +
+ + ) : null} + + )} +
+
+ setIsAddNewFactorOpen(false)} + /> + setFactorToBeDeleted(null)} + /> + + ) +} + +export default TOTPFactors diff --git a/studio/components/interfaces/Account/index.ts b/studio/components/interfaces/Account/index.ts index 4ce89542872..4630ecbb9d8 100644 --- a/studio/components/interfaces/Account/index.ts +++ b/studio/components/interfaces/Account/index.ts @@ -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 } diff --git a/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx b/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx index 98655676d3c..e1b8e396fde 100644 --- a/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx +++ b/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx @@ -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) => {
-

- {extension.name} -

+
+

+ {extension.name} +

+ {extensions.find((item: any) => item.name === extension.name) ? ( + 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 ?? '' + } + > + + + + + ) : null} +
{loading ? ( ) : ( @@ -101,7 +127,7 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
{isOn && extension.schema && (
-
+
Schema: {`${extension.schema}`}
diff --git a/studio/components/interfaces/SignIn/SignInForm.tsx b/studio/components/interfaces/SignIn/SignInForm.tsx index 934b401e21e..fcfb9e20a42 100644 --- a/studio/components/interfaces/SignIn/SignInForm.tsx +++ b/studio/components/interfaces/SignIn/SignInForm.tsx @@ -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() diff --git a/studio/components/interfaces/SignIn/SignInMfaForm.tsx b/studio/components/interfaces/SignIn/SignInMfaForm.tsx new file mode 100644 index 00000000000..93f3566cd73 --- /dev/null +++ b/studio/components/interfaces/SignIn/SignInMfaForm.tsx @@ -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(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 && } + + {isErrorFactors && } + + {isSuccessFactors && ( +
+ {() => ( + <> +
+ } + 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 + } + /> + +
+ + +
+
+ + )} +
+ )} + + + + ) +} + +export default SignInMfaForm diff --git a/studio/components/interfaces/SignIn/SignInSSOForm.tsx b/studio/components/interfaces/SignIn/SignInSSOForm.tsx index 3d1585c0e57..e53a992b2cb 100644 --- a/studio/components/interfaces/SignIn/SignInSSOForm.tsx +++ b/studio/components/interfaces/SignIn/SignInSSOForm.tsx @@ -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, }, }) diff --git a/studio/components/interfaces/SignIn/SignInWithGitHub.tsx b/studio/components/interfaces/SignIn/SignInWithGitHub.tsx index a34d53a49b1..4a5eb812cc1 100644 --- a/studio/components/interfaces/SignIn/SignInWithGitHub.tsx +++ b/studio/components/interfaces/SignIn/SignInWithGitHub.tsx @@ -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 diff --git a/studio/components/interfaces/Support/Success.tsx b/studio/components/interfaces/Support/Success.tsx index a9835a912e1..c690d0fa9dd 100644 --- a/studio/components/interfaces/Support/Success.tsx +++ b/studio/components/interfaces/Support/Success.tsx @@ -70,9 +70,9 @@ const Success = ({ sentCategory = '' }: SuccessProps) => {
diff --git a/studio/components/interfaces/Support/Support.constants.ts b/studio/components/interfaces/Support/Support.constants.ts index d5922d09182..0e14297fb64 100644 --- a/studio/components/interfaces/Support/Support.constants.ts +++ b/studio/components/interfaces/Support/Support.constants.ts @@ -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 = [ diff --git a/studio/components/interfaces/Support/SupportForm.tsx b/studio/components/interfaces/Support/SupportForm.tsx index 54d68cc43e2..8404d86635f 100644 --- a/studio/components/interfaces/Support/SupportForm.tsx +++ b/studio/components/interfaces/Support/SupportForm.tsx @@ -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 (
@@ -314,121 +313,125 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
-
-
- {isLoadingProjects && ( -
-

Which project is affected?

- -
- )} - {isErrorProjects && ( -
-

Which project is affected?

-
- -

Failed to retrieve projects

-
-
- )} - {isSuccessProjects && ( - { - setSelectedProjectRef(val) - }} - > - {projects.map((option) => { - const organization = organizations?.find( - (x) => x.id === option.organization_id - ) - return ( - - {option.name} - {organization?.name} - - ) - })} - - )} - - {SEVERITY_OPTIONS.map((option: any) => { - return ( - - {option.label} - {option.description} - - ) - })} - -
- - {values.projectRef !== 'no-project' && subscription && isSuccessProjects ? ( -

- This project is on the{' '} - {subscription.plan.name} plan -

- ) : isLoadingSubscription && selectedProjectRef !== 'no-project' ? ( -
- -

Checking project's plan

-
- ) : ( - <> - )} -
- - {isSuccessProjects && values.projectRef === 'no-project' && ( + {values.category !== 'Login_issues' && (
- {isLoadingOrganizations && ( -
-

Which organization is affected?

- -
- )} - {isErrorOrganizations && ( -
-

Which organization is affected?

-
- -

Failed to retrieve organizations

+
+ {isLoadingProjects && ( +
+

Which project is affected?

+
-
- )} - {isSuccessOrganizations && ( - - {organizations?.map((option) => { + )} + {isErrorProjects && ( +
+

Which project is affected?

+
+ +

Failed to retrieve projects

+
+
+ )} + {isSuccessProjects && ( + { + setSelectedProjectRef(val) + }} + > + {projects.map((option) => { + const organization = organizations?.find( + (x) => x.id === option.organization_id + ) + return ( + + {option.name} + {organization?.name} + + ) + })} + + )} + + {SEVERITY_OPTIONS.map((option: any) => { return ( - {option.name} + {option.label} + {option.description} ) })} +
+ + {values.projectRef !== 'no-project' && subscription && isSuccessProjects ? ( +

+ This project is on the{' '} + {subscription.plan.name} plan +

+ ) : isLoadingSubscription && selectedProjectRef !== 'no-project' ? ( +
+ +

Checking project's plan

+
+ ) : ( + <> )}
)} - {subscription?.plan.id === 'free' && ( + {isSuccessProjects && + values.projectRef === 'no-project' && + values.category !== 'Login_issues' && ( +
+ {isLoadingOrganizations && ( +
+

Which organization is affected?

+ +
+ )} + {isErrorOrganizations && ( +
+

Which organization is affected?

+
+ +

Failed to retrieve organizations

+
+
+ )} + {isSuccessOrganizations && ( + + {organizations?.map((option) => { + return ( + + {option.name} + + ) + })} + + )} +
+ )} + + {subscription?.plan.id === 'free' && values.category !== 'Login_issues' && (
} @@ -603,16 +606,18 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
)} -
-

Which services are affected?

- -
+ {values.category !== 'Login_issues' && ( +
+

Which services are affected?

+ +
+ )}
0 + ? [ + { + isActive: router.pathname === `/account/security`, + icon: `${router.basePath}/img/user.svg`, + label: 'Security', + href: `/account/security`, + key: `/account/security`, + }, + ] + : []), ], }, ] diff --git a/studio/components/layouts/SettingsLayout/AccountSettingsMenu.tsx b/studio/components/layouts/SettingsLayout/AccountSettingsMenu.tsx index 80c3b593dd3..73fa77e608b 100644 --- a/studio/components/layouts/SettingsLayout/AccountSettingsMenu.tsx +++ b/studio/components/layouts/SettingsLayout/AccountSettingsMenu.tsx @@ -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 ( diff --git a/studio/components/layouts/SignInLayout/SignInLayout.tsx b/studio/components/layouts/SignInLayout/SignInLayout.tsx index 387c0f15167..d8907e92cef 100644 --- a/studio/components/layouts/SignInLayout/SignInLayout.tsx +++ b/studio/components/layouts/SignInLayout/SignInLayout.tsx @@ -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) => { + 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 && ( - -