mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
75 files changed
+485
-544
No files matched your search
@@ -0,0 +1,33 @@
|
||||
name: Mirror Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Image tag"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
registry: public.ecr.aws
|
||||
username: ${{ secrets.PROD_ACCESS_KEY_ID }}
|
||||
password: ${{ secrets.PROD_SECRET_ACCESS_KEY }}
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: akhilerm/tag-push-action@v2.1.0
|
||||
with:
|
||||
src: docker.io/supabase/studio:${{ inputs.version }}
|
||||
dst: |
|
||||
public.ecr.aws/supabase/studio:${{ inputs.version }}
|
||||
ghcr.io/supabase/studio:${{ inputs.version }}
|
||||
@@ -16,13 +16,17 @@ on:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
supabase/studio
|
||||
public.ecr.aws/t3w2s2c9/studio
|
||||
public.ecr.aws/supabase/studio
|
||||
ghcr.io/supabase/studio
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
@@ -49,6 +53,13 @@ jobs:
|
||||
username: ${{ secrets.PROD_ACCESS_KEY_ID }}
|
||||
password: ${{ secrets.PROD_SECRET_ACCESS_KEY }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ Then visit, and edit, any of the following sites:
|
||||
| ---------------------------------------------- | -------------- | ---------- | ------------------------------------ | ------------------------ |
|
||||
| [supabase.com](https://supabase.com) | `/apps/www` | www | The main website | http://localhost:3000 |
|
||||
| [app.supabase.com](https://app.supabase.com) | `/studio` | studio | Studio dashboard | http://localhost:8082 |
|
||||
| [supabase.com/docs](https://supabase.com/docs) | `/apps/docs` | docs | Guides and Reference (Next.js based) | http://localhost:3001 |
|
||||
| [supabase.com/docs](https://supabase.com/docs) | `/apps/docs` | docs | Guides and Reference (Next.js based) | http://localhost:3001/docs |
|
||||
|
||||
#### Running sites individually
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ Supabase is a combination of open source tools. We’re building the features of
|
||||
Supabase is a [hosted platform](https://app.supabase.com). You can sign up and start using Supabase without installing anything.
|
||||
You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and [develop locally](https://supabase.com/docs/guides/local-development).
|
||||
|
||||

|
||||

|
||||
|
||||
- [PostgreSQL](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.
|
||||
- [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients.
|
||||
|
||||
@@ -237,7 +237,7 @@ export const menuItems: NavMenu = {
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/realtime', items: [] },
|
||||
{ name: 'Quickstart', url: '/guides/realtime/quickstart', items: [] },
|
||||
{ name: 'Postgres CDC', url: '/guides/realtime/postgres-cdc', items: [] },
|
||||
{ name: 'Postgres Changes', url: '/guides/realtime/postgres-changes', items: [] },
|
||||
{ name: 'Rate Limits', url: '/guides/realtime/rate-limits', items: [] },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -92,7 +92,7 @@ Fast GraphQL APIs using our custom Postgres GraphQL extension. [Docs](/docs/guid
|
||||
|
||||
### Realtime Database changes
|
||||
|
||||
Receive your database changes through websockets. [Docs](/docs/guides/realtime/postgres-cdc).
|
||||
Receive your database changes through websockets. [Docs](/docs/guides/realtime/postgres-changes).
|
||||
|
||||
### User Broadcasting
|
||||
|
||||
@@ -156,7 +156,7 @@ Both Postgres and the Supabase Platform are production-ready. Some tools we offe
|
||||
| Database | Point-in-Time Recovery | `alpha` |
|
||||
| Database | Vault | `alpha` |
|
||||
| Studio | | `GA` |
|
||||
| Realtime | Postgres CDC | `GA` |
|
||||
| Realtime | Postgres Changes | `GA` |
|
||||
| Realtime | Broadcast | `beta` |
|
||||
| Realtime | Presence | `beta` |
|
||||
| Storage | Backend (S3) | `GA` |
|
||||
|
||||
@@ -8,7 +8,7 @@ export const meta = {
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
The pg_net API is in beta. Functions signatures may change.
|
||||
The pg_net API is in alpha. Functions signatures may change.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -150,102 +150,6 @@ request_id
|
||||
|
||||
After triggering `http_post`, use [`http_get_result`](#http_get_result) to get the result of the request.
|
||||
|
||||
## `http_collect_response` [#http_collect_response]
|
||||
|
||||
Given a `request_id` reference, retrieves the response.
|
||||
|
||||
When `async:=false` is set it is recommended that [statement_timeout](https://www.postgresql.org/docs/13/runtime-config-client.html) is set for the maximum amount of time the caller is willing to wait in case the response is slow to populate.
|
||||
|
||||
### Signature
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
This is a Postgres SECURITY DEFINER function
|
||||
|
||||
</Admonition>
|
||||
|
||||
```sql
|
||||
net.http_collect_response(
|
||||
-- request_id reference
|
||||
request_id bigint,
|
||||
-- when `true`, return immediately. when `false` wait for the request to complete before returning
|
||||
async bool default true
|
||||
)
|
||||
-- http response composite wrapped in a result type
|
||||
returns net.http_response_result
|
||||
|
||||
strict
|
||||
volatile
|
||||
parallel safe
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
`net.http_collect_response` must be in a separate transaction from the calls to `net.http_<method>`
|
||||
|
||||
</Admonition>
|
||||
|
||||
```sql
|
||||
select
|
||||
net.http_post(
|
||||
url:='https://httpbin.org/post',
|
||||
body:='{"hello": "world"}'::jsonb
|
||||
) as request_id;
|
||||
request_id
|
||||
----------
|
||||
1
|
||||
(1 row)
|
||||
|
||||
select * from net.http_collect_response(1, async:=false);
|
||||
status | message | response
|
||||
--------+---------+----------
|
||||
SUCCESS ok (
|
||||
status_code := 200,
|
||||
headers := '{"date": ...}',
|
||||
body := '{"args": ...}'
|
||||
)::net.http_response_result
|
||||
|
||||
|
||||
select
|
||||
(response).body::json
|
||||
from
|
||||
net.http_collect_response(request_id:=1);
|
||||
body
|
||||
-------------------------------------------------------------------
|
||||
{
|
||||
"args": {},
|
||||
"data": "{\"hello\": \"world\"}",
|
||||
"files": {},
|
||||
"form": {},
|
||||
"headers": {
|
||||
"Accept": "*/*",
|
||||
"Content-Length": "18",
|
||||
"Content-Type": "application/json",
|
||||
"Host": "httpbin.org",
|
||||
"User-Agent": "pg_net/0.2",
|
||||
"X-Amzn-Trace-Id": "Root=1-61031a5c-7e1afeae69bffa8614d8e48e"
|
||||
},
|
||||
"json": {
|
||||
"hello": "world"
|
||||
},
|
||||
"origin": "135.63.38.488",
|
||||
"url": "https://httpbin.org/post"
|
||||
}
|
||||
(1 row)
|
||||
```
|
||||
|
||||
Where `response` is a composite:
|
||||
|
||||
```sql
|
||||
status_code integer
|
||||
headers jsonb
|
||||
body text
|
||||
```
|
||||
|
||||
Possible values for `net.http_response_result.status` are `('PENDING', 'SUCCESS', 'ERROR')`
|
||||
|
||||
## Resources
|
||||
|
||||
- Source code: [github.com/supabase/pg_net](https://github.com/supabase/pg_net/)
|
||||
|
||||
@@ -106,7 +106,7 @@ You should see the response `{ "message":"Hello Functions!" }`.
|
||||
<details>
|
||||
<summary>Implementation details</summary>
|
||||
|
||||
- All Edge Functions are `POST` requests.
|
||||
- Edge Functions don't serve HTML content (`GET` requests that return `text/html` are rewritten to `text/plain`).
|
||||
- The `Authorization` header is required. You can use either the `ANON` key, the `SERVICE_ROLE` key, or a logged-in user's JWT.
|
||||
- The Function is proxied through the local API (`http://localhost:54321`)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ Manually enter a couple of rows of data, so that we have something to read from
|
||||
|
||||
## The `Get all` reviews route in Zuplo
|
||||
|
||||
Login to Zuplo at [portal.zuplo.com](https://portal.zuplo.com] and create a new project in Zuplo - I went with `supabase-ski-reviews`.
|
||||
Login to Zuplo at [portal.zuplo.com](https://portal.zuplo.com) and create a new project in Zuplo - I went with `supabase-ski-reviews`.
|
||||
|
||||
Select the **File** tab and choose **Routes**. Add your first route with the following settings:
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Custom Domains are currently in beta, and are being slowly made available to pro
|
||||
|
||||
</Admonition>
|
||||
|
||||
Custom domains allow you to present a branded experience to your users. You can also use a subdomain for the purposes of this guide.
|
||||
Custom domains allow you to present a branded experience to your users. Currently, you must use a subdomain (e.g., `api.example.com`, rather than `example.com`) for the purposes of this guide.
|
||||
|
||||
To get started:
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
export const meta = {
|
||||
id: 'realtime',
|
||||
title: 'Realtime',
|
||||
description: 'Supabase Realtime with Broadcast, Presence, and Postgres CDC.',
|
||||
description: 'Supabase Realtime with Broadcast, Presence, and Postgres Changes.',
|
||||
sidebar_label: 'Overview',
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ Supabase provides a globally distributed cluster of [Realtime](https://github.co
|
||||
|
||||
- [Broadcast](#broadcast): Send ephemeral messages from client to clients with low latency.
|
||||
- [Presence](#presence): Track and synchronize shared state between clients.
|
||||
- [Postgres CDC](#postgres-cdc): Listen to Postgres database changes and send them to authorized clients.
|
||||
- [Postgres Changes](#postgres-changes): Listen to Postgres database changes and send them to authorized clients.
|
||||
|
||||
A [channel](https://hexdocs.pm/phoenix/channels.html) is the basic building block of Realtime and narrows the scope of data flow to subscribed clients. You can think of a channel as a chatroom where participants are able to see who's online and send and receive messages; similar to a Discord or Slack channel.
|
||||
|
||||
All clients can connect to a channel and take advantage of the built-in features, Broadcast and Presence, while extenstions, like Postgres CDC, must be enabled prior to use.
|
||||
All clients can connect to a channel and take advantage of the built-in features, Broadcast and Presence, while extenstions, like Postgres Changes, must be enabled prior to use.
|
||||
|
||||
## Broadcast
|
||||
|
||||
@@ -37,9 +37,9 @@ Clients are free to come-and-go as they please, and as long as they are all subs
|
||||
|
||||
The neat thing about Presence is that if a client is suddenly disconnected (for example, they go offline), their state will be automatically removed from the shared state. If you've ever tried to build an “I'm online” feature which handles unexpected disconnects, you'll appreciate how useful this is.
|
||||
|
||||
## Postgres CDC
|
||||
## Postgres Changes
|
||||
|
||||
Postgres Change Data Capture (CDC) enables you to listen to database changes and have them broadcast to authorized clients based on [Row Level Security (RLS)](/docs/guides/auth/row-level-security) policies.
|
||||
Postgres Changes enable you to listen to database changes and have them broadcast to authorized clients based on [Row Level Security (RLS)](/docs/guides/auth/row-level-security) policies.
|
||||
|
||||
This works by Realtime polling your database's logical replication slot for changes, passing those changes to the [apply_rls](https://github.com/supabase/walrus#reading-wal) SQL function to determine which clients have permission, and then using Broadcast to send those changes to clients.
|
||||
|
||||
|
||||
+5
-5
@@ -1,18 +1,18 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'postgres-cdc',
|
||||
title: 'Postgres CDC',
|
||||
description: "Getting started with Realtime's Postgres CDC feature",
|
||||
id: 'postgres-changes',
|
||||
title: 'Postgres Changes',
|
||||
description: "Getting started with Realtime's Postgres Changes feature",
|
||||
}
|
||||
|
||||
Realtime's Postgres Change Data Capture (CDC) feature listens for database changes and sends them to clients. Clients are required to subscribe with a JWT dictating which changes they are allowed to receive based on the database's [Row Level Security](/docs/guides/auth/row-level-security).
|
||||
Realtime's Postgres Changes feature listens for database changes and sends them to clients. Clients are required to subscribe with a JWT dictating which changes they are allowed to receive based on the database's [Row Level Security](/docs/guides/auth/row-level-security).
|
||||
|
||||
Anyone with access to a valid JWT signed with the project's JWT secret is able to listen to your database's changes, unless tables have [Row Level Security](/docs/guides/auth/row-level-security) enabled and policies in place.
|
||||
|
||||
Clients can choose to receive `INSERT`, `UPDATE`, `DELETE`, or `*` (all) changes for all changes in a schema, a table in a schema, or a column's value in a table. Your clients should only listen to tables in the `public` schema and you must first enable the tables you want your clients to listen to.
|
||||
|
||||
Postgres CDC works out of the box for tables in the `public` schema. You can listen to tables in your private schemas by granting table `SELECT` permissions to the database role found in your access token. You can run a query similar to the following:
|
||||
Postgres Changes works out of the box for tables in the `public` schema. You can listen to tables in your private schemas by granting table `SELECT` permissions to the database role found in your access token. You can run a query similar to the following:
|
||||
|
||||
```sql
|
||||
GRANT SELECT ON "private_schema"."table" TO authenticated;
|
||||
@@ -7,7 +7,7 @@ export const meta = {
|
||||
sidebar_label: 'Quickstart',
|
||||
}
|
||||
|
||||
Learn how to build [multiplayer.dev](https://multiplayer.dev), a collaborative app that demonstrates Broadcast, Presence, and Postgres CDC using [Realtime](/docs/guides/realtime).
|
||||
Learn how to build [multiplayer.dev](https://multiplayer.dev), a collaborative app that demonstrates Broadcast, Presence, and Postgres Changes using [Realtime](/docs/guides/realtime).
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
@@ -185,7 +185,7 @@ If a channel is set up without a presence key, the server generates a random UUI
|
||||
|
||||
## Insert and Receive Persisted Messages
|
||||
|
||||
[Postgres Change Data Capture (CDC)](/docs/guides/realtime#postgres-cdc) enables your client to insert, update, or delete database records and send the changes to clients. Create a `messages` table to keep track of messages created by users in specific rooms:
|
||||
[Postgres Changes](/docs/guides/realtime#postgres-changes) enables your client to insert, update, or delete database records and send the changes to clients. Create a `messages` table to keep track of messages created by users in specific rooms:
|
||||
|
||||
```sql
|
||||
create table messages (
|
||||
|
||||
@@ -56,7 +56,7 @@ You can use it completely, or just the features you need.
|
||||
className="card"
|
||||
to={'/guides/realtime'}
|
||||
title={'Realtime'}
|
||||
description={'Build with Broadcast, Presence, and Postgres CDC.'}
|
||||
description={'Build with Broadcast, Presence, and Postgres Changes.'}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -799,7 +799,7 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/postgres-cdc</loc>
|
||||
<loc>https://supabase.com/docs/guides/realtime/postgres-changes</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
@@ -18,7 +18,7 @@ To understand how TCE works, let's first do a deep-dive into an important encryp
|
||||
|
||||
<div className="bg-gray-300 rounded-lg p-6 italic">
|
||||
|
||||
This post is a sneak peek of a big feature we are shipping on LW6. To not miss a thing, get your ticket for [Supabase Launch Week 6!](https://supabase.com/launch-week).
|
||||
This post is a sneak peek of a big feature we are shipping on LW6. To not miss a thing, get your ticket for [Supabase Launch Week 6!](https://supabase.com/launch-week).
|
||||
|
||||
</div>
|
||||
|
||||
@@ -28,23 +28,23 @@ The current state-of-the-art in encryption libraries is [libsodium](https://doc.
|
||||
|
||||
**libsodium** offers a range of APIs for authenticated secret and public key encryption, key derivation, encrypted streaming, [AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption), various forms of hashing, and much more.
|
||||
|
||||
This powerful API is available to PostgreSQL using the [**pgsodium**](https://github.com/michelp/pgsodium) extension. **pgsodium** provides all the functionality of the full **libsodium** API, but previously it required developers to set up database encryption themselves, which remained a challenge even for those familiar with database administration.
|
||||
This powerful API is available to PostgreSQL using the [**pgsodium**](https://github.com/michelp/pgsodium) extension. **pgsodium** provides all the functionality of the full **libsodium** API, but previously it required developers to set up database encryption themselves, which remained a challenge even for those familiar with database administration.
|
||||
|
||||
To solve this problem, **pgsodium** now has a full key management API, primarily via the table `pgsodium.key` and the `pgsodium.create_key()` function. This key table contains no raw keys, but instead uses libsodium Key IDs to derive keys that are used internally for encryption. A [key derivation function](https://libsodium.gitbook.io/doc/key_derivation) is used with an internal root key that is unavailable to SQL and not stored in the database, but rather managed by you externally using flexible scripts, or by Supabase automatically as part of our service offering.
|
||||
To solve this problem, **pgsodium** now has a full key management API, primarily via the table `pgsodium.key` and the `pgsodium.create_key()` function. This key table contains no raw keys, but instead uses libsodium Key IDs to derive keys that are used internally for encryption. A [key derivation function](https://libsodium.gitbook.io/doc/key_derivation) is used with an internal root key that is unavailable to SQL and not stored in the database, but rather managed by you externally using flexible scripts, or by Supabase automatically as part of our service offering.
|
||||
|
||||
The simplest way to use **pgsodium** to encrypt and decrypt data is to first create a ******Key ID******. Valid Key IDs are stored in pgsodium in a special extension table, and they can be created using the `pgsodium.create_key()` function. This function takes a number of arguments depending on how it's used, but the simplest case is to create a new key with no arguments:
|
||||
The simplest way to use **pgsodium** to encrypt and decrypt data is to first create a **Key ID**. Valid Key IDs are stored in pgsodium in a special extension table, and they can be created using the `pgsodium.create_key()` function. This function takes a number of arguments depending on how it's used, but the simplest case is to create a new key with no arguments:
|
||||
|
||||
```sql
|
||||
select * from pgsodium.create_key();
|
||||
-[ RECORD 1 ]---+-------------------------------------
|
||||
id | eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d
|
||||
name |
|
||||
name |
|
||||
status | valid
|
||||
key_type | aead-det
|
||||
key_id | 2
|
||||
key_context | \x7067736f6469756d
|
||||
created | 2022-11-13 21:19:35.765823+00
|
||||
expires |
|
||||
expires |
|
||||
associated_data |
|
||||
```
|
||||
|
||||
@@ -65,13 +65,13 @@ This produces the following encrypted “ciphertext” using the `aead-det` algo
|
||||
crypto_aead_det_encrypt | \\x099baa820250d7375ed141f8f1936af384bc229f3de1010a6eff6ffdaf3998baffbae75b5cd83d1c469407ff2d3764a428b742
|
||||
```
|
||||
|
||||
Now to decrypt the ciphertext, pass it to the decryption function *with the same Key ID*:
|
||||
Now to decrypt the ciphertext, pass it to the decryption function _with the same Key ID_:
|
||||
|
||||
```sql
|
||||
select *
|
||||
select *
|
||||
from convert_from(pgsodium.crypto_aead_det_decrypt (
|
||||
'\\x099baa820250d7375ed141f8f1936af384bc229f3de1010a6eff6ffdaf3998baffbae75b5cd83d1c469407ff2d3764a428b742',
|
||||
'this is associated data',
|
||||
'\\x099baa820250d7375ed141f8f1936af384bc229f3de1010a6eff6ffdaf3998baffbae75b5cd83d1c469407ff2d3764a428b742',
|
||||
'this is associated data',
|
||||
'eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d'::uuid
|
||||
), 'utf8');
|
||||
```
|
||||
@@ -83,24 +83,24 @@ Which recovers the original “plaintext” message:
|
||||
convert_from | this is the message
|
||||
```
|
||||
|
||||
In the above example, there is *no raw key* like shown in the pgcrypto example above, only a Key ID which is used to derive the key used to encrypt the message and authenticate it with the associated data. In fact, it is impossible for a SQL user to derive the key used above, and if the Key ID is stored, then no decrypted information will leak into backups, disk storage, or the database WAL stream.
|
||||
In the above example, there is _no raw key_, only a Key ID which is used to derive the key used to encrypt the message and authenticate it with the associated data. In fact, it is impossible for a SQL user to derive the key used above, and if the Key ID is stored, then no encryption keys or decrypted information will leak into backups, disk storage, or the database WAL stream.
|
||||
|
||||
## Transparent Column Encryption
|
||||
|
||||
As of **pgsodium** 3.0.0 and up, the extension offers a simple and declarative Transparent Column Encryption feature (TCE). This feature is now shipped with all Supabase projects. TCE allows you to specify encrypted columns within a table and generates a new view that “wraps” that table to decrypt the contents.
|
||||
As of **pgsodium** 3.0.0 and up, the extension offers a simple and declarative Transparent Column Encryption feature (TCE). This feature is now shipped with all Supabase projects. TCE allows you to specify encrypted columns within a table and generates a new view that “wraps” that table to decrypt the contents.
|
||||
|
||||
TCE works using two dynamically generated objects for tables that contain encrypted columns:
|
||||
TCE works using two dynamically generated objects for tables that contain encrypted columns:
|
||||
|
||||
- an `INSERT UPDATE` trigger that encrypts data when it is inserted or modified
|
||||
- a view that is created to wrap the table to decrypt the data when it is accessed
|
||||
|
||||
To “transparently” decrypt the table, access the dynamically generated view *instead of the table*. For every encrypted column in the table, the view will have an additional decrypted column that shows the decrypted result.
|
||||
To “transparently” decrypt the table, access the dynamically generated view _instead of the table_. For every encrypted column in the table, the view will have an additional decrypted column that shows the decrypted result.
|
||||
|
||||
It's worth noting at this point that sometimes there is some confusion about handling encrypted data with TCE. The `T` stands for ***********Transparent*********** which means, you can always see decrypted data through the view, where the decrypted data can't be see is when stored on disk, or in pg_dumps, backups, WAL streams, etc. This is often called ******************Encryption At Rest****************** and is one layer in many that may be used to encrypt and protect your data.
|
||||
It's worth noting at this point that sometimes there is some confusion about handling encrypted data with TCE. The `T` stands for **Transparent** which means, you can always see decrypted data through the view, where the decrypted data can't be see is when stored on disk, or in pg_dumps, backups, WAL streams, etc. This is often called **Encryption At Rest** and is one layer in many that may be used to encrypt and protect your data.
|
||||
|
||||
Often Transparent encryption is understood to be “Transparent Disk Encryption” or “Full Disk Encryption”, this is where a drive is encrypted but reading and writing that drive is decrypted. TCE is similar to this, where data on disk is encrypted, but it is more fine grained, only particular columns are encrypted. The data is also encrypted in the sense that the table stored on disk contains encrypted data, without the view or the key, pg_dumps and backups still contain encrypted data, this is not possible with disk-only encryption.
|
||||
Often Transparent encryption is understood to be “Transparent Disk Encryption” or “Full Disk Encryption”, this is where a drive is encrypted but reading and writing that drive is decrypted. TCE is similar to this, where data on disk is encrypted, but it is more fine grained, only particular columns are encrypted. The data is also encrypted in the sense that the table stored on disk contains encrypted data, without the view or the key, pg_dumps and backups still contain encrypted data, this is not possible with disk-only encryption.
|
||||
|
||||
For the moment TCE only works for columns of type `text` (or types castable to `text` like `json`). Soon we will also support `bytea` and possibly more as use cases and tests get better.
|
||||
For the moment TCE only works for columns of type `text` (or types castable to `text` like `json`). Soon we will also support `bytea` and possibly more as use cases and tests get better.
|
||||
|
||||
TCE uses one of PostgreSQL's lesser-known features: [`SECURITY LABEL`](https://www.postgresql.org/docs/current/sql-security-label.html). A security label can be thought of as a simple label which is attached to an object (a table, column, etc). Each label is scoped to an extension and that extension can provide security features depending on the label.
|
||||
|
||||
@@ -127,7 +127,7 @@ SECURITY LABEL FOR pgsodium
|
||||
IS 'ENCRYPT WITH KEY ID e348034b-3f07-4878-aad6-000511d12826';
|
||||
```
|
||||
|
||||
The advantage of this approach is simplicity - the user creates one key and labels a column with it. The cryptographic algorithm for this approach uses a *nonceless* encryption algorithm called `crypto_aead_det_xchacha20()`. This algorithm is written by the author of libsodium and can be found [here](https://github.com/jedisct1/libsodium-xchacha20-siv).
|
||||
The advantage of this approach is simplicity - the user creates one key and labels a column with it. The cryptographic algorithm for this approach uses a _nonceless_ encryption algorithm called `crypto_aead_det_xchacha20()`. This algorithm is written by the author of libsodium and can be found [here](https://github.com/jedisct1/libsodium-xchacha20-siv).
|
||||
|
||||
Using one key for an entire column means that whoever can decrypt one row can decrypt them all from a database dump. Also changing (rotating) the key means rewriting the whole table.
|
||||
|
||||
@@ -153,11 +153,11 @@ Notice also how there is a `DEFAULT` value for the `key_id`. In a way, this give
|
||||
|
||||
### One Key ID per Row with Nonce Support
|
||||
|
||||
The default cryptographic algorithm for the above approach uses a *nonceless* encryption algorithm called `[crypto_aead_det_xchacha20()](https://github.com/jedisct1/libsodium-xchacha20-siv)`. This algorithm has the advantage that it does not require nonce values, the disadvantage is that duplicate plaintexts will produce duplicate ciphertexts.
|
||||
The default cryptographic algorithm for the above approach uses a _nonceless_ encryption algorithm called `[crypto_aead_det_xchacha20()](https://github.com/jedisct1/libsodium-xchacha20-siv)`. This algorithm has the advantage that it does not require nonce values, the disadvantage is that duplicate plaintexts will produce duplicate ciphertexts.
|
||||
|
||||
Nonces are some extra cryptographic context that is used in many cryptographic algorithms to produce different ciphertexts, even if the plaintexts are the same. The nonce does not have to be secret, but it *does* have to be unique. **pgsodium** comes with a useful function `pgsodium.crypto_aead_det_noncegen()` that will generate a cryptographically secure nonce for you, and in almost all cases it's best to use that function unless you know specifically what you are doing. In password hashing approaches, this is often similar to how a “salt” value is used to deduplicate password hashes.
|
||||
Nonces are some extra cryptographic context that is used in many cryptographic algorithms to produce different ciphertexts, even if the plaintexts are the same. The nonce does not have to be secret, but it _does_ have to be unique. **pgsodium** comes with a useful function `pgsodium.crypto_aead_det_noncegen()` that will generate a cryptographically secure nonce for you, and in almost all cases it's best to use that function unless you know specifically what you are doing. In password hashing approaches, this is often similar to how a “salt” value is used to deduplicate password hashes.
|
||||
|
||||
Duplicate ciphertexts cannot be used to “attack the key”, it can only reveal the duplication. However, duplication is still information. In our examples so far, an attacker might be able to use this information to determine that two accounts share the same credit card number. While not technically breaking the encryption, this still leaks information to an attacker.
|
||||
Duplicate ciphertexts cannot be used to “attack the key”, it can only reveal the duplication. However, duplication is still information. In our examples so far, an attacker might be able to use this information to determine that two accounts share the same credit card number. While not technically breaking the encryption, this still leaks information to an attacker.
|
||||
|
||||
```sql
|
||||
CREATE TABLE credit_cards (
|
||||
@@ -172,15 +172,15 @@ SECURITY LABEL FOR pgsodium
|
||||
IS 'ENCRYPT WITH KEY COLUMN key_id NONCE nonce';
|
||||
```
|
||||
|
||||
This is the most secure form of TCE - there is a unique key ID and a unique nonce per row.
|
||||
This is the most secure form of TCE - there is a unique key ID and a unique nonce per row.
|
||||
|
||||
### One Key ID per Row with Associated Data
|
||||
|
||||
The encryption that is used for TCE is one of a family of functions provided by **libsodium** to do Authenticated Encryption with Associated Data or [AEAD Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption). The “associated” data is plaintext (unencrypted) information that is mixed into the authentication signature of the encrypted data, such that when you authenticate the data, you also know that the associated data is authentic.
|
||||
The encryption that is used for TCE is one of a family of functions provided by **libsodium** to do Authenticated Encryption with Associated Data or [AEAD Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption). The “associated” data is plaintext (unencrypted) information that is mixed into the authentication signature of the encrypted data, such that when you authenticate the data, you also know that the associated data is authentic.
|
||||
|
||||
AEAD is helpful because often you have metadata associated with a secret, which isn't confidential but must not be forged.
|
||||
AEAD is helpful because often you have metadata associated with a secret, which isn't confidential but must not be forged.
|
||||
|
||||
In our credit card example, we might associate a "`credit_card_number`" with an "`account_id`". But what if a malicious actor wanted to use someone else's credit card on their own account? If someone could forge the `account_id` data column, swapping an `account_id` with their own `account_id`, then you could be tricked into using the wrong credit card. By “associating” the `account_id` with the `credit_card_number`, it cannot be forged without throwing an error.
|
||||
In our credit card example, we might associate a "`credit_card_number`" with an "`account_id`". But what if a malicious actor wanted to use someone else's credit card on their own account? If someone could forge the `account_id` data column, swapping an `account_id` with their own `account_id`, then you could be tricked into using the wrong credit card. By “associating” the `account_id` with the `credit_card_number`, it cannot be forged without throwing an error.
|
||||
|
||||
Like above, this is done simply by extending the security label with the associated data column:
|
||||
|
||||
@@ -202,18 +202,18 @@ The new label indicates which column is to be associated with the secret, and th
|
||||
|
||||
## Using an Encrypted Table
|
||||
|
||||
Now that you have TCE setup for a table, it's easy to use by simply inserting data into the table, and querying that data by looking at its generated view. The view is named `decrypted_<table_name>` and by default is in the same schema as your table:
|
||||
Now that you have TCE setup for a table, it's easy to use by simply inserting data into the table, and querying that data by looking at its generated view. The view is named `decrypted_<table_name>` and by default is in the same schema as your table:
|
||||
|
||||
```sql
|
||||
INSERT INTO credit_cards (
|
||||
credit_card_number,
|
||||
account_id
|
||||
)
|
||||
)
|
||||
VALUES
|
||||
('1234-5678-8765-4321', 123);
|
||||
```
|
||||
|
||||
Now that you have inserted data, look at the table and notice how the credit card number is encrypted. This is the data that is stored on disk, the encrypted card number, the key id, and the account id, ********************************but the key itself is not stored********************************. This means if someone gets a backup or dump of your database, they cannot decrypt the credit card number, they do not have the key, only the key ID:
|
||||
Now that you have inserted data, look at the table and notice how the credit card number is encrypted. This is the data that is stored on disk, the encrypted card number, the key id, and the account id, **but the key itself is not stored**. This means if someone gets a backup or dump of your database, they cannot decrypt the credit card number, they do not have the key, only the key ID:
|
||||
|
||||
```sql
|
||||
> select * from credit_cards where account_id = 123;
|
||||
@@ -238,11 +238,11 @@ key_id | 7f753c4f-8c68-457a-8801-1798b2e9f44d
|
||||
nonce | \x300a14aa721184ff7cf0f6bf088da267
|
||||
```
|
||||
|
||||
Notice how there is a new column called `decrypted_credit_card_number`. This column is not stored in database or on disk at all, it is generated “on-the-fly” as you select from the view. Database dumps do not contain this information, only the view itself, and most importantly, *************************************raw decryption keys are never stored*************************************.
|
||||
Notice how there is a new column called `decrypted_credit_card_number`. This column is not stored in database or on disk at all, it is generated “on-the-fly” as you select from the view. Database dumps do not contain this information, only the view itself, and most importantly, **raw decryption keys are never stored**.
|
||||
|
||||
## Future possibilities
|
||||
|
||||
We're always thinking about the future possibilities for features and tools that we can bring to the PostgreSQL community, and we'd love to hear from you about what kind of encryption features you'd like to see. Some things we've considered but not yet explored yet are:
|
||||
We're always thinking about the future possibilities for features and tools that we can bring to the PostgreSQL community, and we'd love to hear from you about what kind of encryption features you'd like to see. Some things we've considered but not yet explored yet are:
|
||||
|
||||
- Built-in Key Management Server (KMS) with REST API ala AWS or GCP.
|
||||
- Seamless Integration with external KMS services for key management.
|
||||
@@ -251,7 +251,7 @@ We're always thinking about the future possibilities for features and tools that
|
||||
- Group encryption using [signcryption](https://github.com/jedisct1/libsodium-signcryption)
|
||||
- Your idea here?
|
||||
|
||||
There's a lot of potential in the world of cryptography with Postgres and pgsodium, and we'd love to hear any ideas you may have as well. Join us in our [Discord #encryption channel](https://discord.com/channels/839993398554656828/1009906326480101417) if you want to chat more about it with us!
|
||||
There's a lot of potential in the world of cryptography with Postgres and pgsodium, and we'd love to hear any ideas you may have as well. Join us in our [Discord #encryption channel](https://discord.com/channels/839993398554656828/1009906326480101417) if you want to chat more about it with us!
|
||||
|
||||
## More Postgres Resources
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{
|
||||
"text": "Careers",
|
||||
"description": "Join the Supabase team and get involved.",
|
||||
"url": "https://about.supabase.com/careers",
|
||||
"url": "https://supabase.com/careers",
|
||||
"icon": "M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
}
|
||||
]
|
||||
@@ -229,7 +229,7 @@
|
||||
"icon": "M15.042 21.672L13.684 16.6m0 0l-2.51 2.225.569-9.47 5.227 7.917-3.286-.672zM12 2.25V4.5m5.834.166l-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243l-1.59-1.59",
|
||||
"features": [
|
||||
{
|
||||
"title": "Change Data Capture (CDC)",
|
||||
"title": "Postgres Changes",
|
||||
"tiers": {
|
||||
"free": true,
|
||||
"pro": true,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
{
|
||||
"icon": "curious",
|
||||
"title": "Polyglot",
|
||||
"text": "Broad skillets. Growth mindset."
|
||||
"text": "Broad skillsets. Growth mindset."
|
||||
},
|
||||
{
|
||||
"icon": "process",
|
||||
|
||||
@@ -1320,8 +1320,8 @@ module.exports = [
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/realtime/postgres-changes',
|
||||
destination: '/docs/guides/realtime/postgres-cdc',
|
||||
source: '/docs/guides/realtime/postgres-cdc',
|
||||
destination: '/docs/guides/realtime/postgres-changes',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
@@ -1565,4 +1565,9 @@ module.exports = [
|
||||
source: '/docs/guides/functions/examples',
|
||||
destination: '/docs/guides/functions',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/projects',
|
||||
destination: 'https://app.supabase.com/projects',
|
||||
},
|
||||
]
|
||||
@@ -103,7 +103,7 @@ const Team = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className=" col-span-4 pt-8 md:mt-0 md:text-right">
|
||||
<a href="https://about.supabase.com/careers">
|
||||
<a href="https://supabase.com/careers">
|
||||
<Button size="medium" className="text-white">
|
||||
Join the team
|
||||
</Button>
|
||||
|
||||
@@ -144,7 +144,7 @@ function RealtimePage() {
|
||||
Listen to changes in the Database inserts, updates, and deletes and other changes.
|
||||
</p>
|
||||
<div className="not-prose mt-3">
|
||||
<Link passHref href="/docs/guides/realtime/postgres-cdc">
|
||||
<Link passHref href="/docs/guides/realtime/postgres-changes">
|
||||
<Button as="a" type="default">
|
||||
View docs
|
||||
</Button>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 175 KiB |
@@ -0,0 +1,3 @@
|
||||
# Upload files to Storage
|
||||
|
||||
This example shows how to process `multipart/form-data` in an Edge Function. Then, upload files in the request to Supabase Storage and insert other fields as a record to a database table.
|
||||
@@ -0,0 +1,66 @@
|
||||
// This example shows how to use Edge Functions to read incoming multipart/form-data request,
|
||||
// and write files to Supabase Storage and other fields to a database table.
|
||||
|
||||
import { Application, Router } from 'https://deno.land/x/oak/mod.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
const MB = 1024 * 1024
|
||||
|
||||
const app = new Application()
|
||||
|
||||
app.use(async (ctx) => {
|
||||
const body = ctx.request.body({ type: 'form-data' })
|
||||
const formData = await body.value.read({
|
||||
// Need to set the maxSize so files will be stored in memory.
|
||||
// This is necessary as Edge Functions don't have disk write access.
|
||||
// We are setting the max size as 10MB (an Edge Function has a max memory limit of 150MB)
|
||||
// For more config options, check: https://deno.land/x/oak@v11.1.0/mod.ts?s=FormDataReadOptions
|
||||
maxSize: 10 * MB,
|
||||
})
|
||||
if (!formData.files || !formData.files.length) {
|
||||
ctx.response.status = 400
|
||||
ctx.response.body = 'missing file'
|
||||
return
|
||||
}
|
||||
|
||||
const supabaseClient = createClient(
|
||||
// Supabase API URL - env var exported by default.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API ANON KEY - env var exported by default.
|
||||
Deno.env.get('SUPABASE_ANON_KEY') ?? ''
|
||||
)
|
||||
|
||||
//upload image to Storage
|
||||
const file = formData.files[0]
|
||||
const timestamp = +new Date()
|
||||
const uploadName = `${file.name}-${timestamp}`
|
||||
const { data: upload, error: uploadError } = await supabaseClient.storage
|
||||
.from('images')
|
||||
.upload(uploadName, file.content!.buffer, {
|
||||
contentType: file.contentType,
|
||||
cacheControl: '3600',
|
||||
upsert: false,
|
||||
})
|
||||
if (uploadError) {
|
||||
console.error(uploadError)
|
||||
ctx.response.status = 500
|
||||
ctx.response.body = 'Failed to upload the file'
|
||||
return
|
||||
}
|
||||
|
||||
// insert record to messages table
|
||||
const { error } = await supabaseClient
|
||||
.from('comments')
|
||||
.insert({ message: formData.fields!.message || '', image_path: upload.path })
|
||||
if (error) {
|
||||
console.error(error)
|
||||
ctx.response.status = 500
|
||||
ctx.response.body = 'Fail to add the record'
|
||||
return
|
||||
}
|
||||
|
||||
ctx.response.status = 201
|
||||
ctx.response.body = 'Success!'
|
||||
})
|
||||
|
||||
await app.listen({ port: 8000 })
|
||||
+41
-17
@@ -27,11 +27,14 @@
|
||||
|
||||
Para documentação completa, visite [supabase.com/docs](https://supabase.com/docs)
|
||||
|
||||
Para ver como Contribuir, visite [Primeiros Passos](./DEVELOPERS.md)
|
||||
|
||||
## Comunidade & Suporte
|
||||
|
||||
- [Fórum da comunidade](https://github.com/supabase/supabase/discussions). Melhor para: ajuda com a construção, discussões sobre melhores práticas em banco de dados.
|
||||
- [GitHub Issues](https://github.com/supabase/supabase/issues). Melhor para: problemas ou erros que você encontrar usando o Supabase.
|
||||
- [Suporte por E-mail](https://supabase.com/docs/support#business-support). Melhor para: problemas com o seu banco de dados ou infraestrutura.
|
||||
- [Discord](https://discord.supabase.com). Melhor para: compartilhar seus aplicativos e se divertir com a comunidade.
|
||||
|
||||
## Status
|
||||
|
||||
@@ -53,6 +56,7 @@ Supabase é uma combinação de ferramentas de código aberto. Nós estamos cons
|
||||
**Arquitetura atual**
|
||||
|
||||
Supabase é uma [plataforma hospedada](https://app.supabase.com). Você pode se cadastrar e começar a usar Supabase sem instalar nada. Nós ainda estamos criando a experiência de trabalho local - esse é o nosso foco principal, junto com a estabilidade da plataforma.
|
||||
Voce tambem pode [auto-hospedar](https://supabase.com/docs/guides/hosting/overview) e [develop locally](https://supabase.com/docs/guides/local-development).
|
||||
|
||||

|
||||
|
||||
@@ -66,7 +70,7 @@ Supabase é uma [plataforma hospedada](https://app.supabase.com). Você pode se
|
||||
|
||||
#### Bibliotecas de cliente
|
||||
|
||||
Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementação independente para cada sistema externo. Esse é uma das formas de apoiar as ferramentas existentes.
|
||||
Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementação independente para cada sistema externo. Essa é uma das formas de apoiar as ferramentas existentes.
|
||||
|
||||
<table style="table-layout:fixed; white-space: nowrap;">
|
||||
<tr>
|
||||
@@ -81,6 +85,7 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<th><a href="https://github.com/supabase/gotrue" target="_blank" rel="noopener noreferrer">GoTrue</a></th>
|
||||
<th><a href="https://github.com/supabase/realtime" target="_blank" rel="noopener noreferrer">Realtime</a></th>
|
||||
<th><a href="https://github.com/supabase/storage-api" target="_blank" rel="noopener noreferrer">Storage</a></th>
|
||||
<th>Funções</th>
|
||||
</tr>
|
||||
<!-- TEMPLATE FOR NEW ROW -->
|
||||
<!-- START ROW
|
||||
@@ -93,7 +98,7 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase-community/storage-lang" target="_blank" rel="noopener noreferrer">storage-lang</a></td>
|
||||
</tr>
|
||||
END ROW -->
|
||||
<th colspan="6">⚡️ Oficial ⚡️</th>
|
||||
<th colspan="7">⚡️ Oficial ⚡️</th>
|
||||
<tr>
|
||||
<td>JavaScript (TypeScript)</td>
|
||||
<td><a href="https://github.com/supabase/supabase-js" target="_blank" rel="noopener noreferrer">supabase-js</a></td>
|
||||
@@ -101,8 +106,18 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase/gotrue-js" target="_blank" rel="noopener noreferrer">gotrue-js</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-js" target="_blank" rel="noopener noreferrer">realtime-js</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-js" target="_blank" rel="noopener noreferrer">storage-js</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-js" target="_blank" rel="noopener noreferrer">functions-js</a></td>
|
||||
</tr>
|
||||
<th colspan="6">💚 Comunidade 💚</th>
|
||||
<tr>
|
||||
<td>Flutter</td>
|
||||
<td><a href="https://github.com/supabase/supabase-flutter" target="_blank" rel="noopener noreferrer">supabase-flutter</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-dart" target="_blank" rel="noopener noreferrer">realtime-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
|
||||
</tr>
|
||||
<th colspan="7">💚 Comunidade 💚</th>
|
||||
<tr>
|
||||
<td>C#</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-csharp" target="_blank" rel="noopener noreferrer">supabase-csharp</a></td>
|
||||
@@ -110,22 +125,16 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase-community/gotrue-csharp" target="_blank" rel="noopener noreferrer">gotrue-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-csharp" target="_blank" rel="noopener noreferrer">realtime-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-csharp" target="_blank" rel="noopener noreferrer">storage-csharp</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Flutter</td>
|
||||
<td><a href="https://github.com/supabase/supabase-flutter" target="_blank" rel="noopener noreferrer">supabase-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-lang</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-dart" target="_blank" rel="noopener noreferrer">realtime-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-csharp" target="_blank" rel="noopener noreferrer">functions-csharp</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Go</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-go" target="_blank" rel="noopener noreferrer">postgrest-go</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-go" target="_blank" rel="noopener noreferrer">gotrue-go</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/storage-go" target="_blank" rel="noopener noreferrer">storage-go</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-go" target="_blank" rel="noopener noreferrer">functions-go</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Java</td>
|
||||
@@ -134,14 +143,16 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase-community/gotrue-java" target="_blank" rel="noopener noreferrer">gotrue-java</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kotlin</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt" target="_blank" rel="noopener noreferrer">supabase-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-kt" target="_blank" rel="noopener noreferrer">postgrest-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-kt" target="_blank" rel="noopener noreferrer">gotrue-kt</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Realtime" target="_blank" rel="noopener noreferrer">realtime-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Storage" target="_blank" rel="noopener noreferrer">storage-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Functions" target="_blank" rel="noopener noreferrer">functions-kt</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Python</td>
|
||||
@@ -149,7 +160,8 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase-community/postgrest-py" target="_blank" rel="noopener noreferrer">postgrest-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-py" target="_blank" rel="noopener noreferrer">gotrue-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-py" target="_blank" rel="noopener noreferrer">realtime-py</a></td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/storage-py" target="_blank" rel="noopener noreferrer">storage-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-py" target="_blank" rel="noopener noreferrer">functions-py</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ruby</td>
|
||||
@@ -158,6 +170,7 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rust</td>
|
||||
@@ -166,6 +179,7 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swift</td>
|
||||
@@ -174,6 +188,16 @@ Nossa biblioteca de cliente é modular. Cada sub-biblioteca é uma implementaç
|
||||
<td><a href="https://github.com/supabase-community/gotrue-swift" target="_blank" rel="noopener noreferrer">gotrue-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-swift" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-swift" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-swift" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Godot Engine (GDScript)</td>
|
||||
<td><a href="https://github.com/supabase-community/godot-engine.supabase" target="_blank" rel="noopener noreferrer">supabase-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-gdscript" target="_blank" rel="noopener noreferrer">postgrest-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-gdscript" target="_blank" rel="noopener noreferrer">gotrue-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-gdscript" target="_blank" rel="noopener noreferrer">realtime-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-gdscript" target="_blank" rel="noopener noreferrer">storage-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-gdscript" target="_blank" rel="noopener noreferrer">functions-gdscript</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ const RowHeader: FC<RowHeaderProps> = ({ sorts, filters }) => {
|
||||
} else {
|
||||
const rowIdxs = Array.from(selectedRows) as number[]
|
||||
const rows = allRows.filter((x) => rowIdxs.includes(x.idx))
|
||||
const { error } = state.rowService!.delete(rows)
|
||||
const { error } = await state.rowService!.delete(rows)
|
||||
if (error) {
|
||||
if (state.onError) state.onError(error)
|
||||
} else {
|
||||
|
||||
@@ -17,13 +17,13 @@ const RowContextMenu: React.FC<RowContextMenuProps> = ({}) => {
|
||||
confirmAlert({
|
||||
title: 'Confirm to delete',
|
||||
message: 'Are you sure you want to delete this row? This action cannot be undone.',
|
||||
onConfirm: async () => {
|
||||
onAsyncConfirm: async () => {
|
||||
const { props } = p
|
||||
const { rowIdx } = props
|
||||
const row = state.rows[rowIdx]
|
||||
if (!row) return
|
||||
|
||||
const { error } = state.rowService!.delete([row])
|
||||
const { error } = await state.rowService!.delete([row])
|
||||
if (error) {
|
||||
if (state.onError) state.onError(error)
|
||||
} else {
|
||||
|
||||
@@ -37,11 +37,7 @@ export class SqlRowService implements IRowService {
|
||||
}
|
||||
}
|
||||
|
||||
async create(row: SupaRow) {
|
||||
return { error: { message: 'not implemented' } }
|
||||
}
|
||||
|
||||
delete(rows: SupaRow[]) {
|
||||
async delete(rows: SupaRow[]) {
|
||||
const { primaryKeys, error } = this.getPrimaryKeys()
|
||||
if (error) return { error }
|
||||
|
||||
@@ -53,14 +49,7 @@ export class SqlRowService implements IRowService {
|
||||
})
|
||||
|
||||
const query = queryChains.toSql()
|
||||
SupabaseGridQueue.add(async () => {
|
||||
const { error } = await this.onSqlQuery(query)
|
||||
if (error) throw error
|
||||
}).catch((error) => {
|
||||
this.onError(error)
|
||||
})
|
||||
|
||||
return {}
|
||||
return await this.onSqlQuery(query)
|
||||
}
|
||||
|
||||
// For deleting all rows based on a given filter
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Filter, ServiceError, Sort, SupaRow } from '../../types'
|
||||
export interface IRowService {
|
||||
count: (filters: Filter[]) => Promise<{ data?: number; error?: ServiceError }>
|
||||
|
||||
create: (row: SupaRow) => Promise<{ data?: SupaRow; error?: ServiceError }>
|
||||
|
||||
fetchPage: (
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
@@ -25,7 +23,7 @@ export interface IRowService {
|
||||
* Right now, we remove rows immediately, so it's confused when delete operation fails
|
||||
* and rows are already removed from the grid
|
||||
*/
|
||||
delete: (rows: SupaRow[]) => { error?: ServiceError }
|
||||
delete: (rows: SupaRow[]) => Promise<{ error?: ServiceError }>
|
||||
deleteAll: (filters: Filter[]) => Promise<{ error?: ServiceError }>
|
||||
|
||||
update: (
|
||||
|
||||
@@ -32,14 +32,16 @@ const PortalToast = () => (
|
||||
{({ icon, message }) => (
|
||||
<>
|
||||
{icon}
|
||||
{message}
|
||||
{t.type !== 'loading' && (
|
||||
<div className="ml-4">
|
||||
<Button className="!p-1" type="text" onClick={() => toast.dismiss(t.id)}>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<div className="toast-message w-full">{message}</div>
|
||||
{t.type !== 'loading' && (
|
||||
<div className="ml-4">
|
||||
<Button className="!p-1" type="text" onClick={() => toast.dismiss(t.id)}>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ToastBar>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { DatabaseAddon } from './AddOns.types'
|
||||
|
||||
// e.g supabase-postgres-14.1.0.88 (There's 4 segments instead, so we can't use semver)
|
||||
export const getSemanticVersion = (version: string) => {
|
||||
if (!version) return 0
|
||||
|
||||
const segments = version.split('-')
|
||||
// e.g supabase-postgres-14.1.0.88
|
||||
// There's 4 segments instead so we can't use the semver package
|
||||
const segments = version.split('supabase-postgres-')
|
||||
const semver = segments[segments.length - 1]
|
||||
return Number(semver.split('.').join(''))
|
||||
|
||||
// e.g supabase-postgres-14.1.0.99-vault-rc1
|
||||
const formattedSemver = semver.split('-')[0]
|
||||
|
||||
return Number(formattedSemver.split('.').join(''))
|
||||
}
|
||||
|
||||
export const formatComputeSizes = (addons: DatabaseAddon[]) => {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useStore } from 'hooks'
|
||||
import { post, patch } from 'lib/common/fetch'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import { CANCELLATION_REASONS } from '../Billing.constants'
|
||||
import { generateFeedbackMessage } from './ExitSurvey.utils'
|
||||
import { UpdateSuccess } from '../'
|
||||
import { SubscriptionPreview } from '../Billing.types'
|
||||
import { StripeSubscription } from 'components/interfaces/Billing'
|
||||
@@ -138,14 +137,11 @@ const ExitSurvey: FC<Props> = ({ freeTier, subscription, onSelectBack }) => {
|
||||
setIsSuccessful(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Submit exit survey to Hubspot
|
||||
const feedbackRes = await post(`${API_URL}/feedback/send`, {
|
||||
const feedbackRes = await post(`${API_URL}/feedback/downgrade`, {
|
||||
projectRef,
|
||||
subject: 'Subscription cancellation - Exit survey [Downgrade]',
|
||||
tags: ['dashboard-exitsurvey'],
|
||||
category: 'Billing',
|
||||
message: generateFeedbackMessage(selectedReasons, downgradeMessage),
|
||||
reasons: selectedReasons.reduce((a, b) => `${a}- ${b}\n`, ''),
|
||||
additionalFeedback: downgradeMessage,
|
||||
exitAction: 'downgrade',
|
||||
})
|
||||
if (feedbackRes.error) throw feedbackRes.error
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export const generateFeedbackMessage = (reasons: string[], message: string) => {
|
||||
return `
|
||||
Exit survey \n
|
||||
|
||||
Reasons for leaving:
|
||||
${reasons.reduce((a, b) => `${a}- ${b}\n`, '')}
|
||||
|
||||
Additional feedback:
|
||||
${message}
|
||||
`
|
||||
}
|
||||
@@ -71,12 +71,11 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
}) => {
|
||||
const { ui } = useStore()
|
||||
const projectRegion = ui.selectedProject?.region
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const canUpdatePaymentMethods = enablePermissions
|
||||
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.payment_methods')
|
||||
: isOwner
|
||||
const canUpdatePaymentMethods = checkPermissions(
|
||||
PermissionAction.BILLING_WRITE,
|
||||
'stripe.payment_methods'
|
||||
)
|
||||
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||
|
||||
@@ -311,9 +310,8 @@ const PaymentSummaryPanel: FC<Props> = ({
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
{enablePermissions
|
||||
? 'You need additional permissions to add new payment methods to this organization'
|
||||
: 'Only organization owners can add new payment methods'}
|
||||
You need additional permissions to add new payment methods to this
|
||||
organization
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
|
||||
@@ -36,13 +36,13 @@ const Subscription: FC<Props> = ({
|
||||
const router = useRouter()
|
||||
|
||||
const { ref } = router.query
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
|
||||
const canReadSubscription = checkPermissions(PermissionAction.READ, 'subscriptions')
|
||||
const canUpdateSubscription = enablePermissions
|
||||
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.subscriptions')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
const canUpdateSubscription = checkPermissions(
|
||||
PermissionAction.BILLING_WRITE,
|
||||
'stripe.subscriptions'
|
||||
)
|
||||
|
||||
const { usage, isLoading: loadingUsage } = useProjectUsage(ref as string)
|
||||
|
||||
@@ -112,10 +112,8 @@ const Subscription: FC<Props> = ({
|
||||
<br />
|
||||
Our engineers are working on a fix.
|
||||
</>
|
||||
) : !canUpdateSubscription && enablePermissions ? (
|
||||
) : !canUpdateSubscription ? (
|
||||
'You need additional permissions to amend subscriptions'
|
||||
) : !canUpdateSubscription && !enablePermissions ? (
|
||||
'Only the organization owner can amend subscriptions'
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
@@ -51,9 +51,9 @@ const ProjectUsage: FC<Props> = ({}) => {
|
||||
if (timestampDigits < 16) {
|
||||
// pad unix timestamp with additional 0 and then forward
|
||||
const paddedTimestamp = String(timestamp) + '0'.repeat(16 - timestampDigits)
|
||||
router.push(`/project/${ref}/database/api-logs?te=${paddedTimestamp}`)
|
||||
router.push(`/project/${ref}/logs/edge-logs?te=${paddedTimestamp}`)
|
||||
} else {
|
||||
router.push(`/project/${ref}/database/api-logs?te=${timestamp}`)
|
||||
router.push(`/project/${ref}/logs/edge-logs?te=${timestamp}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@ import { Dictionary } from 'components/grid'
|
||||
import { Form, Input, Button, Listbox } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { checkPermissions, useStore, useFlag } from 'hooks'
|
||||
import { checkPermissions, useStore } from 'hooks'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { COUNTRIES } from './BillingAddress.constants'
|
||||
@@ -22,11 +22,11 @@ const BillingAddress: FC<Props> = ({ loading, address, onAddressUpdated }) => {
|
||||
const orgSlug = ui.selectedOrganization?.slug ?? ''
|
||||
const { city, country, line1, line2, postal_code, state } = address
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const canReadBillingAddress = checkPermissions(PermissionAction.BILLING_READ, 'stripe.customer')
|
||||
const canUpdateBillingAddress = enablePermissions
|
||||
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.customer')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
const canUpdateBillingAddress = checkPermissions(
|
||||
PermissionAction.BILLING_WRITE,
|
||||
'stripe.customer'
|
||||
)
|
||||
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const initialValues = {
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { useContext, useEffect } from 'react'
|
||||
import { Form, Input } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useFlag, useStore, checkPermissions } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
|
||||
import {
|
||||
FormActions,
|
||||
FormPanel,
|
||||
FormSection,
|
||||
FormSectionContent,
|
||||
FormSectionLabel,
|
||||
} from 'components/ui/Forms'
|
||||
import { FormActions, FormPanel, FormSection, FormSectionContent } from 'components/ui/Forms'
|
||||
|
||||
import { PageContext } from 'pages/org/[slug]/settings'
|
||||
|
||||
@@ -24,12 +18,7 @@ const BillingEmail = () => {
|
||||
billing_email: PageState.organization?.billing_email ?? '',
|
||||
}
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const canUpdateOrganization = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
|
||||
const canUpdateOrganization = checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
const canReadBillingEmail = checkPermissions(PermissionAction.READ, 'organizations')
|
||||
|
||||
const onUpdateOrganization = async (values: any, { setSubmitting, resetForm }: any) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
|
||||
import { useFlag, useStore, checkPermissions } from 'hooks'
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { get } from 'lib/common/fetch'
|
||||
|
||||
@@ -9,7 +9,6 @@ import CreditBalance from './CreditBalance'
|
||||
import PaymentMethods from './PaymentMethods'
|
||||
import BillingAddress from './BillingAddress/BillingAddress'
|
||||
import TaxID from './TaxID/TaxID'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import BillingEmail from './BillingEmail'
|
||||
|
||||
interface Props {
|
||||
@@ -39,14 +38,6 @@ const BillingSettings: FC<Props> = ({ organization, projects = [] }) => {
|
||||
? customerBalance.toString().replace('-', '')
|
||||
: customerBalance
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const canUpdateOrganization = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
|
||||
const canReadBillingEmail = checkPermissions(PermissionAction.READ, 'organizations')
|
||||
|
||||
useEffect(() => {
|
||||
getCustomerProfile()
|
||||
getPaymentMethods()
|
||||
|
||||
@@ -46,15 +46,14 @@ const PaymentMethods: FC<Props> = ({
|
||||
const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState(false)
|
||||
const [isUpdatingPaymentMethod, setIsUpdatingPaymentMethod] = useState(false)
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const canReadPaymentMethods = checkPermissions(
|
||||
PermissionAction.BILLING_READ,
|
||||
'stripe.payment_methods'
|
||||
)
|
||||
const canUpdatePaymentMethods = enablePermissions
|
||||
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.payment_methods')
|
||||
: isOwner
|
||||
const canUpdatePaymentMethods = checkPermissions(
|
||||
PermissionAction.BILLING_WRITE,
|
||||
'stripe.payment_methods'
|
||||
)
|
||||
|
||||
const onConfirmMakeDefaultPaymentMethod = async () => {
|
||||
try {
|
||||
@@ -120,9 +119,7 @@ const PaymentMethods: FC<Props> = ({
|
||||
<div className="flex w-full justify-between">
|
||||
{!canUpdatePaymentMethods ? (
|
||||
<p className="text-sm text-scale-1000">
|
||||
{enablePermissions
|
||||
? "You need additional permissions to manage this organization's payment methods"
|
||||
: 'Only organization owners can update payment methods'}
|
||||
You need additional permissions to manage this organization's payment methods
|
||||
</p>
|
||||
) : (
|
||||
<div />
|
||||
|
||||
@@ -50,12 +50,8 @@ const TaxID: FC<Props> = ({ loading, taxIds, onTaxIdsUpdated }) => {
|
||||
}, [taxIds])
|
||||
|
||||
const hasChanges = !isEqual(taxIdValues, formattedTaxIds)
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const canReadTaxIds = checkPermissions(PermissionAction.BILLING_READ, 'stripe.tax_ids')
|
||||
const canUpdateTaxIds = enablePermissions
|
||||
? checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.tax_ids')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
const canUpdateTaxIds = checkPermissions(PermissionAction.BILLING_WRITE, 'stripe.tax_ids')
|
||||
|
||||
const onUpdateTaxId = (id: string, key: string, value: string) => {
|
||||
const updatedTaxIds = taxIdValues.map((taxId: any) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { Form, Input } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useFlag, useStore, checkPermissions } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
|
||||
@@ -24,16 +24,8 @@ const GeneralSettings = observer(() => {
|
||||
|
||||
const formId = 'org-general-settings'
|
||||
const initialValues = { name: PageState.organization.name }
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const canUpdateOrganization = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
|
||||
const canDeleteOrganization = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
const canUpdateOrganization = checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
const canDeleteOrganization = checkPermissions(PermissionAction.UPDATE, 'organizations')
|
||||
|
||||
const onUpdateOrganization = async (values: any, { setSubmitting, resetForm }: any) => {
|
||||
if (!canUpdateOrganization) {
|
||||
|
||||
+1
-7
@@ -1,12 +1,8 @@
|
||||
import { IconAlertCircle } from 'ui'
|
||||
|
||||
import { useFlag } from 'hooks'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
|
||||
// [Joshen] This can just use NoPermission component i think
|
||||
const NotOrganizationOwnerWarning = () => {
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<InformationBox
|
||||
@@ -17,9 +13,7 @@ const NotOrganizationOwnerWarning = () => {
|
||||
description={
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm leading-normal">
|
||||
{enablePermissions
|
||||
? 'Contact your organization owner or adminstrator to create a new project.'
|
||||
: 'Only the organization owner can create new projects. Contact your organization owner to create a new project for this organization.'}
|
||||
Contact your organization owner or adminstrator to create a new project.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, Form, IconMail, Input, Modal, Select } from 'ui'
|
||||
|
||||
import { Member, User, Role } from 'types'
|
||||
import { checkPermissions, useFlag, useOrganizationDetail, useStore } from 'hooks'
|
||||
import { checkPermissions, useOrganizationDetail, useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
@@ -23,8 +23,6 @@ const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAd
|
||||
const router = useRouter()
|
||||
const { slug } = router.query
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { mutateOrgMembers } = useOrganizationDetail((slug as string) || '')
|
||||
|
||||
@@ -57,16 +55,14 @@ const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAd
|
||||
}
|
||||
}
|
||||
|
||||
const roleId = enablePermissions
|
||||
? Number(values.role)
|
||||
: roles.find((role) => role.name === 'Developer')?.id ?? roles[0].id
|
||||
const roleId = Number(values.role)
|
||||
|
||||
setSubmitting(true)
|
||||
|
||||
const response = await post(`${API_URL}/organizations/${slug}/members/invite`, {
|
||||
invited_email: values.email.toLowerCase(),
|
||||
owner_id: user.id,
|
||||
...(enablePermissions ? { role_id: roleId } : {}),
|
||||
role_id: roleId,
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
@@ -141,9 +137,7 @@ const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAd
|
||||
}, [roles])
|
||||
|
||||
const selectedRole = roles.find((role) => role.id === Number(values.role))
|
||||
const invalidRoleSelected = enablePermissions
|
||||
? values.role && !rolesAddable.includes(Number(values.role))
|
||||
: false
|
||||
const invalidRoleSelected = values.role && !rolesAddable.includes(Number(values.role))
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -151,7 +145,7 @@ const InviteMemberButton: FC<Props> = ({ user, members = [], roles = [], rolesAd
|
||||
<div className="w-full py-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
{roles && enablePermissions && (
|
||||
{roles && (
|
||||
<Select
|
||||
name="role"
|
||||
label="Member role"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Button, Dropdown, IconTrash, IconMoreHorizontal } from 'ui'
|
||||
|
||||
import { Member, Role } from 'types'
|
||||
import { useStore, useOrganizationDetail, useFlag, checkPermissions } from 'hooks'
|
||||
import { useStore, useOrganizationDetail, checkPermissions } from 'hooks'
|
||||
import { delete_, post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
@@ -27,7 +27,6 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
const { rolesRemovable } = getRolesManagementPermissions(roles)
|
||||
|
||||
const { app, ui } = useStore()
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const { mutateOrgMembers } = useOrganizationDetail(ui.selectedOrganization?.slug || '')
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -37,9 +36,7 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
const isPendingInviteAcceptance = member.invited_id
|
||||
|
||||
const roleId = member.role_ids?.[0] ?? -1
|
||||
const canRemoveMember = enablePermissions
|
||||
? rolesRemovable.includes((member?.role_ids ?? [-1])[0])
|
||||
: true
|
||||
const canRemoveMember = rolesRemovable.includes((member?.role_ids ?? [-1])[0])
|
||||
const canResendInvite = checkPermissions(PermissionAction.CREATE, 'user_invites', {
|
||||
resource: { role_id: roleId },
|
||||
})
|
||||
@@ -54,11 +51,9 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
onAsyncConfirm: async () => {
|
||||
setLoading(true)
|
||||
|
||||
const response = enablePermissions
|
||||
? await delete_(`${API_URL}/organizations/${slug}/members/${member.gotrue_id}`)
|
||||
: await delete_(`${API_URL}/organizations/${slug}/members/remove`, {
|
||||
member_id: member.id,
|
||||
})
|
||||
const response = await delete_(
|
||||
`${API_URL}/organizations/${slug}/members/${member.gotrue_id}`
|
||||
)
|
||||
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
@@ -67,9 +62,7 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
})
|
||||
setLoading(false)
|
||||
} else {
|
||||
const updatedMembers = enablePermissions
|
||||
? members.filter((m) => m.gotrue_id !== member.gotrue_id)
|
||||
: members.filter((m) => m.id !== member.id)
|
||||
const updatedMembers = members.filter((m) => m.gotrue_id !== member.gotrue_id)
|
||||
|
||||
mutateOrgMembers(updatedMembers)
|
||||
ui.setNotification({
|
||||
@@ -117,7 +110,7 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
const response = await post(`${API_URL}/organizations/${slug}/members/invite`, {
|
||||
invited_email: member.primary_email,
|
||||
owner_id: member.invited_id,
|
||||
...(enablePermissions ? { role_id: roleId } : {}),
|
||||
role_id: roleId,
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
@@ -189,7 +182,7 @@ const MemberActions: FC<Props> = ({ members, member, roles }) => {
|
||||
align="end"
|
||||
overlay={
|
||||
<>
|
||||
{!enablePermissions && !isPendingInviteAcceptance && (
|
||||
{!isPendingInviteAcceptance && (
|
||||
<>
|
||||
<Dropdown.Item onClick={() => setOwnerTransferIsVisible(!ownerTransferIsVisible)}>
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { Badge, Button, Loading, Listbox, IconUser, Modal } from 'ui'
|
||||
|
||||
import { Member, Role } from 'types'
|
||||
import { useStore, useFlag, useOrganizationDetail } from 'hooks'
|
||||
import { useStore, useOrganizationDetail } from 'hooks'
|
||||
import { patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { isInviteExpired, getUserDisplayName } from '../Organization.utils'
|
||||
@@ -28,17 +28,13 @@ const MembersView = () => {
|
||||
|
||||
const { ui } = useStore()
|
||||
const slug = ui.selectedOrganization?.slug || ''
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const { mutateOrgMembers } = useOrganizationDetail(slug)
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedMember, setSelectedMember] = useState<SelectedMember>()
|
||||
const [userRoleChangeModalVisible, setUserRoleChangeModalVisible] = useState(false)
|
||||
|
||||
const hasAccessToMemberActions = enablePermissions ? true : isOwner
|
||||
|
||||
const getRoleNameById = (id: number | undefined) => {
|
||||
if (!roles) return id
|
||||
return roles.find((x: any) => x.id === id)?.name
|
||||
@@ -87,7 +83,7 @@ const MembersView = () => {
|
||||
<Table.th key="header-status"></Table.th>,
|
||||
<Table.th key="header-role" className="flex items-center space-x-2">
|
||||
<span>Role</span>
|
||||
{enablePermissions && <RolesHelperModal />}
|
||||
<RolesHelperModal />
|
||||
</Table.th>,
|
||||
<Table.th key="header-action"></Table.th>,
|
||||
]}
|
||||
@@ -157,71 +153,64 @@ const MembersView = () => {
|
||||
</Table.td>
|
||||
|
||||
<Table.td>
|
||||
{!role && !enablePermissions && <p>{x.is_owner ? 'Owner' : 'Developer'}</p>}
|
||||
{!role && <p>{x.is_owner ? 'Owner' : 'Developer'}</p>}
|
||||
{role && (
|
||||
<>
|
||||
{!enablePermissions ? (
|
||||
<p>{role?.name ?? 'Developer'}</p>
|
||||
) : (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Listbox
|
||||
className={disableRoleEdit ? 'pointer-events-none' : ''}
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Listbox
|
||||
className={disableRoleEdit ? 'pointer-events-none' : ''}
|
||||
disabled={disableRoleEdit}
|
||||
value={role.id}
|
||||
onChange={validateSelectedRoleToChange}
|
||||
>
|
||||
{roles.map((r: any) => (
|
||||
<Listbox.Option
|
||||
key={r.id}
|
||||
value={r.id}
|
||||
label={r.name}
|
||||
disabled={disableRoleEdit}
|
||||
value={role.id}
|
||||
onChange={validateSelectedRoleToChange}
|
||||
>
|
||||
{roles.map((r: any) => (
|
||||
<Listbox.Option
|
||||
key={r.id}
|
||||
value={r.id}
|
||||
label={r.name}
|
||||
disabled={disableRoleEdit}
|
||||
>
|
||||
{r.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
</Tooltip.Trigger>
|
||||
{memberIsPendingInvite ? (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
Role can only be changed after the user has accepted the
|
||||
invite
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
) : !memberIsUser && !canRemoveRole ? (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to manage this team member
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
{r.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
</Tooltip.Trigger>
|
||||
{memberIsPendingInvite ? (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
Role can only be changed after the user has accepted the invite
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
) : !memberIsUser && !canRemoveRole ? (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
||||
'border border-scale-200 ', //border
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to manage this team member
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
{hasAccessToMemberActions && !memberIsUser && (
|
||||
{!memberIsUser && (
|
||||
<MemberActions members={PageState.members} member={x} roles={roles} />
|
||||
)}
|
||||
</Table.td>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useContext } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Input, IconSearch } from 'ui'
|
||||
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import InviteMemberButton from './InviteMemberButton'
|
||||
@@ -21,10 +21,8 @@ const TeamSettings = observer(() => {
|
||||
const slug = ui.selectedOrganization?.slug ?? ''
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const [isLeaving, setIsLeaving] = useState(false)
|
||||
|
||||
const canAddMembers = enablePermissions ? rolesAddable.length > 0 : isOwner
|
||||
const canAddMembers = rolesAddable.length > 0
|
||||
|
||||
function onFilterMemberChange(e: any) {
|
||||
PageState.membersFilterString = e.target.value
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ const CustomDomainConfig = () => {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<FormHeader title="Custom Domains" description="" />
|
||||
<FormHeader title="Custom Domains" description="Present a branded experience to your users" />
|
||||
{data?.status === '0_no_hostname_configured' ? (
|
||||
<CustomDomainsConfigureHostname projectRef={ref} />
|
||||
) : (
|
||||
|
||||
+23
-3
@@ -83,9 +83,29 @@ const CustomDomainVerify = ({ projectRef, customDomain, settings }: CustomDomain
|
||||
: 'Please note that it may take up to 24 hours for the DNS records to propagate.'
|
||||
}
|
||||
description={
|
||||
isNotVerifiedYet
|
||||
? 'Do check again in a bit as it may take up to 24 hours for changes in DNS records to propagate.'
|
||||
: undefined
|
||||
isNotVerifiedYet ? (
|
||||
<div className="space-y-1">
|
||||
<p>
|
||||
Do check again in a bit as it may take up to 24 hours for changes in DNS
|
||||
records to propagate.
|
||||
</p>
|
||||
<p>
|
||||
You may also visit{' '}
|
||||
<Link href={`https://whatsmydns.net/#TXT/${customDomain.hostname}`}>
|
||||
<a className="text-brand-900">here</a>
|
||||
</Link>{' '}
|
||||
to check if your DNS has been propagated successfully before clicking verify.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p>
|
||||
You may also visit{' '}
|
||||
<Link href={`https://whatsmydns.net/#TXT/${customDomain.hostname}`}>
|
||||
<a className="text-brand-900">here</a>
|
||||
</Link>{' '}
|
||||
to check if your DNS has been propagated successfully before clicking verify.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ const CustomDomainsConfigureHostname = ({ projectRef }: CustomDomainsConfigureHo
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FormSection header={<FormSectionLabel>Add a Custom Domain</FormSectionLabel>}>
|
||||
<FormSection header={<FormSectionLabel>Add a custom domain</FormSectionLabel>}>
|
||||
<FormSectionContent loading={false}>
|
||||
<Input
|
||||
id="domain"
|
||||
|
||||
+6
-13
@@ -4,12 +4,11 @@ import { Button, Input } from 'ui'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useStore, checkPermissions, useFlag } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import { delete_, post } from 'lib/common/fetch'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
import { CANCELLATION_REASONS } from 'components/interfaces/Billing/Billing.constants'
|
||||
import { generateFeedbackMessage } from 'components/interfaces/Billing/ExitSurvey/ExitSurvey.utils'
|
||||
|
||||
interface Props {
|
||||
type?: 'danger' | 'default'
|
||||
@@ -18,13 +17,10 @@ interface Props {
|
||||
const DeleteProjectButton: FC<Props> = ({ type = 'danger' }) => {
|
||||
const router = useRouter()
|
||||
const { app, ui } = useStore()
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
const project = ui.selectedProject
|
||||
const projectRef = project?.ref
|
||||
const projectTier = project?.subscription_tier ?? PRICING_TIER_PRODUCT_IDS.FREE
|
||||
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
const isFree = projectTier === PRICING_TIER_PRODUCT_IDS.FREE
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
@@ -39,9 +35,7 @@ const DeleteProjectButton: FC<Props> = ({ type = 'danger' }) => {
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const canDeleteProject = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
: isOwner
|
||||
const canDeleteProject = checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
|
||||
const toggle = () => {
|
||||
if (loading) return
|
||||
@@ -85,12 +79,11 @@ const DeleteProjectButton: FC<Props> = ({ type = 'danger' }) => {
|
||||
|
||||
// Submit exit survey to Hubspot for paid projects
|
||||
if (!isFree) {
|
||||
const feedbackRes = await post(`${API_URL}/feedback/send`, {
|
||||
const feedbackRes = await post(`${API_URL}/feedback/downgrade`, {
|
||||
projectRef,
|
||||
subject: 'Subscription cancellation - Exit survey [Delete]',
|
||||
tags: ['dashboard-exitsurvey'],
|
||||
category: 'Billing',
|
||||
message: generateFeedbackMessage(selectedReasons, cancellationMessage),
|
||||
reasons: selectedReasons.reduce((a, b) => `${a}- ${b}\n`, ''),
|
||||
additionalFeedback: cancellationMessage,
|
||||
exitAction: 'delete',
|
||||
})
|
||||
if (feedbackRes.error) throw feedbackRes.error
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC } from 'react'
|
||||
import { Form, Input } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useStore, checkPermissions, useFlag } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import {
|
||||
@@ -20,16 +20,11 @@ const General: FC<Props> = ({}) => {
|
||||
const { app, ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const formId = 'project-general-settings'
|
||||
const initialValues = { name: project?.name ?? '', ref: project?.ref ?? '' }
|
||||
const canUpdateProject = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
: isOwner
|
||||
const canUpdateProject = checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
|
||||
const onSubmit = async (values: any, { setSubmitting, resetForm }: any) => {
|
||||
const onSubmit = async (values: any, { resetForm }: any) => {
|
||||
const response = await post(`${API_URL}/projects/${project?.ref}/update`, { name: values.name })
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, IconPause } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { checkPermissions, useStore, useFlag } from 'hooks'
|
||||
import { checkPermissions, useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
@@ -21,15 +21,13 @@ const PauseProjectButton: FC<Props> = observer(({ projectRef, projectId }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
|
||||
const openModal = () => setIsModalOpen(true)
|
||||
const closeModal = () => setIsModalOpen(false)
|
||||
|
||||
const canPauseProject = enablePermissions
|
||||
? checkPermissions(PermissionAction.INFRA_EXECUTE, 'queue_jobs.projects.pause')
|
||||
: isOwner
|
||||
const canPauseProject = checkPermissions(
|
||||
PermissionAction.INFRA_EXECUTE,
|
||||
'queue_jobs.projects.pause'
|
||||
)
|
||||
|
||||
const requestPauseProject = async () => {
|
||||
if (!canPauseProject) {
|
||||
|
||||
@@ -180,7 +180,9 @@ export const maybeShowUpgradePrompt = (
|
||||
return (day > 1 && tierKey === 'FREE') || (day > 7 && tierKey === 'PRO') || day > 90 && tierKey === 'ENTERPRISE'
|
||||
}
|
||||
|
||||
export const genCountQuery = (table: string): string => `SELECT count(*) as count FROM ${table}`
|
||||
export const genCountQuery = (table: LogsTableName, filters: Filters): string => {
|
||||
const where = _genWhereStatement(table, filters)
|
||||
return `SELECT count(*) as count FROM ${table} ${where}`}
|
||||
|
||||
/** calculates how much the chart start datetime should be offset given the current datetime filter params */
|
||||
export const calcChartStart = (params: Partial<LogsEndpointParams>): [Dayjs, string] => {
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Tabs } from 'ui'
|
||||
import { useFlag } from 'hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
interface TabItem {
|
||||
label: string
|
||||
id: string
|
||||
}
|
||||
const LogsNavigation = () => {
|
||||
const router = useRouter()
|
||||
const activeRoute = router.pathname.split('/')[5]
|
||||
const { ref } = router.query
|
||||
|
||||
const indexRoute = 'query'
|
||||
|
||||
const tabs = [
|
||||
{ id: 'query', label: 'Query' },
|
||||
{ id: 'saved', label: 'Saved' },
|
||||
{ id: 'recent', label: 'Recent' },
|
||||
{ id: 'templates', label: 'Templates' },
|
||||
].filter(Boolean) as TabItem[]
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultActiveId="1"
|
||||
type="underlined"
|
||||
size="medium"
|
||||
activeId={!activeRoute ? indexRoute : activeRoute}
|
||||
onClick={(e: string) => {
|
||||
router.push(`/project/${ref}/logs/explorer/${e === indexRoute ? '' : e}`)
|
||||
}}
|
||||
>
|
||||
{tabs.map((item) => (
|
||||
<Tabs.Panel key={item.id} id={item.id} label={item.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogsNavigation
|
||||
@@ -44,6 +44,7 @@ const SignInSSOForm = () => {
|
||||
window.location.href = data.url
|
||||
}
|
||||
} else {
|
||||
setCaptchaToken(null)
|
||||
captchaRef.current?.resetCaptcha()
|
||||
|
||||
ui.setNotification({
|
||||
|
||||
@@ -15,7 +15,8 @@ interface LogsLayoutProps {
|
||||
const LogsLayout = ({ title, children }: PropsWithChildren<LogsLayoutProps>) => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
const page = router.pathname.split('/')[4]
|
||||
const pathArr = router.pathname.split('/')
|
||||
const page = pathArr[pathArr.length - 1]
|
||||
|
||||
const project = ui.selectedProject
|
||||
|
||||
|
||||
@@ -12,16 +12,24 @@ export const generateLogsMenu = (project?: Project): ProductMenuGroup[] => {
|
||||
return [
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
{
|
||||
title: 'Logs Explorer',
|
||||
items: [
|
||||
{ key: 'explorer', name: 'Query', root: true },
|
||||
{ key: 'saved', name: 'Saved Queries' },
|
||||
{ key: 'recent', name: 'Recent Queries' },
|
||||
{ key: 'templates', name: 'Templates' },
|
||||
].map(({ key, name, root }) => ({
|
||||
name,
|
||||
key,
|
||||
url: `/project/${ref}/logs/explorer${root ? '' : '/' + key}`,
|
||||
items: [],
|
||||
})),
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
name: 'Logs Explorer',
|
||||
key: 'explorer',
|
||||
url: `/project/${ref}/logs/explorer`,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Edge',
|
||||
name: 'API Edge Network',
|
||||
key: 'edge-logs',
|
||||
url: `/project/${ref}/logs/edge-logs`,
|
||||
items: [],
|
||||
|
||||
@@ -16,21 +16,19 @@ interface Props {
|
||||
|
||||
const ProjectPausedState: FC<Props> = ({ project }) => {
|
||||
const { ui, app } = useStore()
|
||||
const isOwner = ui.selectedOrganization?.is_owner
|
||||
const orgSlug = ui.selectedOrganization?.slug
|
||||
|
||||
const kpsEnabled = useFlag('initWithKps')
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const { membersExceededLimit } = useFreeProjectLimitCheck(orgSlug)
|
||||
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
|
||||
|
||||
const [showConfirmRestore, setShowConfirmRestore] = useState(false)
|
||||
const [showFreeProjectLimitWarning, setShowFreeProjectLimitWarning] = useState(false)
|
||||
|
||||
const { membersExceededLimit } = useFreeProjectLimitCheck(orgSlug)
|
||||
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
|
||||
|
||||
const canResumeProject = enablePermissions
|
||||
? checkPermissions(PermissionAction.INFRA_EXECUTE, 'queue_jobs.projects.initialize_or_resume')
|
||||
: isOwner
|
||||
const canResumeProject = checkPermissions(
|
||||
PermissionAction.INFRA_EXECUTE,
|
||||
'queue_jobs.projects.initialize_or_resume'
|
||||
)
|
||||
|
||||
const onSelectRestore = () => {
|
||||
if (!canResumeProject) {
|
||||
|
||||
@@ -43,10 +43,15 @@ function ConfirmModal({
|
||||
loading={loading}
|
||||
customFooter={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="default" onClick={() => onCancelClick()}>
|
||||
<Button type="default" disabled={loading} onClick={() => onCancelClick()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => onConfirmClick()} loading={loading}>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={loading}
|
||||
onClick={() => onConfirmClick()}
|
||||
loading={loading}
|
||||
>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import LogsNavigation from 'components/interfaces/Settings/Logs/LogsNavigation'
|
||||
import { IconList } from 'ui'
|
||||
|
||||
const LogsExplorerHeader = () => {
|
||||
return (
|
||||
<div className={['flex flex-col gap-4 transition-all'].join(' ')}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="flex h-6 w-6 items-center justify-center rounded border
|
||||
border-brand-600 bg-brand-300 text-brand-900
|
||||
"
|
||||
>
|
||||
<IconList size={14} strokeWidth={3} />
|
||||
</div>
|
||||
<h1 className="text-2xl text-scale-1200">Logs Explorer</h1>
|
||||
{/* {subtitle && <Badge color="scale">{subtitle}</Badge>} */}
|
||||
</div>
|
||||
<LogsNavigation />
|
||||
</div>
|
||||
)
|
||||
interface Props {
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
const LogsExplorerHeader: React.FC<Props> = ({ subtitle }) => (
|
||||
<div className={['flex flex-col gap-8 transition-all pb-6'].join(' ')}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="flex h-6 w-6 items-center justify-center rounded border
|
||||
border-brand-600 bg-brand-300 text-brand-900
|
||||
"
|
||||
>
|
||||
<IconList size={14} strokeWidth={3} />
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl text-scale-1200">Logs Explorer</h1>
|
||||
{subtitle && <span className="text-2xl text-scale-1100">{subtitle}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
export default LogsExplorerHeader
|
||||
@@ -26,7 +26,7 @@ const UpgradeToPro: FC<Props> = ({ icon, primaryText, projectRef, secondaryText
|
||||
>
|
||||
<div className="flex space-x-3">
|
||||
{icon && <div className="mt-1">{icon}</div>}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex w-full items-center justify-between space-x-32">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">{primaryText}</p>
|
||||
<div>
|
||||
|
||||
@@ -111,7 +111,7 @@ function useLogsPreview(
|
||||
|
||||
return `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${genQueryParams({
|
||||
...params,
|
||||
sql: genCountQuery(table),
|
||||
sql: genCountQuery(table, filters),
|
||||
iso_timestamp_start: latestRefresh,
|
||||
} as any)}`
|
||||
}
|
||||
|
||||
@@ -3,25 +3,13 @@ import useSWR, { mutate } from 'swr'
|
||||
import { Member } from 'types'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { useFlag } from 'hooks'
|
||||
|
||||
export function useOrganizationDetail(slug: string) {
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
|
||||
// Get org members
|
||||
const url = enablePermissions
|
||||
? `${API_URL}/organizations/${slug}/members?member_roles`
|
||||
: `${API_URL}/props/org/${slug}`
|
||||
const url = `${API_URL}/organizations/${slug}/members?member_roles`
|
||||
|
||||
const { data, error } = useSWR<any>(slug ? url : null, get)
|
||||
let members = enablePermissions
|
||||
? data
|
||||
: (data?.members ?? []).map((m: any) => ({
|
||||
id: m.id,
|
||||
is_owner: m.is_owner,
|
||||
username: m.profile.username,
|
||||
primary_email: m.profile.primary_email,
|
||||
})) ?? []
|
||||
let members = data
|
||||
|
||||
// Get pending invite users
|
||||
const pendingInviteUrl = `${API_URL}/organizations/${slug}/members/invite`
|
||||
@@ -37,7 +25,7 @@ export function useOrganizationDetail(slug: string) {
|
||||
username: x.invited_email.slice(0, 1),
|
||||
primary_email: x.invited_email,
|
||||
}
|
||||
return enablePermissions ? { ...member, role_ids: [x.role_id] } : member
|
||||
return { ...member, role_ids: [x.role_id] }
|
||||
})
|
||||
|
||||
members = [...members, ...invitedMembers]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import jsonLogic from 'json-logic-js'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { useStore } from 'hooks'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import useSWR from 'swr'
|
||||
@@ -40,9 +40,6 @@ export function checkPermissions(
|
||||
) {
|
||||
if (!IS_PLATFORM) return true
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
if (!enablePermissions) return true
|
||||
|
||||
const { ui } = useStore()
|
||||
const orgid = organizationId ?? ui?.selectedOrganization?.id
|
||||
|
||||
|
||||
@@ -75,6 +75,11 @@ const nextConfig = {
|
||||
destination: '/project/:ref/settings/billing/subscription',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/project/:ref/database/api-logs',
|
||||
destination: '/project/:ref/logs/edge-logs',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/project/:ref/database/postgres-logs',
|
||||
destination: '/project/:ref/logs/postgres-logs',
|
||||
|
||||
@@ -45,7 +45,6 @@ const Wizard: NextPageWithLayout = () => {
|
||||
const { slug } = router.query
|
||||
const { app, ui } = useStore()
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
const kpsEnabled = useFlag('initWithKps')
|
||||
const subscriptionStats = useSubscriptionStats()
|
||||
@@ -66,13 +65,8 @@ const Wizard: NextPageWithLayout = () => {
|
||||
const currentOrg = organizations.find((o: any) => o.slug === slug)
|
||||
const stripeCustomerId = currentOrg?.stripe_customer_id
|
||||
|
||||
const isOrganizationOwner = ui.selectedOrganization?.is_owner
|
||||
const availableRegions = getAvailableRegions()
|
||||
|
||||
const isAdmin = enablePermissions
|
||||
? checkPermissions(PermissionAction.CREATE, 'projects')
|
||||
: isOrganizationOwner
|
||||
|
||||
const isAdmin = checkPermissions(PermissionAction.CREATE, 'projects')
|
||||
const isInvalidSlug = isUndefined(currentOrg)
|
||||
const isEmptyOrganizations = organizations.length <= 0 && app.organizations.isInitialized
|
||||
const isEmptyPaymentMethod = paymentMethods ? !paymentMethods.length : false
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Button, IconClock } from 'ui'
|
||||
import { useStore } from 'hooks'
|
||||
import RecentQueriesItem from 'components/interfaces/Settings/Logs/RecentQueriesItem'
|
||||
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
|
||||
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import { useRouter } from 'next/router'
|
||||
import { LogSqlSnippets, NextPageWithLayout } from 'types'
|
||||
@@ -23,7 +22,7 @@ export const LogsSavedPage: NextPageWithLayout = () => {
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full px-5 py-6 h-full">
|
||||
<LogsExplorerHeader />
|
||||
<LogsExplorerHeader subtitle="Recent Queries" />
|
||||
{recent.length > 0 && (
|
||||
<Table
|
||||
head={
|
||||
|
||||
@@ -26,7 +26,7 @@ export const LogsSavedPage: NextPageWithLayout = () => {
|
||||
const saved = content.savedLogSqlSnippets()
|
||||
return (
|
||||
<div className="mx-auto w-full px-5 py-6 h-full">
|
||||
<LogsExplorerHeader />
|
||||
<LogsExplorerHeader subtitle="Saved Queries" />
|
||||
<div className="flex flex-col gap-3 py-6">
|
||||
{saved.length > 0 && (
|
||||
<Table
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer, useStaticRendering } from 'mobx-react-lite'
|
||||
import { IconCode, Badge, Collapsible, Button, Popover } from 'ui'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { IconCode, Button, Popover } from 'ui'
|
||||
import { TEMPLATES } from 'components/interfaces/Settings/Logs'
|
||||
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
|
||||
import CardButton from 'components/ui/CardButton'
|
||||
@@ -14,7 +14,7 @@ export const LogsTemplatesPage: NextPageWithLayout = () => {
|
||||
|
||||
return (
|
||||
<div className="mx-auto h-full w-full px-5 py-6">
|
||||
<LogsExplorerHeader />
|
||||
<LogsExplorerHeader subtitle="Templates" />
|
||||
<div className="grid grid-cols-3 gap-6 mt-4">
|
||||
{TEMPLATES.sort((a, b) => a.label!.localeCompare(b.label!))
|
||||
.filter((template) => template.mode === 'custom')
|
||||
|
||||
@@ -42,10 +42,7 @@ const ResetDbPassword: FC<any> = () => {
|
||||
const { ui } = useStore()
|
||||
const projectRef = ui.selectedProject?.ref
|
||||
|
||||
const enablePermissions = useFlag('enablePermissions')
|
||||
const canResetDbPassword = enablePermissions
|
||||
? checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
: ui.selectedOrganization?.is_owner
|
||||
const canResetDbPassword = checkPermissions(PermissionAction.UPDATE, 'projects')
|
||||
|
||||
const [showResetDbPass, setShowResetDbPass] = useState<boolean>(false)
|
||||
const [isUpdatingPassword, setIsUpdatingPassword] = useState<boolean>(false)
|
||||
|
||||
@@ -19,6 +19,8 @@ export const CONFIRMATION: FormSchema = {
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the message
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -48,6 +50,8 @@ export const INVITE: FormSchema = {
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the message
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -77,6 +81,8 @@ export const MAGIC_LINK: FormSchema = {
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the message
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -106,6 +112,8 @@ export const EMAIL_CHANGE: FormSchema = {
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the email change
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -135,6 +143,8 @@ export const RECOVERY: FormSchema = {
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the password reset
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
`,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -552,4 +552,12 @@
|
||||
animation-name: Toastify__slideOutDown;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=ReactToastify.css.map */
|
||||
.toast-message {
|
||||
max-width: 258px;
|
||||
div[role='status'] {
|
||||
@apply justify-start w-full break-words;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=ReactToastify.css.map */
|
||||
@@ -369,6 +369,9 @@ test('filters alter generated query', async () => {
|
||||
userEvent.click(await screen.findByText(/Save/))
|
||||
|
||||
await waitFor(() => {
|
||||
// counts are adjusted
|
||||
expect(get).toHaveBeenCalledWith(expect.stringMatching(/count.+\*.+as.count.+where.+500.+599/))
|
||||
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('500'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('599'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('200'))
|
||||
@@ -387,6 +390,11 @@ test('filters alter generated query', async () => {
|
||||
userEvent.click(await screen.findByText(/Save/))
|
||||
|
||||
await waitFor(() => {
|
||||
// counts are adjusted
|
||||
expect(get).not.toHaveBeenCalledWith(expect.stringMatching(/count.+\*.+as.count.+where.+500.+599/))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringMatching(/count.+\*.+as.count.+where.+400.+499/))
|
||||
|
||||
|
||||
expect(get).not.toHaveBeenCalledWith(expect.stringContaining('500'))
|
||||
expect(get).not.toHaveBeenCalledWith(expect.stringContaining('599'))
|
||||
expect(get).not.toHaveBeenCalledWith(expect.stringContaining('200'))
|
||||
|
||||
Reference in new issue
Block a user