diff --git a/.github/workflows/config-preview.yaml b/.github/workflows/config-preview.yaml deleted file mode 100644 index cca3c2d7de3..00000000000 --- a/.github/workflows/config-preview.yaml +++ /dev/null @@ -1,63 +0,0 @@ -name: Config (Preview) - -on: - pull_request: - types: - - opened - - reopened - - synchronize - branches: - - master - paths: - - "supabase/**" - workflow_dispatch: - -jobs: - wait: - runs-on: ubuntu-latest - outputs: - status: ${{ steps.check.outputs.conclusion }} - steps: - - uses: fountainhead/action-wait-for-check@v1.2.0 - id: check - with: - checkName: Supabase - ref: ${{ github.event.pull_request.head.sha }} - token: ${{ secrets.GITHUB_TOKEN }} - - apply: - needs: - - wait - if: ${{ needs.wait.outputs.status == 'success' }} - runs-on: ubuntu-latest - env: - SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} - TF_VAR_linked_project: xguihxuzqibwxjnimxev - TF_VAR_git_branch: ${{ github.head_ref }} - TF_CLI_ARGS_apply: -target=supabase_settings.preview - defaults: - run: - working-directory: supabase/remotes - outputs: - db_user: ${{ steps.branch.outputs.user }} - db_pass: ${{ steps.branch.outputs.password }} - db_host: ${{ steps.branch.outputs.host }} - db_port: ${{ steps.branch.outputs.port }} - jwt_secret: ${{ steps.branch.outputs.jwt_secret }} - ref: ${{ steps.branch.outputs.id }} - status: ${{ steps.branch.outputs.status }} - version: ${{ steps.branch.outputs.version }} - - steps: - - uses: actions/checkout@v4 - - uses: hashicorp/setup-terraform@v3 - with: - terraform_wrapper: false - - - run: terraform init - - run: terraform apply -auto-approve -no-color - - id: branch - run: | - terraform output -json branch_database \ - | jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]" \ - >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/config-production.yaml b/.github/workflows/config-production.yaml deleted file mode 100644 index b7f3dd3d63c..00000000000 --- a/.github/workflows/config-production.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Config (Production) - -on: - push: - branches: - - master - paths: - - "supabase/remotes/**" - workflow_dispatch: - -jobs: - apply: - runs-on: ubuntu-latest - env: - SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} - TF_VAR_linked_project: xguihxuzqibwxjnimxev - defaults: - run: - working-directory: supabase/remotes - - steps: - - uses: actions/checkout@v4 - - uses: hashicorp/setup-terraform@v3 - with: - terraform_wrapper: false - - - run: terraform init - - run: terraform apply -auto-approve -no-color diff --git a/.github/workflows/lint-prose.yml b/.github/workflows/lint-prose.yml index c2a402697a7..160985e74f0 100644 --- a/.github/workflows/lint-prose.yml +++ b/.github/workflows/lint-prose.yml @@ -11,15 +11,20 @@ jobs: name: runner / vale runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: sparse-checkout: | .vale.ini vale apps/docs/content + - name: Get changed files + id: changed-files + env: + BASE_REF: ${{ github.base_ref }} + SHA: ${{ github.sha }} + run: | + git fetch origin $BASE_REF + echo "CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT origin/$BASE_REF $SHA | grep '^apps/docs/content/' | tr '\n' ',' | sed 's/,$//')" >> $GITHUB_OUTPUT - uses: errata-ai/vale-action@reviewdog with: - files: apps/docs/content - # [Charis] Temporary because we still have to go through giant backlog of existing errors - # Don't want people to worry about this (just want them looking at the specific PR comments) - # fail_on_error: true + files: ${{ steps.changed-files.outputs.CHANGED_FILES }} diff --git a/apps/docs/content/guides/auth/quickstarts/react-native.mdx b/apps/docs/content/guides/auth/quickstarts/react-native.mdx index 4a0176049f3..220a8db979a 100644 --- a/apps/docs/content/guides/auth/quickstarts/react-native.mdx +++ b/apps/docs/content/guides/auth/quickstarts/react-native.mdx @@ -219,7 +219,6 @@ hideToc: true import { useState, useEffect } from 'react' import { supabase } from './lib/supabase' import Auth from './components/Auth' - import Account from './components/Account' import { View, Text } from 'react-native' import { Session } from '@supabase/supabase-js' diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index 260a32c341e..11b24c66efd 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -439,6 +439,8 @@ Unlike the OAuth flow which requires the use of a web browser, the native Sign i When the user provides consent, Google issues an identity token (commonly abbreviated as ID token) that is then sent to your project's Supabase Auth server. When valid, a new user session is started by issuing an access and refresh token from Supabase Auth. +By default, Supabase Auth implements nonce validation during the authentication flow. This can be disabled in production under `Authentication > Providers > Google > Skip Nonce Check` in the Dashboard, or when developing locally by setting `auth.external..skip_nonce_check`. Only disable this if your client libraries cannot properly handle nonce verification. + When working with Expo, you can use the [`react-native-google-signin/google-signin` library](https://github.com/react-native-google-signin/google-signin#expo-installation) library to obtain an ID token that you can pass to supabase-js [`signInWithIdToken` method](/docs/reference/javascript/auth-signinwithidtoken). Follow the [Expo installation docs](https://react-native-google-signin.github.io/docs/setting-up/expo) for installation and configuration instructions. See the [supabase-js reference](/docs/reference/javascript/initializing?example=react-native-options-async-storage) for instructions on initializing the supabase-js client in React Native. diff --git a/apps/docs/content/guides/database/custom-postgres-config.mdx b/apps/docs/content/guides/database/custom-postgres-config.mdx index 599f37e9810..59a41642bdd 100644 --- a/apps/docs/content/guides/database/custom-postgres-config.mdx +++ b/apps/docs/content/guides/database/custom-postgres-config.mdx @@ -53,6 +53,8 @@ Some settings can only be modified by a superuser. Supabase pre-enables the [`su | `auto_explain.log_min_duration` | Logs query plans taking longer than this duration. | | `auto_explain.log_nested_statements` | Log nested statements' plans. | | `log_min_messages` | Minimum severity level of messages to log. | +| `pg_net.ttl` | Sets how long the [pg_net extension](/docs/guides/database/extensions/pg_net) saves responses | +| `pg_net.batch_size` | Sets how many requests the [pg_net extension](/docs/guides/database/extensions/pg_net) can make per second | | `pgaudit.*` | Configures the [PGAudit extension](https://supabase.com/docs/guides/database/extensions/pgaudit). The `log_parameter` is still restricted to protect secrets | | `pgrst.*` | [`PostgREST` settings](https://docs.postgrest.org/en/stable/references/configuration.html#db-aggregates-enabled) | | `plan_filter.*` | Configures the [pg_plan_filter extension](https://supabase.com/docs/guides/database/extensions/pg_plan_filter) | diff --git a/apps/docs/content/guides/database/extensions/pgsodium.mdx b/apps/docs/content/guides/database/extensions/pgsodium.mdx index dbddf13b51f..5c3a9d4c92b 100644 --- a/apps/docs/content/guides/database/extensions/pgsodium.mdx +++ b/apps/docs/content/guides/database/extensions/pgsodium.mdx @@ -8,6 +8,12 @@ Supabase DOES NOT RECOMMEND any new usage of [`pgsodium`](https://github.com/mic The [`pgsodium`](https://github.com/michelp/pgsodium) extension is expected to go through a deprecation cycle in the near future. We will reach out to owners of impacted projects to assist with migrations away from [`pgsodium`](https://github.com/michelp/pgsodium) once the deprecation process begins. + + +The [Vault extension](/docs/guides/database/vault) won’t be impacted. Its internal implementation will shift away from pgsodium, but the interface and API will remain unchanged. + + + [`pgsodium`](https://github.com/michelp/pgsodium) is a Postgres extension which provides SQL access to [`libsodium`'s](https://doc.libsodium.org/) high-level cryptographic algorithms. Supabase previously documented two features derived from pgsodium. Namely [Server Key Management](https://github.com/michelp/pgsodium#server-key-management) and [Transparent Column Encryption](https://github.com/michelp/pgsodium#transparent-column-encryption). At this time, we do not recommend using either on the Supabase platform due to their high level of operational complexity and misconfiguration risk. diff --git a/apps/docs/content/guides/database/extensions/postgis.mdx b/apps/docs/content/guides/database/extensions/postgis.mdx index 9c470366db1..72b85b09aff 100644 --- a/apps/docs/content/guides/database/extensions/postgis.mdx +++ b/apps/docs/content/guides/database/extensions/postgis.mdx @@ -58,7 +58,7 @@ Let’s create our table. Each row represents a restaurant with its location sto create table if not exists public.restaurants ( id int generated by default as identity primary key, name text not null, - location geography(POINT) not null + location gis.geography(POINT) not null ); ``` diff --git a/apps/docs/content/guides/functions/examples/github-actions.mdx b/apps/docs/content/guides/functions/examples/github-actions.mdx index 3298913f089..5b665d8f4c8 100644 --- a/apps/docs/content/guides/functions/examples/github-actions.mdx +++ b/apps/docs/content/guides/functions/examples/github-actions.mdx @@ -34,7 +34,7 @@ jobs: PROJECT_ID: YOUR_SUPABASE_PROJECT_ID steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: supabase/setup-cli@v1 with: diff --git a/apps/docs/content/guides/platform/database-size.mdx b/apps/docs/content/guides/platform/database-size.mdx index 3ddc6cb224c..bb9174b5f9f 100644 --- a/apps/docs/content/guides/platform/database-size.mdx +++ b/apps/docs/content/guides/platform/database-size.mdx @@ -16,7 +16,7 @@ This SQL query will show the size of all databases in your Postgres cluster: ```sql select - sum(pg_database_size(pg_database.datname)) / (1024 * 1024) as db_size_mb + pg_size_pretty(sum(pg_database_size(pg_database.datname))) from pg_database; ``` @@ -30,6 +30,16 @@ Depending on your billing plan, your database can go into read-only mode which c +### Disk space usage + +Your database size is part of the disk usage for your Supabase project, there are many components to Postgres that consume additional disk space. One of the primary components, is the [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html). Postgres will store database changes in log files that are cleared away after they are applied to the database. These same files are also used by [Read Replicas](/docs/guides/platform/read-replicas) or other replication methods. + +If you would like to determine the size of the WAL files stored on disk, Postgres provides `pg_ls_waldir` as a helper function; the following query can be run: + +```sql +select pg_size_pretty(sum(size)) as wal_size from pg_ls_waldir(); +``` + ### Vacuum operations Postgres does not immediately reclaim the physical space used by dead tuples (i.e., deleted rows) in the DB. They are marked as "removed" until a [vacuum operation](https://www.postgresql.org/docs/current/routine-vacuuming.html) is executed. As a result, deleting data from your database may not immediately reduce the reported disk usage. You can use the [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started) `inspect db bloat` command to view all dead tuples in your database. Alternatively, you can run the [query](https://github.com/supabase/cli/blob/c9cce58025fded16b4c332747f819a44f45c3b83/internal/inspect/bloat/bloat.go#L17) found in the CLI's GitHub repo in the [SQL Editor](https://supabase.com/dashboard/project/_/sql/) diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index aef800a9d67..1bdab44de90 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -22,6 +22,11 @@ This upgrade method is currently in Beta. + + For security purposes, passwords for custom roles are not backed up and, following a restore, they + would need to be reset. See [here](/docs/guides/platform/backups#daily-backups) for more details + + pg_upgrade performs an in-place upgrade on your database. For projects larger than 1GB, pg_upgrade is generally faster than a pause+restore cycle, and the speed advantage grows with the size of the database. 1. Plan for an appropriate downtime window, and ensure you have reviewed the [caveats](#caveats) section of this document before executing the upgrade. diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index 9a9589100e9..e07ad7fb852 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 1.223.7 + version: 1.223.10 title: Supabase CLI language: sh source: https://github.com/supabase/cli @@ -3354,6 +3354,27 @@ commands: name: --use-pgadmin description: Use pgAdmin to generate schema diff. default_value: 'false' + - id: supabase-config + title: supabase config + summary: Manage Supabase project configurations + tags: + - management-api + links: [] + subcommands: + - supabase-config-push + flags: [] + - id: supabase-config-push + title: supabase config push + summary: Pushes local config.toml to the linked project + tags: [] + links: [] + usage: supabase config push + subcommands: [] + flags: + - id: project-ref + name: --project-ref + description: Project ref of the Supabase project. + default_value: '' - id: supabase-completion title: supabase completion summary: Generate the autocompletion script for the specified shell diff --git a/apps/docs/spec/cli_v1_config.yaml b/apps/docs/spec/cli_v1_config.yaml index c1c5657b914..1ef9416164d 100644 --- a/apps/docs/spec/cli_v1_config.yaml +++ b/apps/docs/spec/cli_v1_config.yaml @@ -44,6 +44,9 @@ info: - id: analytics title: Analytics description: Analytics settings. + - id: experimental + title: Experimental + description: Settings in alpha testing, subject to changes or removal at any time. - id: local title: Local Development description: Local Development settings. @@ -190,6 +193,237 @@ parameters: - name: 'PgBouncer Configuration' link: https://www.pgbouncer.org/config.html#default_pool_size + - id: 'db.settings.effective_cache_size' + title: 'db.settings.effective_cache_size' + tags: ['database'] + required: false + description: | + Sets the planner's assumption about the effective size of the disk cache. + This is a query planner parameter that doesn't affect actual memory allocation. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE' + + - id: 'db.settings.logical_decoding_work_mem' + title: 'db.settings.logical_decoding_work_mem' + tags: ['database'] + required: false + description: | + Specifies the amount of memory to be used by logical decoding, before writing data to local disk. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-LOGICAL-DECODING-WORK-MEM' + + - id: 'db.settings.maintenance_work_mem' + title: 'db.settings.maintenance_work_mem' + tags: ['database'] + required: false + description: | + Specifies the maximum amount of memory to be used by maintenance operations, such as VACUUM, CREATE INDEX, and ALTER TABLE ADD FOREIGN KEY. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM' + + - id: 'db.settings.max_connections' + title: 'db.settings.max_connections' + tags: ['database'] + required: false + description: | + Determines the maximum number of concurrent connections to the database server. + Note: Changing this parameter requires a database restart. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-connection.html#GUC-MAX-CONNECTIONS' + + - id: 'db.settings.max_locks_per_transaction' + title: 'db.settings.max_locks_per_transaction' + tags: ['database'] + required: false + description: | + Controls the average number of object locks allocated for each transaction. + Note: Changing this parameter requires a database restart. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-locks.html#GUC-MAX-LOCKS-PER-TRANSACTION' + + - id: 'db.settings.max_parallel_maintenance_workers' + title: 'db.settings.max_parallel_maintenance_workers' + tags: ['database'] + required: false + description: | + Sets the maximum number of parallel workers that can be started by a single utility command. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-MAINTENANCE-WORKERS' + + - id: 'db.settings.max_parallel_workers' + title: 'db.settings.max_parallel_workers' + tags: ['database'] + required: false + description: | + Sets the maximum number of parallel workers that the system can support. + Note: Changing this parameter requires a database restart. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-WORKERS' + + - id: 'db.settings.max_parallel_workers_per_gather' + title: 'db.settings.max_parallel_workers_per_gather' + tags: ['database'] + required: false + description: | + Sets the maximum number of parallel workers that can be started by a single Gather or Gather Merge node. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-WORKERS-PER-GATHER' + + - id: 'db.settings.max_replication_slots' + title: 'db.settings.max_replication_slots' + tags: ['database'] + required: false + description: | + Specifies the maximum number of replication slots that the server can support. + Note: Changing this parameter requires a database restart. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-REPLICATION-SLOTS' + + - id: 'db.settings.max_slot_wal_keep_size' + title: 'db.settings.max_slot_wal_keep_size' + tags: ['database'] + required: false + description: | + Specifies the maximum size of WAL files that replication slots are allowed to retain in the pg_wal directory. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-SLOT-WAL-KEEP-SIZE' + + - id: 'db.settings.max_standby_archive_delay' + title: 'db.settings.max_standby_archive_delay' + tags: ['database'] + required: false + description: | + Sets the maximum delay before canceling queries when a hot standby server is processing archived WAL data. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-STANDBY-ARCHIVE-DELAY' + + - id: 'db.settings.max_standby_streaming_delay' + title: 'db.settings.max_standby_streaming_delay' + tags: ['database'] + required: false + description: | + Sets the maximum delay before canceling queries when a hot standby server is processing streamed WAL data. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-STANDBY-STREAMING-DELAY' + + - id: 'db.settings.max_wal_size' + title: 'db.settings.max_wal_size' + tags: ['database'] + required: false + description: | + Sets the maximum size of WAL files that the system will keep in the pg_wal directory. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-MAX-WAL-SIZE' + + - id: 'db.settings.max_wal_senders' + title: 'db.settings.max_wal_senders' + tags: ['database'] + required: false + description: | + Specifies the maximum number of concurrent connections from standby servers or streaming base backup clients. + Note: Changing this parameter requires a database restart. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-WAL-SENDERS' + + - id: 'db.settings.max_worker_processes' + title: 'db.settings.max_worker_processes' + tags: ['database'] + required: false + type: 'uint' + description: | + Sets the maximum number of background processes that the system can support. + Note: Changing this parameter requires a database restart. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-WORKER-PROCESSES' + + - id: 'db.settings.session_replication_role' + title: 'db.settings.session_replication_role' + tags: ['database'] + required: false + description: | + Controls whether triggers and rewrite rules are enabled. Valid values are: "origin", "replica", or "local". + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE' + + - id: 'db.settings.shared_buffers' + title: 'db.settings.shared_buffers' + tags: ['database'] + required: false + description: | + Sets the amount of memory the database server uses for shared memory buffers. + Note: Changing this parameter requires a database restart. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS' + + - id: 'db.settings.statement_timeout' + title: 'db.settings.statement_timeout' + tags: ['database'] + required: false + description: | + Abort any statement that takes more than the specified amount of time. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT' + + - id: 'db.settings.track_commit_timestamp' + title: 'db.settings.track_commit_timestamp' + tags: ['database'] + required: false + type: 'bool' + description: | + Record commit time of transactions. + Note: Changing this parameter requires a database restart. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-TRACK-COMMIT-TIMESTAMP' + + - id: 'db.settings.wal_keep_size' + title: 'db.settings.wal_keep_size' + tags: ['database'] + required: false + description: | + Specifies the minimum size of past log file segments kept in the pg_wal directory. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-KEEP-SIZE' + + - id: 'db.settings.wal_sender_timeout' + title: 'db.settings.wal_sender_timeout' + tags: ['database'] + required: false + description: | + Terminate replication connections that are inactive for longer than this amount of time. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT' + + - id: 'db.settings.work_mem' + title: 'db.settings.work_mem' + tags: ['database'] + required: false + description: | + Specifies the amount of memory to be used by internal sort operations and hash tables before writing to temporary disk files. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM' + - id: 'db.pooler.max_client_conn' title: 'db.pooler.max_client_conn' tags: ['database'] @@ -463,6 +697,56 @@ parameters: - name: 'Auth Server configuration' link: 'https://supabase.com/docs/reference/auth' + - id: 'auth.email.secure_password_change' + title: 'auth.email.secure_password_change' + tags: ['auth'] + required: false + type: 'bool' + default: false + description: | + If enabled, requires the user's current password to be provided when changing to a new password. + links: + - name: 'Auth Server configuration' + link: 'https://supabase.com/docs/reference/auth' + + - id: 'auth.email.max_frequency' + title: 'auth.email.max_frequency' + tags: ['auth'] + required: false + type: 'duration' + default: '1m' + description: | + The minimum amount of time that must pass between email requests. + Helps prevent email spam by limiting how frequently emails can be sent. + Example values: "1m", "1h", "24h" + links: + - name: 'Auth Server configuration' + link: 'https://supabase.com/docs/reference/auth' + + - id: 'auth.email.otp_length' + title: 'auth.email.otp_length' + tags: ['auth'] + required: false + default: '6' + description: | + The length of the OTP code to be sent in emails. + Must be between 6 and 10 digits. + links: + - name: 'Auth Server configuration' + link: 'https://supabase.com/docs/reference/auth' + + - id: 'auth.email.otp_exp' + title: 'auth.email.otp_exp' + tags: ['auth'] + required: false + default: '300' + description: | + The expiry time for an OTP code in seconds. + Default is 300 seconds (5 minutes). + links: + - name: 'Auth Server configuration' + link: 'https://supabase.com/docs/reference/auth' + - id: 'auth.email.smtp.host' title: 'auth.email.smtp.host' tags: ['auth'] @@ -780,6 +1064,38 @@ parameters: - name: 'Auth Server configuration' link: 'https://supabase.com/docs/reference/auth' + - id: 'auth.external.provider.skip_nonce_check' + title: 'auth.external..skip_nonce_check' + tags: ['auth'] + required: false + description: | + Disables nonce validation during OIDC authentication flow for the specified provider. Enable only when client libraries cannot properly handle nonce verification. Be aware that this reduces security by allowing potential replay attacks with stolen ID tokens. + links: + - name: 'Auth Server configuration' + link: 'https://supabase.com/docs/reference/auth' + + - id: 'auth.mfa.web_authn.enroll_enabled' + title: 'auth.mfa.web_authn.enroll_enabled' + tags: ['auth', 'local'] + required: false + default: 'false' + description: | + Enable WebAuthn enrollment for multi-factor authentication. + links: + - name: 'Auth Server configuration' + link: 'https://supabase.com/docs/reference/auth' + + - id: 'auth.mfa.web_authn.verify_enabled' + title: 'auth.mfa.web_authn.verify_enabled' + tags: ['auth', 'local'] + required: false + default: 'false' + description: | + Enable WebAuthn verification for multi-factor authentication. + links: + - name: 'Auth Server configuration' + link: 'https://supabase.com/docs/reference/auth' + - id: 'functions.function_name.enabled' title: 'functions..enabled' tags: ['edge-functions'] @@ -821,6 +1137,20 @@ parameters: - name: '`supabase functions` CLI subcommands' link: 'https://supabase.com/docs/reference/cli/supabase-functions' + - id: 'functions.function_name.entrypoint' + title: 'functions..entrypoint' + tags: ['edge-functions'] + required: false + description: | + Specify a custom entrypoint path for the function relative to the project root. + When not specified, defaults to `supabase/functions//index.ts`. + links: + - name: '`supabase functions` CLI subcommands' + link: 'https://supabase.com/docs/reference/cli/supabase-functions' + usage: | + [functions.my_function] + entrypoint = "path/to/custom/function.ts" + - id: 'analytics.enabled' title: 'analytics.enabled' tags: ['analytics'] @@ -863,3 +1193,66 @@ parameters: links: - name: Self-hosted Logflare Configuration link: https://supabase.com/docs/reference/self-hosting-analytics/list-endpoints#getting-started + + - id: 'experimental.webhooks.enabled' + title: 'experimental.webhooks.enabled' + tags: ['experimental'] + required: false + default: 'false' + description: | + Automatically enable webhook features on each new created branch + Note: This is an experimental feature and may change in future releases. + links: [] + + - id: 'experimental.orioledb_version' + title: 'experimental.orioledb_version' + tags: ['experimental'] + required: false + description: | + Configures Postgres storage engine to use OrioleDB with S3 support. + Note: This is an experimental feature and may change in future releases. + links: [] + + - id: 'experimental.s3_host' + title: 'experimental.s3_host' + tags: ['experimental'] + required: false + default: 'env(S3_HOST)' + description: | + Configures S3 bucket URL for OrioleDB storage. + Format example: .s3-.amazonaws.com + Note: This is an experimental feature and may change in future releases. + links: [] + + - id: 'experimental.s3_region' + title: 'experimental.s3_region' + tags: ['experimental'] + required: false + default: 'env(S3_REGION)' + description: | + Configures S3 bucket region for OrioleDB storage. + Example: us-east-1 + Note: This is an experimental feature and may change in future releases. + links: [] + + - id: 'experimental.s3_access_key' + title: 'experimental.s3_access_key' + tags: ['experimental'] + required: false + default: 'env(S3_ACCESS_KEY)' + description: | + Configures AWS_ACCESS_KEY_ID for S3 bucket access. + DO NOT commit your AWS access key to git. Use environment variable substitution instead. + Note: This is an experimental feature and may change in future releases. + links: [] + + - id: 'experimental.s3_secret_key' + title: 'experimental.s3_secret_key' + tags: ['experimental'] + required: false + default: 'env(S3_SECRET_KEY)' + description: | + Configures AWS_SECRET_ACCESS_KEY for S3 bucket access. + DO NOT commit your AWS secret key to git. Use environment variable substitution instead. + Note: This is an experimental feature and may change in future releases. + links: [] diff --git a/apps/docs/spec/common-cli-sections.json b/apps/docs/spec/common-cli-sections.json index 81a29efb105..6e2aaff44be 100644 --- a/apps/docs/spec/common-cli-sections.json +++ b/apps/docs/spec/common-cli-sections.json @@ -357,6 +357,30 @@ } ] }, + { + "type": "category", + "title": "Organizations", + "items": [ + { + "id": "supabase-orgs", + "title": "Manage organizations", + "slug": "supabase-orgs", + "type": "cli-command" + }, + { + "id": "supabase-orgs-create", + "title": "Create an organization", + "slug": "supabase-orgs-create", + "type": "cli-command" + }, + { + "id": "supabase-orgs-list", + "title": "List all organizations", + "slug": "supabase-orgs-list", + "type": "cli-command" + } + ] + }, { "type": "category", "title": "Projects", @@ -393,6 +417,24 @@ } ] }, + { + "type": "category", + "title": "Config", + "items": [ + { + "id": "supabase-config", + "title": "Manage project configurations", + "slug": "supabase-config", + "type": "cli-command" + }, + { + "id": "supabase-config-push", + "title": "Push config from a local file", + "slug": "supabase-config-push", + "type": "cli-command" + } + ] + }, { "type": "category", "title": "Branches", @@ -441,30 +483,6 @@ } ] }, - { - "type": "category", - "title": "Organizations", - "items": [ - { - "id": "supabase-orgs", - "title": "Manage organizations", - "slug": "supabase-orgs", - "type": "cli-command" - }, - { - "id": "supabase-orgs-create", - "title": "Create an organization", - "slug": "supabase-orgs-create", - "type": "cli-command" - }, - { - "id": "supabase-orgs-list", - "title": "List all organizations", - "slug": "supabase-orgs-list", - "type": "cli-command" - } - ] - }, { "type": "category", "title": "Edge Functions", diff --git a/apps/docs/spec/examples/examples.yml b/apps/docs/spec/examples/examples.yml index 9a35a086b5e..77249cc8fa8 100644 --- a/apps/docs/spec/examples/examples.yml +++ b/apps/docs/spec/examples/examples.yml @@ -194,6 +194,7 @@ functions: - If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP. - If you're using phone, you can configure whether you want the user to receive a OTP. - The magic link's destination URL is determined by the [`SITE_URL`](https://supabase.com/docs/guides/auth/redirect-urls). You can modify the `SITE_URL` or add additional redirect URLs in [your project](https://supabase.com/dashboard/project/_/auth/url-configuration). + - If you are looking to send your own OTP emails, you can use the [Auth Admin command](https://supabase.com/docs/reference/javascript/auth-admin-generatelink) or the `Send Email` [Auth Hook](https://supabase.com/docs/guides/auth/auth-hooks) examples: - id: sign-in-with-email. name: Sign in with email. diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx index 87e0ca2ba68..dbe3227fec7 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx @@ -28,10 +28,12 @@ interface PolicyRowProps { policy: PostgresPolicy onSelectEditPolicy: (policy: PostgresPolicy) => void onSelectDeletePolicy: (policy: PostgresPolicy) => void + isLocked: boolean } const PolicyRow = ({ policy, + isLocked: isLockedDefault, onSelectEditPolicy = noop, onSelectDeletePolicy = noop, }: PolicyRowProps) => { @@ -42,6 +44,10 @@ const PolicyRow = ({ const { project } = useProjectContext() const { data: authConfig } = useAuthConfigQuery({ projectRef: project?.ref }) + // override islocked for Realtime messages table + const isLocked = + policy.schema === 'realtime' && policy.table === 'messages' ? false : isLockedDefault + // TODO(km): Simple check for roles that allow authenticated access. // In the future, we'll use splinter to return proper warnings for policies that allow anonymous user access. const appliesToAnonymousUsers = @@ -88,50 +94,52 @@ const PolicyRow = ({
- - -
) diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx index 3e76ab0df6a..1397050ade5 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx @@ -88,6 +88,7 @@ const PolicyTableRow = ({ {policies?.map((policy) => ( { )} -
+
{ export const RowData = ({ property, value }: { property: string; value?: string | boolean }) => { return ( -
-

{property}

- {typeof value === 'boolean' ? ( -
- {value ? ( -
- -
- ) : ( -
- -
- )} -
- ) : ( -
-

{!value ? '-' : value}

- {!!value && ( - } - className="transition opacity-0 group-hover:opacity-100 px-1" - text={value} - /> - )} -
- )} -
+ <> +
+

{property}

+ {typeof value === 'boolean' ? ( +
+ {value ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ ) : ( +
+

{!value ? '-' : value}

+ {!!value && ( + } + className="transition opacity-0 group-hover:opacity-100 px-1" + text={value} + /> + )} +
+ )} +
+ + ) } diff --git a/apps/studio/components/interfaces/Auth/Users/Users.constants.ts b/apps/studio/components/interfaces/Auth/Users/Users.constants.ts index c66df1abdba..97b20ae76c6 100644 --- a/apps/studio/components/interfaces/Auth/Users/Users.constants.ts +++ b/apps/studio/components/interfaces/Auth/Users/Users.constants.ts @@ -3,7 +3,12 @@ import { PROVIDER_PHONE, PROVIDERS_SCHEMAS } from '../AuthProvidersFormValidatio export const PROVIDER_FILTER_OPTIONS = PROVIDERS_SCHEMAS.map((provider) => ({ name: provider.title, - value: provider.title.toLowerCase(), + value: + provider.title === 'Slack (OIDC)' + ? 'slack_oidc' + : provider.title === 'LinkedIn (OIDC)' + ? 'linkedin_oidc' + : provider.title.toLowerCase(), icon: `${BASE_PATH}/img/icons/${provider.misc.iconKey}.svg`, iconClass: provider.title === 'GitHub' ? 'dark:invert' : '', })).concat( diff --git a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx index f0fe0b1d5e4..cd0ef0f72a7 100644 --- a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx +++ b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx @@ -1,36 +1,56 @@ +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useFlag } from 'hooks/ui/useFlag' import Link from 'next/link' import React from 'react' -import { useParams } from 'common' -import { NavMenu, NavMenuItem } from 'ui' +import { Badge, NavMenu, NavMenuItem } from 'ui' type Props = { - active: 'scheduled' | 'pitr' + active: 'pitr' | 'scheduled' | 'rtnp' } function DatabaseBackupsNav({ active }: Props) { - const { ref } = useParams() + const isCloneToNewProjectEnabled = useFlag('clonetonewproject') + const ref = useProjectContext()?.project?.ref const navMenuItems = [ { + enabled: true, id: 'scheduled', label: 'Scheduled backups', href: `/project/${ref}/database/backups/scheduled`, }, { + enabled: true, id: 'pitr', label: 'Point in time', href: `/project/${ref}/database/backups/pitr`, }, - ] + { + enabled: isCloneToNewProjectEnabled, + id: 'rtnp', + label: ( +
+ Restore to new project{' '} + + New + +
+ ), + href: `/project/${ref}/database/backups/restore-to-new-project`, + }, + ] as const return ( - {navMenuItems.map((item) => ( - - {item.label} - - ))} + {navMenuItems.map( + (item) => + item.enabled && ( + + {item.label} + + ) + )} ) } diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx index c5af3d64a9c..bfc8fb9a35b 100644 --- a/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx +++ b/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx @@ -1,23 +1,14 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' -import { format } from 'date-fns' -import dayjs from 'dayjs' -import { ChevronLeft, ChevronRight, HelpCircle } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' -import DatePicker from 'react-datepicker' import { useParams } from 'common' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FormHeader } from 'components/ui/Forms/FormHeader' -import { FormPanel } from 'components/ui/Forms/FormPanel' -import InformationBox from 'components/ui/InformationBox' import { useBackupsQuery } from 'data/database/backups-query' import { usePitrRestoreMutation } from 'data/database/pitr-restore-mutation' import { setProjectStatus } from 'data/projects/projects-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { PROJECT_STATUS } from 'lib/constants' import { Alert, @@ -31,15 +22,9 @@ import { import BackupsEmpty from '../BackupsEmpty' import BackupsStorageAlert from '../BackupsStorageAlert' import type { Timezone } from './PITR.types' -import { - constrainDateToRange, - formatNumberToTwoDigits, - getClientTimezone, - getDatesBetweenRange, -} from './PITR.utils' +import { getClientTimezone } from './PITR.utils' import PITRStatus from './PITRStatus' -import TimeInput from './TimeInput' -import { TimezoneSelection } from './TimezoneSelection' +import { PITRForm } from './pitr-form' const PITRSelection = () => { const router = useRouter() @@ -51,11 +36,11 @@ const PITRSelection = () => { const [showConfiguration, setShowConfiguration] = useState(false) const [showConfirmation, setShowConfirmation] = useState(false) const [selectedTimezone, setSelectedTimezone] = useState(getClientTimezone()) - - const canTriggerPhysicalBackups = useCheckPermissions( - PermissionAction.INFRA_EXECUTE, - 'queue_job.restore.prepare' - ) + const [selectedRecoveryPoint, setSelectedRecoveryPoint] = useState<{ + recoveryTimeTargetUnix: number + recoveryTimeString: string + recoveryTimeStringUtc: string + }>() const hasReadReplicas = (databases ?? []).length > 1 @@ -76,67 +61,16 @@ const PITRSelection = () => { const { earliestPhysicalBackupDateUnix, latestPhysicalBackupDateUnix } = backups?.physicalBackupData ?? {} const hasNoBackupsAvailable = !earliestPhysicalBackupDateUnix || !latestPhysicalBackupDateUnix - const earliestAvailableBackup = dayjs - .unix(earliestPhysicalBackupDateUnix ?? 0) - .tz(selectedTimezone.utc[0]) - const latestAvailableBackup = dayjs - .unix(latestPhysicalBackupDateUnix ?? 0) - .tz(selectedTimezone.utc[0]) - - const [selectedDateRaw, setSelectedDateRaw] = useState(latestAvailableBackup.toDate()) - const selectedDate = dayjs(selectedDateRaw).tz(selectedTimezone.utc[0], true) // true to keep local time and just change +whatever - - const isSelectedOnEarliestDay = selectedDate.isSame(earliestAvailableBackup, 'day') - const isSelectedOnLatestDay = selectedDate.isSame(latestAvailableBackup, 'day') - const availableDates = getDatesBetweenRange(earliestAvailableBackup, latestAvailableBackup) - - const selectedTime = { - h: selectedDate.hour(), - m: selectedDate.minute(), - s: selectedDate.second(), - } - - const earliestAvailableBackupTime = { - h: earliestAvailableBackup.hour(), - m: earliestAvailableBackup.minute(), - s: earliestAvailableBackup.second(), - } - - const latestAvailableBackupTime = { - h: latestAvailableBackup.hour(), - m: latestAvailableBackup.minute(), - s: latestAvailableBackup.second(), - } - - // This will be the actual unix timestamp for the backup - const recoveryTimeTargetUnix = selectedDate.unix() - // Formatting from the unix again just to double check correctness - const recoveryTimeString = selectedDate.format('DD MMM YYYY HH:mm:ss') - const recoveryTimeStringUtc = selectedDate.utc().format('DD MMM YYYY HH:mm:ss') - - const isSelectedOutOfRange = - selectedDate && - (selectedDate.isBefore(earliestAvailableBackup) || selectedDate.isAfter(latestAvailableBackup)) - - const onUpdateDate = (date: Date) => { - setSelectedDateRaw( - constrainDateToRange( - dayjs(date).tz(selectedTimezone.utc[0], true), - earliestAvailableBackup, - latestAvailableBackup - ).toDate() - ) - } - - const onCancel = () => { - setShowConfiguration(false) - setSelectedTimezone(getClientTimezone()) - } const onConfirmRestore = async () => { if (!ref) return console.error('Project ref is required') - if (!recoveryTimeTargetUnix) return console.error('Recovery time target unix is required') - restoreFromPitr({ ref, recovery_time_target_unix: recoveryTimeTargetUnix }) + if (!selectedRecoveryPoint?.recoveryTimeTargetUnix) + return console.error('Recovery time target unix is required') + + restoreFromPitr({ + ref, + recovery_time_target_unix: selectedRecoveryPoint.recoveryTimeTargetUnix, + }) } return ( @@ -177,173 +111,14 @@ const PITRSelection = () => { onSetConfiguration={() => setShowConfiguration(true)} /> ) : ( - - - - setShowConfirmation(true)} - tooltip={{ - content: { - side: 'bottom', - text: isSelectedOutOfRange - ? 'Selected date is out of range where backups are available' - : !canTriggerPhysicalBackups - ? 'You need additional permissions to trigger a restore' - : undefined, - }, - }} - > - Review restore details - -
- } - > -
-
- 'cursor-pointer'} - minDate={earliestAvailableBackup.toDate()} - maxDate={latestAvailableBackup.toDate()} - highlightDates={availableDates.map((date) => date.toDate())} - renderCustomHeader={({ - date, - decreaseMonth, - increaseMonth, - prevMonthButtonDisabled, - nextMonthButtonDisabled, - }) => ( -
-
- - - {format(date, 'MMMM yyyy')} - - -
-
- )} - /> -
-
-

Point in time back up available

-
-
- -
- {!selectedDate ? ( -
-
- } - title="Select a date which you'd like to restore your database to" - /> -
-
- ) : ( -
-
-

Restore database to

-

- {dayjs(selectedDate).format('DD MMM YYYY')} - - , {formatNumberToTwoDigits(selectedTime.h)}: - {formatNumberToTwoDigits(selectedTime.m)}: - {formatNumberToTwoDigits(selectedTime.s)} - -

-
-
-
-

Time zone

-
- -
-
-
-
-

Recovery time

- { - const newDate = dayjs(selectedDateRaw) - .set('hour', h) - .set('minute', m) - .set('second', s) - - setSelectedDateRaw(newDate.toDate()) - }} - /> -
- -

- Enter a time within the available range to restore from.
Backups - are captured every 2 minutes, allowing you to enter a time and restore - your database to the closest backup point. We'll match the time you - enter to the closest backup within the 2-minute window -

-
-
-

- {isSelectedOnEarliestDay && ( -

- Earliest backup available for this date:{' '} - {earliestAvailableBackup.format('HH:mm:ss')} -

- )} - {isSelectedOnLatestDay && ( -

- Latest backup available for this date:{' '} - {latestAvailableBackup.format('HH:mm:ss')} -

- )} -
-
-
- )} -
-
- + { + setSelectedRecoveryPoint(recoveryPoint) + setShowConfirmation(true) + }} + /> )} )} @@ -379,12 +154,12 @@ const PITRSelection = () => {
-

{selectedTimezone?.text}

-

{recoveryTimeString}

+

Local Time

+

{selectedRecoveryPoint?.recoveryTimeString}

(UTC+00:00)

-

{recoveryTimeStringUtc}

+

{selectedRecoveryPoint?.recoveryTimeStringUtc}

diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/TimezoneSelection.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/TimezoneSelection.tsx index d8b943ae0bb..d527da4b6d4 100644 --- a/apps/studio/components/interfaces/Database/Backups/PITR/TimezoneSelection.tsx +++ b/apps/studio/components/interfaces/Database/Backups/PITR/TimezoneSelection.tsx @@ -61,7 +61,7 @@ export const TimezoneSelection = ({ value={option} onSelect={(text) => { const selectedTimezone = ALL_TIMEZONES.find( - (option) => option.text.toLocaleLowerCase() === text + (option) => option.text === text ) if (selectedTimezone) { onSelectTimezone(selectedTimezone) diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx new file mode 100644 index 00000000000..5bd537845cb --- /dev/null +++ b/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx @@ -0,0 +1,264 @@ +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import DatePicker from 'react-datepicker' +import { FormPanel } from 'components/ui/Forms/FormPanel' +import { format } from 'date-fns' +import InformationBox from 'components/ui/InformationBox' +import dayjs from 'dayjs' +import { ChevronLeft, ChevronRight, HelpCircle } from 'lucide-react' +import { + constrainDateToRange, + formatNumberToTwoDigits, + getClientTimezone, + getDatesBetweenRange, +} from './PITR.utils' +import { TimezoneSelection } from './TimezoneSelection' +import TimeInput from './TimeInput' +import { Timezone } from './PITR.types' +import { useMemo, useState } from 'react' + +type Props = { + onSubmit: (data: { + recoveryTimeTargetUnix: number + recoveryTimeString: string + recoveryTimeStringUtc: string + }) => void + earliestAvailableBackupUnix: number + latestAvailableBackupUnix: number +} + +export function PITRForm({ + onSubmit, + earliestAvailableBackupUnix, + latestAvailableBackupUnix, +}: Props) { + const [selectedTimezone, setSelectedTimezone] = useState(getClientTimezone()) + const earliestAvailableBackup = dayjs + .unix(earliestAvailableBackupUnix ?? 0) + .tz(selectedTimezone.utc[0]) + const latestAvailableBackup = dayjs + .unix(latestAvailableBackupUnix ?? 0) + .tz(selectedTimezone.utc[0]) + + const [selectedDateRaw, setSelectedDateRaw] = useState(latestAvailableBackup.toDate()) + + const selectedDate = dayjs(selectedDateRaw).tz(selectedTimezone.utc[0], true) + const isSelectedOnEarliestDay = selectedDate.isSame(earliestAvailableBackup, 'day') + const isSelectedOnLatestDay = selectedDate.isSame(latestAvailableBackup, 'day') + const availableDates = getDatesBetweenRange(earliestAvailableBackup, latestAvailableBackup) + + const selectedTime = { + h: selectedDate.hour(), + m: selectedDate.minute(), + s: selectedDate.second(), + } + + const earliestAvailableBackupTime = { + h: earliestAvailableBackup.hour(), + m: earliestAvailableBackup.minute(), + s: earliestAvailableBackup.second(), + } + + const latestAvailableBackupTime = { + h: latestAvailableBackup.hour(), + m: latestAvailableBackup.minute(), + s: latestAvailableBackup.second(), + } + + const recoveryTimeTargetUnix = selectedDate.unix() + const recoveryTimeString = selectedDate.format('DD MMM YYYY HH:mm:ss') + const recoveryTimeStringUtc = selectedDate.utc().format('DD MMM YYYY HH:mm:ss') + + const isWithinRange = useMemo( + () => + (!!selectedDate && selectedDate.isSame(latestAvailableBackup)) || + selectedDate.isSame(earliestAvailableBackup) || + (selectedDate.isBefore(latestAvailableBackup) && + selectedDate.isAfter(earliestAvailableBackup)), + [selectedDate, latestAvailableBackup, earliestAvailableBackup] + ) + + const onUpdateDate = (date: Date) => { + setSelectedDateRaw( + constrainDateToRange( + dayjs(date).tz(selectedTimezone.utc[0], true), + earliestAvailableBackup, + latestAvailableBackup + ).toDate() + ) + } + + const handleSubmit = () => { + onSubmit({ + recoveryTimeTargetUnix, + recoveryTimeString, + recoveryTimeStringUtc, + }) + } + + return ( +
+ + + Continue + +
+ } + > +
+
+ 'cursor-pointer'} + minDate={earliestAvailableBackup.toDate()} + maxDate={latestAvailableBackup.toDate()} + highlightDates={availableDates.map((date) => date.toDate())} + renderCustomHeader={({ + date, + decreaseMonth, + increaseMonth, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }) => ( +
+
+ + + {format(date, 'MMMM yyyy')} + + +
+
+ )} + /> + {availableDates.length > 1 && ( +
+
+

Point in time back up available

+
+ )} +
+ +
+ {!selectedDate ? ( +
+
+ } + title="Select a date which you'd like to restore your database to" + /> +
+
+ ) : ( +
+
+

Date to restore to

+

+ {dayjs(selectedDate).format('DD MMM YYYY')} + + , {formatNumberToTwoDigits(selectedTime.h)}: + {formatNumberToTwoDigits(selectedTime.m)}: + {formatNumberToTwoDigits(selectedTime.s)} + +

+
+
+
+

Time zone

+
+ +
+
+
+
+

Recovery time

+ { + const newDate = dayjs(selectedDateRaw) + .set('hour', h) + .set('minute', m) + .set('second', s) + + setSelectedDateRaw(newDate.toDate()) + }} + /> +
+ +

+ Enter a time within the available range to restore from.
Backups are + captured every 2 minutes, allowing you to enter a time and restore your + database to the closest backup point. We'll match the time you enter to the + closest backup within the 2-minute window +

+
+
+

+ {isSelectedOnEarliestDay && ( +

+ Earliest backup available for this date:{' '} + {earliestAvailableBackup.format('HH:mm:ss')} +

+ )} + {isSelectedOnLatestDay && ( +

+ Latest backup available for this date:{' '} + {latestAvailableBackup.format('HH:mm:ss')} +

+ )} +
+
+
+ )} +
+
+ +
+ ) +} diff --git a/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/AdditionalMonthlySpend.tsx b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/AdditionalMonthlySpend.tsx new file mode 100644 index 00000000000..d78b34b0846 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/AdditionalMonthlySpend.tsx @@ -0,0 +1,45 @@ +import { instanceSizeSpecs } from 'data/projects/new-project.constants' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' + +export const AdditionalMonthlySpend = () => { + const { project } = useProjectContext() + + /** + * New project will have the same compute size and disk size as the original project + */ + function getAdditionalMonthlySpend() { + const currentProjectComputeSize = project?.infra_compute_size + if (!currentProjectComputeSize) { + return null + } + + if (currentProjectComputeSize === 'nano') { + return null + } + + const additionalMonthlySpend = instanceSizeSpecs[currentProjectComputeSize] + + return additionalMonthlySpend + } + + const additionalMonthlySpend = getAdditionalMonthlySpend() + if (!additionalMonthlySpend) { + return null + } + + return ( +
+

+ The new project will have the same compute size and disk size as this project. You will be + able to update the compute size and disk size after the new project is created in{' '} + + Project Settings > Compute and Disk + +

+
+

Additional Monthly Compute + Disk Cost

+

${additionalMonthlySpend.priceMonthly}

+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/BackupsList.tsx b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/BackupsList.tsx new file mode 100644 index 00000000000..ad330001843 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/BackupsList.tsx @@ -0,0 +1,59 @@ +import Panel from 'components/ui/Panel' +import BackupsEmpty from '../BackupsEmpty' +import { useCloneBackupsQuery } from 'data/projects/clone-query' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { Badge, Button } from 'ui' +import { TimestampInfo } from 'ui-patterns' + +interface BackupsListProps { + onSelectRestore: (id: number) => void +} + +export const BackupsList = ({ onSelectRestore }: BackupsListProps) => { + const { project } = useProjectContext() + const organization = useSelectedOrganization() + + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) + const isFreePlan = subscription?.plan?.id === 'free' + + const { data: cloneBackups } = useCloneBackupsQuery( + { projectRef: project?.ref }, + { enabled: !isFreePlan } + ) + + return ( + + {cloneBackups?.backups.length === 0 ? ( + <> + + + ) : ( +
+ {/*
{JSON.stringify({ cloneStatus }, null, 2)}
*/} + {cloneBackups?.backups.map((backup) => { + if (!backup.isPhysicalBackup) return null + return ( +
+
+ +
+ {JSON.stringify(backup.status).replaceAll('"', '')} + {(backup.status as any) === 'COMPLETED' && ( + + )} +
+ ) + })} +
+ )} +
+ ) +} diff --git a/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog.tsx b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog.tsx new file mode 100644 index 00000000000..64a63c5b5b5 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog.tsx @@ -0,0 +1,84 @@ +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogSection, + DialogTitle, +} from 'ui' +import { AdditionalMonthlySpend } from './AdditionalMonthlySpend' + +interface ConfirmRestoreDialogProps { + open: boolean + onOpenChange: (value: boolean) => void + onSelectContinue: () => void +} + +export const ConfirmRestoreDialog = ({ + open, + onOpenChange, + onSelectContinue, +}: ConfirmRestoreDialogProps) => { + const { project } = useProjectContext() + const organization = useSelectedOrganization() + + return ( + + + + Confirm restore to a new project + + This process will create a new project and restore your database to it. + + + +
    +
  • + Project organization will stay the same: {organization?.name} +
  • +
  • + Project region will stay the same: {project?.region || ''} +
  • +
  • + A project can only be restored to another project once.
    + + This is a temporary limitation. Please contact us if you need to restore a project + to multiple other projects. + +
  • +
+
    +
  • What will be transferred?
  • +
      +
    • Database schema (tables, views, procedures)
    • +
    • All data and indexes
    • +
    • Database roles, permissions and users
    • +
    +
+
    +
  • What needs manual reconfiguration?
  • +
      +
    • Storage objects & settings
    • +
    • Edge Functions
    • +
    • Auth settings & API keys
    • +
    • Realtime settings
    • +
    • Database extensions and settings
    • +
    • Read replicas
    • +
    +
+
+ + + + + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/CreateNewProjectDialog.tsx b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/CreateNewProjectDialog.tsx new file mode 100644 index 00000000000..e795f55814b --- /dev/null +++ b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/CreateNewProjectDialog.tsx @@ -0,0 +1,205 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { debounce } from 'lodash' +import { useRef, useState } from 'react' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { z } from 'zod' + +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import PasswordStrengthBar from 'components/ui/PasswordStrengthBar' +import { useProjectCloneMutation } from 'data/projects/clone-mutation' +import { useCloneBackupsQuery } from 'data/projects/clone-query' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { passwordStrength } from 'lib/helpers' +import { generateStrongPassword } from 'lib/project' +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogSection, + DialogTitle, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Input, + Input_Shadcn_, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { AdditionalMonthlySpend } from './AdditionalMonthlySpend' + +interface CreateNewProjectDialogProps { + open: boolean + selectedBackupId: number | null + recoveryTimeTarget: number | null + onOpenChange: (value: boolean) => void + onCloneSuccess: () => void +} + +export const CreateNewProjectDialog = ({ + open, + selectedBackupId, + recoveryTimeTarget, + onOpenChange, + onCloneSuccess, +}: CreateNewProjectDialogProps) => { + const { project } = useProjectContext() + const organization = useSelectedOrganization() + + const [passwordStrengthScore, setPasswordStrengthScore] = useState(0) + const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('') + + const FormSchema = z.object({ + name: z.string().min(1), + password: z.string().min(1), + }) + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + name: '', + password: '', + }, + }) + + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) + const isFreePlan = subscription?.plan?.id === 'free' + + const { data: cloneBackups } = useCloneBackupsQuery( + { projectRef: project?.ref }, + { enabled: !isFreePlan } + ) + const hasPITREnabled = cloneBackups?.pitr_enabled + + const { mutate: triggerClone, isLoading: cloneMutationLoading } = useProjectCloneMutation({ + onError: (error) => { + console.error('error', error) + toast.error('Failed to restore to new project') + }, + onSuccess: () => { + toast.success('Restoration process started') + onCloneSuccess() + }, + }) + + const delayedCheckPasswordStrength = useRef( + debounce((value: string) => checkPasswordStrength(value), 300) + ).current + + async function checkPasswordStrength(value: string) { + const { message, strength } = await passwordStrength(value) + setPasswordStrengthScore(strength) + setPasswordStrengthMessage(message) + } + + const generatePassword = () => { + const password = generateStrongPassword() + form.setValue('password', password) + delayedCheckPasswordStrength(password) + } + + return ( + + + + Create new project + + This process will create a new project and restore your database to it. + + + +
{ + if (!project?.ref) { + toast.error('Project ref is required') + return + } + + if (hasPITREnabled && recoveryTimeTarget) { + triggerClone({ + projectRef: project?.ref, + newProjectName: data.name, + newDbPass: data.password, + recoveryTimeTarget: recoveryTimeTarget, + cloneBackupId: undefined, + }) + } else if (selectedBackupId) { + triggerClone({ + projectRef: project?.ref, + cloneBackupId: selectedBackupId, + newProjectName: data.name, + newDbPass: data.password, + recoveryTimeTarget: undefined, + }) + } else { + toast.error('No backup or point in time selected') + return + } + })} + > + + ( + + + + + + )} + /> + ( + + + 0} + onChange={(e) => { + const value = e.target.value + field.onChange(value) + if (value == '') { + setPasswordStrengthScore(-1) + setPasswordStrengthMessage('') + } else delayedCheckPasswordStrength(value) + }} + descriptionText={ + + } + /> + + + )} + /> + + + + + + + +
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx b/apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx index cf37b3147bc..eb96c6c1877 100644 --- a/apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx @@ -8,6 +8,7 @@ import { Badge, Button, Checkbox, Input, Listbox, SidePanel } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import FormEmptyBox from 'components/ui/FormBoxEmpty' import NoTableState from 'components/ui/States/NoTableState' import { @@ -19,9 +20,9 @@ import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/databas import { useTablesQuery } from 'data/tables/tables-query' import { BASE_PATH } from 'lib/constants' import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { PauseCircle, PlayCircle, Terminal } from 'lucide-react' import type { Dictionary } from 'types' import ChooseFunctionForm from './ChooseFunctionForm' -import { PauseCircle, PlayCircle, Terminal } from 'lucide-react' class CreateTriggerFormState { id: number | undefined @@ -46,6 +47,8 @@ class CreateTriggerFormState { table: { value: string } // @ts-ignore tableId: { value: number; error?: string } + // @ts-ignore + condition: { value: string } constructor() { makeAutoObservable(this) @@ -80,6 +83,7 @@ class CreateTriggerFormState { this.schema = { value: trigger?.schema ?? '' } this.table = { value: trigger?.table ?? '' } this.tableId = { value: trigger?.table_id ?? '' } + this.condition = { value: trigger?.condition ?? '' } } update(state: Dictionary) { @@ -342,6 +346,7 @@ const CreateTrigger = ({ trigger, visible, setVisible }: CreateTriggerProps) =>
+ {_localState.formState.condition.value && }
) : ( <> @@ -492,6 +497,34 @@ const SelectEnabledMode = observer(({}) => { ) }) +const DisplayCondition = observer(() => { + const _localState = useContext(CreateTriggerContext) + + return ( +
+
+ +
+
+ +
+ This condition must be met for the trigger to fire. +
+ + To update the condition, you must drop and recreate this trigger. + +
+
+
+ ) +}) + const SelectOrientation = observer(({}) => { const _localState = useContext(CreateTriggerContext) return ( diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx index e77fbe11591..2a7d974d274 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx @@ -50,7 +50,7 @@ const NewProjectPanel = () => { Table Editor
) @@ -235,7 +247,7 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => const CronSyntaxLoader = () => { return (
- {['*', '*', '*', '*', '*', '*'].map((char, i) => ( + {['*', '*', '*', '*', '*'].map((char, i) => ( {
) } + +const LoadingDots = () => { + return ( + + {[0, 1, 2].map((i) => ( + + . + + ))} + + ) +} diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts index e54f33da4e8..ad1dab0ead7 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts @@ -1,5 +1,5 @@ import { it } from 'vitest' -import { parseCronJobCommand } from './CronJobs.utils' +import { parseCronJobCommand, secondsPattern, cronPattern } from './CronJobs.utils' describe('parseCronJobCommand', () => { it('should return a default object when the command is null', () => { @@ -34,4 +34,38 @@ describe('parseCronJobCommand', () => { type: 'edge_function', }) }) + + // Array of test cases for secondsPattern + const secondsPatternTests = [ + { description: '10 seconds', command: '10 seconds' }, + { description: '30 seconds', command: '30 seconds' }, + ] + + // Run tests for secondsPattern + secondsPatternTests.forEach(({ description, command }) => { + it(`should match the regex for a secondsPattern with "${description}"`, () => { + expect(command).toMatch(secondsPattern) + }) + }) + + // Array of test cases for cronPattern + const cronPatternTests = [ + { description: 'every hour', command: '0 * * * *' }, + { description: 'every day at midnight', command: '0 0 * * *' }, + { description: 'every Monday at 9 AM', command: '0 9 * * 1' }, + { description: 'every 15th of the month at noon', command: '0 12 15 * *' }, + { description: 'every January 1st at 3 AM', command: '0 3 1 1 *' }, + { description: 'every 30 minutes', command: '30 * * * *' }, + { description: 'every weekday at 6 PM', command: '0 18 * * 1-5' }, + { description: 'every weekend at 10 AM', command: '0 10 * * 0,6' }, + { description: 'every quarter hour', command: '*/15 * * * *' }, + { description: 'twice daily at 8 AM and 8 PM', command: '0 8,20 * * *' }, + ] + + // Replace the single cronPattern test with forEach + cronPatternTests.forEach(({ description, command }) => { + it(`should match the regex for a cronPattern with "${description}"`, () => { + expect(command).toMatch(cronPattern) + }) + }) }) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts index f316468e5cf..af9f02fa528 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts @@ -110,3 +110,8 @@ export const parseCronJobCommand = (originalCommand: string): CronJobType => { return DEFAULT_CRONJOB_COMMAND } + +// detect seconds like "10 seconds" or normal cron syntax like "*/5 * * * *" +export const secondsPattern = /^\d+\s+seconds$/ +export const cronPattern = + /^(\*|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)(\s+(\*|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)){4}$/ diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx index 6bd57df71d3..30d4c97ac8b 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx @@ -4,14 +4,13 @@ export default function CronSyntaxChart() {
         {/* prettier-ignore */}
         {`
-┌───────────── second (0 - 59)
-│  ┌───────────── minute (0 - 59)
-│  │  ┌───────────── hour (0 - 23)
-│  │  │  ┌───────────── day of month (1 - 31)
-│  │  │  │  ┌───────────── month (1 - 12)
-│  │  │  │  │  ┌───────────── day of week (0 - 7)
-│  │  │  │  │  │
-*  *  *  *  *  * `}
+ ┌───────────── minute (0 - 59)
+ │  ┌───────────── hour (0 - 23)
+ │  │  ┌───────────── day of month (1 - 31)
+ │  │  │  ┌───────────── month (1 - 12)
+ │  │  │  │  ┌───────────── day of week (0 - 7)
+ │  │  │  │  │
+ *  *  *  *  * `}
       
) diff --git a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicePayButton.tsx b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicePayButton.tsx new file mode 100644 index 00000000000..3bf0e11cc87 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicePayButton.tsx @@ -0,0 +1,30 @@ +import { useInvoicePaymentLinkGetMutation } from 'data/invoices/invoice-payment-link-mutation' +import { toast } from 'sonner' +import { Button } from 'ui' + +interface InvoicePayButtonProps { + slug?: string + invoiceId: string +} + +const InvoicePayButton = ({ slug, invoiceId }: InvoicePayButtonProps) => { + const { mutate, isLoading } = useInvoicePaymentLinkGetMutation({ + onSuccess(data) { + toast.success('Redirecting to payment gateway...') + + window.location.href = data.redirectUrl + }, + }) + + function onPayNow() { + mutate({ slug, invoiceId }) + } + + return ( + + ) +} + +export default InvoicePayButton diff --git a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx index 4804764ae49..baf7142059c 100644 --- a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx +++ b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx @@ -21,6 +21,7 @@ import { formatCurrency } from 'lib/helpers' import { Button } from 'ui' import CurrentPaymentMethod from '../BillingSettings/PaymentMethods/CurrentPaymentMethod' import { FileText, Download, ChevronLeft, ChevronRight } from 'lucide-react' +import InvoicePayButton from './InvoicePayButton' const PAGE_LIMIT = 10 @@ -152,15 +153,7 @@ const InvoicesSettings = () => { InvoiceStatus.OPEN, InvoiceStatus.ISSUED, ].includes(x.status as InvoiceStatus) && ( - + )} + + ) + } + + if (isError) { + return + } + + if (!isActiveHealthy) { + return ( + + ) + } + + if (lastClone?.status === 'FAILED') { + return ( + + + + + ) + } + + if (lastClone?.status === 'COMPLETED') { + return ( + + + + + ) + } + + if (lastClone?.status === 'IN_PROGRESS') { + return ( + + + Restoration in progress + +

+ The new project{!!clonedProject ? ` ${clonedProject.name}` : ''} is currently being + created +

+ +
+
+ ) + } + + if ( + !isLoading && + hasPITREnabled && + !cloneBackups?.physicalBackupData.earliestPhysicalBackupDateUnix + ) { + return ( + + ) + } + + if (!isLoading && !hasPITREnabled && cloneBackups?.backups.length === 0) { + return + } + + return ( + <> + { + setShowConfirmationDialog(false) + setShowNewProjectDialog(true) + }} + /> + { + refetchCloneStatus() + setRefetchInterval(5000) + setShowNewProjectDialog(false) + }} + /> + {hasPITREnabled ? ( + <> + { + setShowConfirmationDialog(true) + setRecoveryTimeTarget(v.recoveryTimeTargetUnix) + }} + earliestAvailableBackupUnix={ + cloneBackups?.physicalBackupData.earliestPhysicalBackupDateUnix || 0 + } + latestAvailableBackupUnix={ + cloneBackups?.physicalBackupData.latestPhysicalBackupDateUnix || 0 + } + /> + + ) : ( + { + setSelectedBackupId(id) + setShowConfirmationDialog(true) + }} + /> + )} + + ) +} + +export default RestoreToNewProjectPage diff --git a/apps/studio/pages/project/[ref]/logs/cron-logs.tsx b/apps/studio/pages/project/[ref]/logs/cron-logs.tsx new file mode 100644 index 00000000000..cb80975342b --- /dev/null +++ b/apps/studio/pages/project/[ref]/logs/cron-logs.tsx @@ -0,0 +1,26 @@ +// select * from cron.job_run_details where jobid = '1' order by start_time desc limit 10 + +import { useRouter } from 'next/router' + +import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' +import LogsLayout from 'components/layouts/LogsLayout/LogsLayout' +import type { NextPageWithLayout } from 'types' +import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' + +export const LogPage: NextPageWithLayout = () => { + const router = useRouter() + const { ref } = router.query + + return ( + + ) +} + +LogPage.getLayout = (page) => {page} + +export default LogPage diff --git a/apps/studio/pages/vercel/setupProject.tsx b/apps/studio/pages/vercel/setupProject.tsx index a9d24ba6070..34e1bab9de7 100644 --- a/apps/studio/pages/vercel/setupProject.tsx +++ b/apps/studio/pages/vercel/setupProject.tsx @@ -1,11 +1,9 @@ -import generator from 'generate-password-browser' import { debounce } from 'lodash' import { makeAutoObservable } from 'mobx' import { observer, useLocalObservable } from 'mobx-react-lite' import { useRouter } from 'next/router' import { ChangeEvent, createContext, useContext, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' -import { Button, Input, Listbox } from 'ui' import VercelIntegrationLayout from 'components/layouts/VercelIntegrationLayout' import { @@ -22,9 +20,11 @@ import { PROVIDERS, } from 'lib/constants' import passwordStrength from 'lib/password-strength' +import { generateStrongPassword } from 'lib/project' import { VERCEL_INTEGRATION_CONFIGS } from 'lib/vercelConfigs' import { AWS_REGIONS } from 'shared-data' import type { Dictionary } from 'types' +import { Button, Input, Listbox } from 'ui' interface ISetupProjectStore { token: string @@ -211,13 +211,8 @@ const CreateProject = observer(() => { setPasswordStrengthMessage(message) } - function generateStrongPassword() { - const password = generator.generate({ - length: 16, - numbers: true, - uppercase: true, - }) - + function generatePassword() { + const password = generateStrongPassword() setDbPass(password) delayedCheckPasswordStrength(password) } @@ -271,7 +266,7 @@ const CreateProject = observer(() => { passwordStrengthScore={passwordStrengthScore} password={dbPass} passwordStrengthMessage={passwordStrengthMessage} - generateStrongPassword={generateStrongPassword} + generateStrongPassword={generatePassword} /> } /> diff --git a/apps/studio/styles/date-picker.scss b/apps/studio/styles/date-picker.scss index f21c7c88557..7825357eb34 100644 --- a/apps/studio/styles/date-picker.scss +++ b/apps/studio/styles/date-picker.scss @@ -96,7 +96,7 @@ } .react-datepicker__day--highlighted { - @apply bg-brand-600 text-foreground-light; + @apply bg-overlay-hover text-foreground-light; } .react-datepicker__aria-live { diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 847aae8cef4..69be2e14a93 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -9,7 +9,7 @@ name: supabase services: studio: container_name: supabase-studio - image: supabase/studio:20241014-c083b3b + image: supabase/studio:20241106-f29003e restart: unless-stopped healthcheck: test: @@ -77,7 +77,7 @@ services: auth: container_name: supabase-auth - image: supabase/gotrue:v2.158.1 + image: supabase/gotrue:v2.164.0 depends_on: db: # Disable this if you are using an external Postgres database @@ -119,6 +119,10 @@ services: GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP} GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS} GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM} + + # Uncomment to bypass nonce check in ID Token flow. Commonly set to true when using Google Sign In on mobile. + # GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: true + # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true # GOTRUE_SMTP_MAX_FREQUENCY: 1s GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL} @@ -302,7 +306,7 @@ services: functions: container_name: supabase-edge-functions - image: supabase/edge-runtime:v1.59.0 + image: supabase/edge-runtime:v1.62.2 restart: unless-stopped depends_on: analytics: @@ -367,7 +371,7 @@ services: # Comment out everything below this point if you are using an external Postgres database db: container_name: supabase-db - image: supabase/postgres:15.1.1.78 + image: supabase/postgres:15.6.1.139 healthcheck: test: pg_isready -U postgres -h localhost interval: 5s diff --git a/i18n/README.zh-cn.md b/i18n/README.zh-cn.md index 92196948240..d88db745230 100644 --- a/i18n/README.zh-cn.md +++ b/i18n/README.zh-cn.md @@ -110,6 +110,24 @@ Supabase 是一个[托管平台](https://supabase.com/dashboard)。你可以注 realtime-dart storage-dart functions-dart + + + Swift + supabase-swift + postgrest-swift + gotrue-swift + realtime-swift + storage-swift + functions-swift + + + Python + supabase-py + postgrest-py + gotrue-py + realtime-py + storage-py + functions-py 💚 社区 💚 @@ -150,15 +168,6 @@ Supabase 是一个[托管平台](https://supabase.com/dashboard)。你可以注 storage-kt functions-kt - - Python - supabase-py - postgrest-py - gotrue-py - realtime-py - storage-py - functions-py - Ruby supabase-rb @@ -177,15 +186,6 @@ Supabase 是一个[托管平台](https://supabase.com/dashboard)。你可以注 - - - - Swift - supabase-swift - postgrest-swift - gotrue-swift - realtime-swift - storage-swift - functions-swift - Godot Engine (GDScript) supabase-gdscript diff --git a/package-lock.json b/package-lock.json index 91b2310c540..fb802fb1465 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13493,14 +13493,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@rollup/plugin-commonjs/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@rollup/pluginutils": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", @@ -20640,8 +20632,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "license": "MIT", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -34273,9 +34266,10 @@ } }, "node_modules/npm-run-all/node_modules/cross-spawn": { - "version": "6.0.5", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, - "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", diff --git a/packages/ai-commands/src/sql/cron.ts b/packages/ai-commands/src/sql/cron.ts index c9f0ce5fdb1..27df01ec0fd 100644 --- a/packages/ai-commands/src/sql/cron.ts +++ b/packages/ai-commands/src/sql/cron.ts @@ -12,28 +12,37 @@ export async function generateCron(openai: OpenAI, prompt: string) { You are a cron syntax expert. Your purpose is to convert natural language time descriptions into valid cron expressions for pg_cron. Rules for responses: - - Output cron expressions in the 6-field format supported by pg_cron (seconds minute hour day month weekday) - - The seconds field is optional - if not specified, it defaults to 0 - - Provide a brief explanation of what the cron expression does + - Output cron expressions in the 5-field format supported by pg_cron + - Do not provide any explanation of what the cron expression does - Format output as markdown with the cron expression in a code block - - If the request is unclear or impossible, ask for clarification - - If the request isn't related to cron scheduling, explain that you can only help with cron expressions + - Do not ask for clarification if you need it. Just output the cron expression. Example input: "Every Monday at 3am" Example output: This cron expression runs every Monday at 3:00:00 AM: \`\`\` - 0 0 3 * * 1 + 0 3 * * 1 \`\`\` - Common patterns to remember: - - * means "every" for that field - - */n means "every n intervals" - - n means "exactly at n" - - n,m means "at n and m" - - n-m means "every value from n to m" - Field order: seconds (0-59), minute (0-59), hour (0-23), day (1-31), month (1-12), weekday (0-6, Sunday=0) + This cron expression runs every minute: + \`\`\` + * * * * * + \`\`\` + + Additional examples: + - Every minute: \`* * * * *\` + - Every 5 minutes: \`*/5 * * * *\` + - Every first of the month, at 00:00: \`0 0 1 * *\` + - Every night at midnight: \`0 0 * * *\` + - Every Monday at 2am: \`0 2 * * 1\` + + Field order: + - minute (0-59) + - hour (0-23) + - day (1-31) + - month (1-12) + - weekday (0-6, Sunday=0) Here is the user's prompt: ${prompt} diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index e4922dc9677..081229cb3ec 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -185,6 +185,16 @@ export interface paths { /** Restore project with a physical backup */ post: operations['BackupsController_restorePhysicalBackup'] } + '/platform/database/{ref}/clone': { + /** List valid backups to clone from */ + get: operations['CloneController_getValidBackups'] + /** Clone the current project from a backup */ + post: operations['CloneController_cloneCurrentProject'] + } + '/platform/database/{ref}/clone/status': { + /** Retrieve the current status of an existing cloning process */ + get: operations['CloneController_cloneProjectStatus'] + } '/platform/database/{ref}/hook-enable': { /** Enables Database Webhooks on the project */ post: operations['HooksController_enableHooks'] @@ -330,6 +340,10 @@ export interface paths { /** Gets invoice with the given invoice ID */ get: operations['OrgInvoicesController_getInvoice'] } + '/platform/organizations/{slug}/billing/invoices/{invoiceId}/payment-link': { + /** Gets the payment link to manually pay the given invoice */ + get: operations['OrgInvoicesController_getInvoicePaymentLink'] + } '/platform/organizations/{slug}/billing/invoices/upcoming': { /** Gets the upcoming invoice */ get: operations['OrgInvoicesController_getUpcomingInvoice'] @@ -752,18 +766,22 @@ export interface paths { delete: operations['v1-delete-a-warehouse-token'] } '/platform/projects/{ref}/analytics/warehouse/collections': { - /** Lists project's warehouse collections from logflare */ - get: operations['v1-list-all-warehouse-collections'] - /** Create a warehouse collection */ - post: operations['v1-create-a-warehouse-collection'] + /** Lists project's telemetry collections from logflare */ + get: operations['v1-list-all-telemetry-collections'] + /** Create a telemetry collection */ + post: operations['v1-create-a-telemetry-collection'] } '/platform/projects/{ref}/analytics/warehouse/collections/{token}': { - /** Get a warehouse collection */ - get: operations['v1-get-a-warehouse-collection'] - /** Delete a warehouse collection */ - delete: operations['v1-delete-a-warehouse-collection'] - /** Update a warehouse collection */ - patch: operations['v1-update-a-warehouse-collection'] + /** Get a telemetry collection */ + get: operations['v1-get-a-telemetry-collection'] + /** Delete a telemetry collection */ + delete: operations['v1-delete-a-telemetry-collection'] + /** Update a telemetry collection */ + patch: operations['v1-update-a-telemetry-collection'] + } + '/platform/projects/{ref}/analytics/warehouse/collections/{token}/schema': { + /** Get a telemetry collection schema */ + get: operations['v1-get-a-telemetry-collection-schema'] } '/platform/projects/{ref}/analytics/warehouse/endpoints': { /** Lists project's warehouse endpoints from logflare */ @@ -907,6 +925,12 @@ export interface paths { /** Modify database disk */ post: operations['DiskController_modifyDisk'] } + '/platform/projects/{ref}/disk/custom-config': { + /** Gets disk autoscale config */ + get: operations['DiskController_getDiskAutoscaleConfig'] + /** Updates disk autoscale config */ + post: operations['DiskController_updateDiskAutoscaleConfig'] + } '/platform/projects/{ref}/disk/util': { /** Get disk utilization */ get: operations['DiskController_getDiskUtilization'] @@ -967,6 +991,10 @@ export interface paths { /** Gets project's settings */ get: operations['SettingsController_getProjectApi'] } + '/platform/projects/{ref}/settings/sensitivity': { + /** Updates the given project sensitivity */ + patch: operations['SensitivityController_updateProjectSensitivity'] + } '/platform/projects/{ref}/status': { /** Gets project's status */ get: operations['ProjectStatusController_getStatus'] @@ -1191,6 +1219,10 @@ export interface paths { /** Processes Orb events */ post: operations['OrbWebhooksController_processEvent'] } + '/system/organizations/{slug}': { + /** Deletes organization */ + delete: operations['OrganizationSlugSystemController_deleteOrganization'] + } '/system/organizations/{slug}/billing/subscription': { /** Gets the current subscription */ get: operations['OrgSubscriptionSystemController_getSubscription'] @@ -1217,6 +1249,10 @@ export interface paths { /** Create a project */ post: operations['SystemProjectsController_createProject'] } + '/system/projects/{ref}': { + /** Deletes the given project */ + delete: operations['ProjectRefSystemController_deleteProject'] + } '/system/projects/{ref}/billing/addons': { /** Updates project addon */ post: operations['AddonsController_updateAddon'] @@ -1225,6 +1261,14 @@ export interface paths { /** Removes project addon */ delete: operations['AddonsController_removeAddon'] } + '/system/projects/{ref}/config/email-restrictions': { + /** Gets the current email restrictions for a project. */ + get: operations['ProjectEmailRestrictionsController_getEmailRestrictions'] + /** Updates the current email restrictions for a project. */ + put: operations['ProjectEmailRestrictionsController_updateEmailRestrictions'] + /** Removes the current email restrictions for a project. */ + delete: operations['ProjectEmailRestrictionsController_deleteEmailRestrictions'] + } '/system/projects/{ref}/config/update-jwt/complete': { /** Handle update project jwt on completion */ post: operations['ProjectUpdateJwtController_completeUpdateJwt'] @@ -1239,6 +1283,12 @@ export interface paths { /** Modify database disk */ post: operations['SystemProjectDiskController_modifyDisk'] } + '/system/projects/{ref}/disk/custom-config': { + /** Gets disk autoscale config */ + get: operations['SystemProjectDiskController_getDiskAutoscaleConfig'] + /** Updates disk autoscale config */ + post: operations['SystemProjectDiskController_updateDiskAutoscaleConfig'] + } '/system/projects/{ref}/disk/util': { /** Get disk utilization */ get: operations['SystemProjectDiskController_getDiskUtilization'] @@ -1310,6 +1360,10 @@ export interface paths { /** Processes a project's WAL verification report. */ put: operations['WalVerificationReportingController_processWalVerification'] } + '/system/projects/email-abuse': { + /** Reports email abuse from a postmark */ + post: operations['ProjectEmailAbuseController_reportEmailAbuseWebhookPostmark'] + } '/system/stripe/webhooks': { /** Processes Stripe event */ post: operations['StripeWebhooksController_processEvent'] @@ -1404,6 +1458,16 @@ export interface paths { /** Restore project with a physical backup */ post: operations['BackupsController_restorePhysicalBackup'] } + '/v0/database/{ref}/clone': { + /** List valid backups to clone from */ + get: operations['CloneController_getValidBackups'] + /** Clone the current project from a backup */ + post: operations['CloneController_cloneCurrentProject'] + } + '/v0/database/{ref}/clone/status': { + /** Retrieve the current status of an existing cloning process */ + get: operations['CloneController_cloneProjectStatus'] + } '/v0/database/{ref}/hook-enable': { /** Enables Database Webhooks on the project */ post: operations['HooksController_enableHooks'] @@ -1701,18 +1765,22 @@ export interface paths { delete: operations['v1-delete-a-warehouse-token'] } '/v0/projects/{ref}/analytics/warehouse/collections': { - /** Lists project's warehouse collections from logflare */ - get: operations['v1-list-all-warehouse-collections'] - /** Create a warehouse collection */ - post: operations['v1-create-a-warehouse-collection'] + /** Lists project's telemetry collections from logflare */ + get: operations['v1-list-all-telemetry-collections'] + /** Create a telemetry collection */ + post: operations['v1-create-a-telemetry-collection'] } '/v0/projects/{ref}/analytics/warehouse/collections/{token}': { - /** Get a warehouse collection */ - get: operations['v1-get-a-warehouse-collection'] - /** Delete a warehouse collection */ - delete: operations['v1-delete-a-warehouse-collection'] - /** Update a warehouse collection */ - patch: operations['v1-update-a-warehouse-collection'] + /** Get a telemetry collection */ + get: operations['v1-get-a-telemetry-collection'] + /** Delete a telemetry collection */ + delete: operations['v1-delete-a-telemetry-collection'] + /** Update a telemetry collection */ + patch: operations['v1-update-a-telemetry-collection'] + } + '/v0/projects/{ref}/analytics/warehouse/collections/{token}/schema': { + /** Get a telemetry collection schema */ + get: operations['v1-get-a-telemetry-collection-schema'] } '/v0/projects/{ref}/analytics/warehouse/endpoints': { /** Lists project's warehouse endpoints from logflare */ @@ -1838,6 +1906,12 @@ export interface paths { /** Modify database disk */ post: operations['DiskController_modifyDisk'] } + '/v0/projects/{ref}/disk/custom-config': { + /** Gets disk autoscale config */ + get: operations['DiskController_getDiskAutoscaleConfig'] + /** Updates disk autoscale config */ + post: operations['DiskController_updateDiskAutoscaleConfig'] + } '/v0/projects/{ref}/disk/util': { /** Get disk utilization */ get: operations['DiskController_getDiskUtilization'] @@ -1890,6 +1964,10 @@ export interface paths { /** Gets project's settings */ get: operations['SettingsController_getProjectApi'] } + '/v0/projects/{ref}/settings/sensitivity': { + /** Updates the given project sensitivity */ + patch: operations['SensitivityController_updateProjectSensitivity'] + } '/v0/projects/{ref}/status': { /** Gets project's status */ get: operations['ProjectStatusController_getStatus'] @@ -2047,6 +2125,8 @@ export interface paths { post: operations['ApiKeysController_createApiKey'] } '/v1/projects/{ref}/api-keys/{id}': { + /** [Alpha] Get API key */ + get: operations['ApiKeysController_getApiKey'] /** [Alpha] Deletes an API key for the project */ delete: operations['ApiKeysController_deleteApiKey'] /** [Alpha] Updates an API key for the project */ @@ -2388,7 +2468,8 @@ export interface components { name: string prefix?: string | null secret_jwt_template?: components['schemas']['ApiKeySecretJWTTemplate'] | null - type?: unknown + /** @enum {string|null} */ + type?: 'publishable' | 'secret' | 'legacy' | null updated_at?: string | null } ApiKeySecretJWTTemplate: { @@ -2744,6 +2825,14 @@ export interface components { result: components['schemas']['CustomHostnameDetails'] success: boolean } + CloneProjectDto: { + /** @default 0 */ + cloneBackupId?: number + newDbPass: string + newProjectName: string + /** @default 0 */ + recoveryTimeTarget?: number + } Column: { id: number name: string @@ -2931,7 +3020,7 @@ export interface components { /** @enum {string} */ tier: 'tier_payg' | 'tier_pro' | 'tier_free' | 'tier_team' | 'tier_enterprise' } - CreateOrganizationBodyV1: { + CreateOrganizationV1Dto: { name: string } CreatePolicyBody: { @@ -3353,6 +3442,14 @@ export interface components { DetachPaymentMethodBody: { card_id: string } + DiskAutoscaleConfig: { + /** @description Growth percentage for disk autoscaling */ + growth_percent?: number | null + /** @description Maximum limit the disk size will grow to in GB */ + max_size_gb?: number | null + /** @description Minimum increment size for disk autoscaling in GB */ + min_increment_gb?: number | null + } DiskRequestAttributesGP3: { iops: number size_gb: number @@ -3433,6 +3530,14 @@ export interface components { downloadName?: string transform?: components['schemas']['StorageObjectTransformOptions'] } + EmailRestrictionsResponseBody: { + inserted_at?: string + is_restricted: boolean + project_id: number + project_ref: string + restricted_to_quota?: string + updated_at?: string + } EventBody: { eventType: string message: string @@ -3499,6 +3604,7 @@ export interface components { import_map?: boolean import_map_path?: string name: string + resource_multiplier?: string slug: string /** @enum {string} */ status: 'ACTIVE' | 'REMOVED' | 'THROTTLED' @@ -3515,6 +3621,7 @@ export interface components { import_map?: boolean import_map_path?: string name: string + resource_multiplier?: string slug: string /** @enum {string} */ status: 'ACTIVE' | 'REMOVED' | 'THROTTLED' @@ -3998,6 +4105,9 @@ export interface components { subscription: string | null subtotal: number } + InvoicePaymentLinkResponse: { + redirectUrl: string + } JoinResponse: { billing_email: string id: number @@ -4121,6 +4231,9 @@ export interface components { MarkDefaultPaymentMethodBody: { payment_method_id: string } + MarkSensitiveBody: { + is_sensitive: boolean + } Member: { gotrue_id: string is_sso_user: boolean | null @@ -4779,6 +4892,16 @@ export interface components { db_schema: string endpoint: string } + ProjectClonedResponse: { + source_project_ref: string + target_project_ref: string + } + ProjectClonedStatusResponse: { + cloned_from: Record + clones: components['schemas']['TargetCloneStatus'][] + id: number + ref: string + } ProjectCreationVersionInfo: { postgres_engine: components['schemas']['PostgresEngine'] release_channel: components['schemas']['ReleaseChannel'] @@ -4807,7 +4930,6 @@ export interface components { inserted_at: string is_branch_enabled: boolean is_physical_backups_enabled: boolean - kpsVersion?: string lastDatabaseResizeAt?: string maxDatabasePreprovisionGb?: number name: string @@ -4816,7 +4938,6 @@ export interface components { ref: string region: string restUrl: string - serviceVersions?: components['schemas']['ServiceVersions'] /** @enum {string} */ status: | 'ACTIVE_HEALTHY' @@ -4904,6 +5025,10 @@ export interface components { | 'unsupported_reg_types' | 'auth_otp_long_expiry' | 'auth_otp_short_length' + | 'ssl_not_enforced' + | 'network_restrictions_not_set' + | 'password_requirements_min_length' + | 'pitr_not_enabled' remediation: Record title: string } @@ -4918,6 +5043,8 @@ export interface components { ref: string } ProjectResourceWarningsResponse: { + /** @enum {string|null} */ + auth_email_offender: 'critical' | 'warning' | null /** @enum {string|null} */ auth_rate_limit_exhaustion: 'critical' | 'warning' | null /** @enum {string|null} */ @@ -4952,6 +5079,9 @@ export interface components { ssl_enforced: boolean status: string } + ProjectSensitivityResponse: { + is_sensitive: boolean + } ProjectServiceApiKeyResponse: { api_key: string name: string @@ -4967,6 +5097,7 @@ export interface components { db_port: number db_user: string inserted_at: string + is_sensitive?: boolean jwt_secret?: string name: string ref: string @@ -5680,6 +5811,18 @@ export interface components { | 'REFERENCES' | 'TRIGGER' } + TargetClonedProject: { + ref: string + } + TargetCloneStatus: { + inserted_at: string | null + project_id: number + /** @enum {string} */ + status: 'COMPLETED' | 'IN_PROGRESS' | 'FAILED' | 'REMOVED' + target_project: components['schemas']['TargetClonedProject'] + target_project_id: number + updated_at: string | null + } TaxId: { country: string type: string @@ -6028,6 +6171,23 @@ export interface components { | '4_origin_setup_completed' | '5_services_reconfigured' } + UpdateDiskAutoscaleConfig: { + /** + * @description Growth percentage for disk autoscaling + * @default 50 + */ + growth_percent?: number | null + /** + * @description Maximum limit the disk size will grow to in GB + * @default 61440 + */ + max_size_gb?: number | null + /** + * @description Minimum increment size for disk autoscaling in GB + * @default 4 + */ + min_increment_gb?: number | null + } UpdateFunctionBody: { args?: string[] /** @enum {string} */ @@ -6360,6 +6520,9 @@ export interface components { /** @enum {string} */ restriction_status: 'grace_period' | 'grace_period_over' | 'null' | 'restricted' } + UpdateRestrictionsRequestBody: { + restricted_to_quota: string + } UpdateRestrictionsResponse: { message?: string restriction_data?: components['schemas']['RestrictionData'] @@ -6651,13 +6814,25 @@ export interface components { V1CreateFunctionBody: { body: string name: string + resource_multiplier?: string slug: string verify_jwt?: boolean } - V1CreateProjectBody: { + V1CreateProjectBodyDto: { /** @description Database password */ db_pass: string - desired_instance_size?: components['schemas']['DesiredInstanceSize'] + /** @enum {string} */ + desired_instance_size?: + | 'micro' + | 'small' + | 'medium' + | 'large' + | 'xlarge' + | '2xlarge' + | '4xlarge' + | '8xlarge' + | '12xlarge' + | '16xlarge' /** * @deprecated * @description This field is deprecated and is ignored in this request @@ -6670,14 +6845,16 @@ export interface components { /** * @deprecated * @description Subscription Plan is now set on organization level and is ignored in this request - * @example free * @enum {string} */ plan?: 'free' | 'pro' - postgres_engine?: components['schemas']['PostgresEngine'] + /** + * @description Postgres engine version. If not provided, the latest version will be used. + * @enum {string} + */ + postgres_engine?: '15' /** * @description Region you want your server to reside in - * @example us-east-1 * @enum {string} */ region: @@ -6699,8 +6876,13 @@ export interface components { | 'ca-central-1' | 'ap-south-1' | 'sa-east-1' - release_channel?: components['schemas']['ReleaseChannel'] /** + * @description Release channel. If not provided, GA will be used. + * @enum {string} + */ + release_channel?: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' + /** + * Format: uri * @description Template URL used to create the project from the CLI. * @example https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone */ @@ -6763,7 +6945,42 @@ export interface components { * @example 2023-03-29T16:32:59Z */ created_at: string - database?: components['schemas']['V1DatabaseResponse'] + /** @description Id of your project */ + id: string + /** @description Name of your project */ + name: string + /** @description Slug of your organization */ + organization_id: string + /** + * @description Region of your project + * @example us-east-1 + */ + region: string + /** @enum {string} */ + status: + | 'ACTIVE_HEALTHY' + | 'ACTIVE_UNHEALTHY' + | 'COMING_UP' + | 'GOING_DOWN' + | 'INACTIVE' + | 'INIT_FAILED' + | 'REMOVED' + | 'RESTARTING' + | 'UNKNOWN' + | 'UPGRADING' + | 'PAUSING' + | 'RESTORING' + | 'RESTORE_FAILED' + | 'PAUSE_FAILED' + | 'RESIZING' + } + V1ProjectWithDatabaseResponse: { + /** + * @description Creation timestamp + * @example 2023-03-29T16:32:59Z + */ + created_at: string + database: components['schemas']['V1DatabaseResponse'] /** @description Id of your project */ id: string /** @description Name of your project */ @@ -6822,6 +7039,7 @@ export interface components { V1UpdateFunctionBody: { body?: string name?: string + resource_multiplier?: string verify_jwt?: boolean } ValidateQueryBody: { @@ -7762,6 +7980,71 @@ export interface operations { } } } + /** List valid backups to clone from */ + CloneController_getValidBackups: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['BackupsResponse'] + } + } + /** @description Failed to list available valid backups */ + 500: { + content: never + } + } + } + /** Clone the current project from a backup */ + CloneController_cloneCurrentProject: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['CloneProjectDto'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['ProjectClonedResponse'] + } + } + /** @description Failed to clone the current project */ + 500: { + content: never + } + } + } + /** Retrieve the current status of an existing cloning process */ + CloneController_cloneProjectStatus: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['ProjectClonedStatusResponse'] + } + } + /** @description Failed to retrieve clone project status */ + 500: { + content: never + } + } + } /** Enables Database Webhooks on the project */ HooksController_enableHooks: { parameters: { @@ -8559,6 +8842,29 @@ export interface operations { } } } + /** Gets the payment link to manually pay the given invoice */ + OrgInvoicesController_getInvoicePaymentLink: { + parameters: { + path: { + invoiceId: string + slug: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['InvoicePaymentLinkResponse'] + } + } + 403: { + content: never + } + /** @description Failed to retrieve invoice payment link */ + 500: { + content: never + } + } + } /** Gets the upcoming invoice */ OrgInvoicesController_getUpcomingInvoice: { parameters: { @@ -11811,8 +12117,8 @@ export interface operations { } } } - /** Lists project's warehouse collections from logflare */ - 'v1-list-all-warehouse-collections': { + /** Lists project's telemetry collections from logflare */ + 'v1-list-all-telemetry-collections': { parameters: { path: { /** @description Project ref */ @@ -11828,14 +12134,14 @@ export interface operations { 403: { content: never } - /** @description Failed to fetch warehouse collections */ + /** @description Failed to fetch telemetry collections */ 500: { content: never } } } - /** Create a warehouse collection */ - 'v1-create-a-warehouse-collection': { + /** Create a telemetry collection */ + 'v1-create-a-telemetry-collection': { responses: { 201: { content: { @@ -11845,14 +12151,14 @@ export interface operations { 403: { content: never } - /** @description Failed to create warehouse collection */ + /** @description Failed to create telemetry collection */ 500: { content: never } } } - /** Get a warehouse collection */ - 'v1-get-a-warehouse-collection': { + /** Get a telemetry collection */ + 'v1-get-a-telemetry-collection': { responses: { 200: { content: { @@ -11862,14 +12168,14 @@ export interface operations { 403: { content: never } - /** @description Failed to fetch warehouse collection */ + /** @description Failed to fetch telemetry collection */ 500: { content: never } } } - /** Delete a warehouse collection */ - 'v1-delete-a-warehouse-collection': { + /** Delete a telemetry collection */ + 'v1-delete-a-telemetry-collection': { responses: { 200: { content: { @@ -11879,14 +12185,14 @@ export interface operations { 403: { content: never } - /** @description Failed to delete warehouse collection */ + /** @description Failed to delete telemetry collection */ 500: { content: never } } } - /** Update a warehouse collection */ - 'v1-update-a-warehouse-collection': { + /** Update a telemetry collection */ + 'v1-update-a-telemetry-collection': { responses: { 200: { content: { @@ -11896,7 +12202,24 @@ export interface operations { 403: { content: never } - /** @description Failed to update warehouse collection */ + /** @description Failed to update telemetry collection */ + 500: { + content: never + } + } + } + /** Get a telemetry collection schema */ + 'v1-get-a-telemetry-collection-schema': { + responses: { + 200: { + content: { + 'application/json': components['schemas']['LFSource'] + } + } + 403: { + content: never + } + /** @description Failed to fetch telemetry collection schema */ 500: { content: never } @@ -12912,6 +13235,57 @@ export interface operations { } } } + /** Gets disk autoscale config */ + DiskController_getDiskAutoscaleConfig: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['DiskAutoscaleConfig'] + } + } + 403: { + content: never + } + /** @description Failed to get project disk autoscale config */ + 500: { + content: never + } + } + } + /** Updates disk autoscale config */ + DiskController_updateDiskAutoscaleConfig: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateDiskAutoscaleConfig'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['DiskAutoscaleConfig'] + } + } + 403: { + content: never + } + /** @description Failed to update project disk autoscale config */ + 500: { + content: never + } + } + } /** Get disk utilization */ DiskController_getDiskUtilization: { parameters: { @@ -13246,6 +13620,34 @@ export interface operations { } } } + /** Updates the given project sensitivity */ + SensitivityController_updateProjectSensitivity: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['MarkSensitiveBody'] + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['ProjectSensitivityResponse'] + } + } + 403: { + content: never + } + /** @description Failed to update project */ + 500: { + content: never + } + } + } /** Gets project's status */ ProjectStatusController_getStatus: { parameters: { @@ -14479,6 +14881,24 @@ export interface operations { } } } + /** Deletes organization */ + OrganizationSlugSystemController_deleteOrganization: { + parameters: { + path: { + /** @description Organization slug */ + slug: string + } + } + responses: { + 200: { + content: never + } + /** @description Failed to delete organization */ + 500: { + content: never + } + } + } /** Gets the current subscription */ OrgSubscriptionSystemController_getSubscription: { parameters: { @@ -14627,6 +15047,20 @@ export interface operations { } } } + /** Deletes the given project */ + ProjectRefSystemController_deleteProject: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 204: { + content: never + } + } + } /** Updates project addon */ AddonsController_updateAddon: { parameters: { @@ -14669,6 +15103,71 @@ export interface operations { } } } + /** Gets the current email restrictions for a project. */ + ProjectEmailRestrictionsController_getEmailRestrictions: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['EmailRestrictionsResponseBody'] + } + } + /** @description Failed to retrieve email restrictions. */ + 500: { + content: never + } + } + } + /** Updates the current email restrictions for a project. */ + ProjectEmailRestrictionsController_updateEmailRestrictions: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateRestrictionsRequestBody'] + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['EmailRestrictionsResponseBody'] + } + } + /** @description Failed to update email restrictions. */ + 500: { + content: never + } + } + } + /** Removes the current email restrictions for a project. */ + ProjectEmailRestrictionsController_deleteEmailRestrictions: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['EmailRestrictionsResponseBody'] + } + } + /** @description Failed to remove email restrictions. */ + 500: { + content: never + } + } + } /** Handle update project jwt on completion */ ProjectUpdateJwtController_completeUpdateJwt: { parameters: { @@ -14757,6 +15256,57 @@ export interface operations { } } } + /** Gets disk autoscale config */ + SystemProjectDiskController_getDiskAutoscaleConfig: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['DiskAutoscaleConfig'] + } + } + 403: { + content: never + } + /** @description Failed to get project disk autoscale config */ + 500: { + content: never + } + } + } + /** Updates disk autoscale config */ + SystemProjectDiskController_updateDiskAutoscaleConfig: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateDiskAutoscaleConfig'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['DiskAutoscaleConfig'] + } + } + 403: { + content: never + } + /** @description Failed to update project disk autoscale config */ + 500: { + content: never + } + } + } /** Get disk utilization */ SystemProjectDiskController_getDiskUtilization: { parameters: { @@ -14833,6 +15383,7 @@ export interface operations { import_map?: boolean entrypoint_path?: string import_map_path?: string + resource_multiplier?: string } path: { /** @description Project ref */ @@ -14913,6 +15464,7 @@ export interface operations { import_map?: boolean entrypoint_path?: string import_map_path?: string + resource_multiplier?: string } path: { /** @description Project ref */ @@ -15134,6 +15686,18 @@ export interface operations { } } } + /** Reports email abuse from a postmark */ + ProjectEmailAbuseController_reportEmailAbuseWebhookPostmark: { + responses: { + 201: { + content: never + } + /** @description Failed to report email abuse */ + 500: { + content: never + } + } + } /** Processes Stripe event */ StripeWebhooksController_processEvent: { parameters: { @@ -15328,7 +15892,7 @@ export interface operations { 'v1-create-an-organization': { requestBody: { content: { - 'application/json': components['schemas']['CreateOrganizationBodyV1'] + 'application/json': components['schemas']['CreateOrganizationV1Dto'] } } responses: { @@ -15381,7 +15945,7 @@ export interface operations { responses: { 200: { content: { - 'application/json': components['schemas']['V1ProjectResponse'][] + 'application/json': components['schemas']['V1ProjectWithDatabaseResponse'][] } } } @@ -15390,7 +15954,7 @@ export interface operations { 'v1-create-a-project': { requestBody: { content: { - 'application/json': components['schemas']['V1CreateProjectBody'] + 'application/json': components['schemas']['V1CreateProjectBodyDto'] } } responses: { @@ -15412,7 +15976,7 @@ export interface operations { responses: { 200: { content: { - 'application/json': components['schemas']['V1ProjectResponse'] + 'application/json': components['schemas']['V1ProjectWithDatabaseResponse'] } } /** @description Failed to retrieve project */ @@ -15443,6 +16007,9 @@ export interface operations { /** Get project api keys */ 'v1-get-project-api-keys': { parameters: { + query: { + reveal: boolean + } path: { /** @description Project ref */ ref: string @@ -15459,6 +16026,9 @@ export interface operations { /** [Alpha] Creates a new API key for the project */ ApiKeysController_createApiKey: { parameters: { + query: { + reveal: boolean + } path: { /** @description Project ref */ ref: string @@ -15477,9 +16047,32 @@ export interface operations { } } } + /** [Alpha] Get API key */ + ApiKeysController_getApiKey: { + parameters: { + query: { + reveal: boolean + } + path: { + /** @description Project ref */ + ref: string + id: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['ApiKeyResponse'] + } + } + } + } /** [Alpha] Deletes an API key for the project */ ApiKeysController_deleteApiKey: { parameters: { + query: { + reveal: boolean + } path: { /** @description Project ref */ ref: string @@ -15500,6 +16093,9 @@ export interface operations { /** [Alpha] Updates an API key for the project */ ApiKeysController_updateApiKey: { parameters: { + query: { + reveal: boolean + } path: { /** @description Project ref */ ref: string @@ -16804,7 +17400,7 @@ export interface operations { 'v1-get-a-snippet': { parameters: { path: { - id: string + id: Record } } responses: { diff --git a/packages/ui-patterns/TimestampInfo/index.tsx b/packages/ui-patterns/TimestampInfo/index.tsx index 505a610611e..c7f49108553 100644 --- a/packages/ui-patterns/TimestampInfo/index.tsx +++ b/packages/ui-patterns/TimestampInfo/index.tsx @@ -84,12 +84,22 @@ export const TimestampInfo = ({ setCopied(false) }, 1000) }} - className={cn('grid grid-cols-2 gap-2 hover:bg-surface-100 px-2 py-1', { - 'bg-surface-100': copied, - })} + className={cn( + 'relative cursor-default grid grid-cols-2 gap-2 hover:bg-surface-100 px-2 py-1', + { + 'bg-surface-100': copied, + } + )} > {label}: - {copied ? 'Copied!' : value} +
+ {copied && ( + + Copied! + + )} + {value} +
) } diff --git a/supabase/config.toml b/supabase/config.toml index 961f4351a25..12986999396 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -30,8 +30,6 @@ port = 54323 [inbucket] # Port to use for the email testing server web interface. port = 54324 -smtp_port = 54325 -pop3_port = 54326 [storage] # The maximum file size allowed (e.g. "5MB", "500KB"). @@ -42,7 +40,11 @@ file_size_limit = "50MiB" # in emails. site_url = "http://localhost:3000" # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://localhost:3000"] +additional_redirect_urls = [ + "https://*-supabase.vercel.app/*/*", + "https://supabase.com/*/*", + "http://localhost:3000/*/*", +] # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one # week). jwt_expiry = 3600 @@ -61,7 +63,7 @@ enable_confirmations = false # Uncomment the following to use gh oAuth app locally with your own test app # to use env vars locally, run > `source ./supabase/.env && supabase [command...]` [auth.external.github] -enabled = true +enabled = false client_id = "env(GITHUB_CLIENT_ID)" secret = "env(GITHUB_SECRET)" # Overrides the default auth redirectUrl. diff --git a/supabase/remotes/.gitignore b/supabase/remotes/.gitignore deleted file mode 100644 index 9b8a46e692b..00000000000 --- a/supabase/remotes/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# Local .terraform directories -**/.terraform/* - -# .tfstate files -*.tfstate -*.tfstate.* - -# Crash log files -crash.log -crash.*.log - -# Exclude all .tfvars files, which are likely to contain sensitive data, such as -# password, private keys, and other secrets. These should not be part of version -# control as they are data points which are potentially sensitive and subject -# to change depending on the environment. -*.tfvars -*.tfvars.json - -# Ignore override files as they are usually used to override resources locally and so -# are not checked in -override.tf -override.tf.json -*_override.tf -*_override.tf.json - -# Include override files you do wish to add to version control using negated pattern -# !example_override.tf - -# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan -# example: *tfplan* - -# Ignore CLI configuration files -.terraformrc -terraform.rc diff --git a/supabase/remotes/preview.tf b/supabase/remotes/preview.tf deleted file mode 100644 index 663af43d97e..00000000000 --- a/supabase/remotes/preview.tf +++ /dev/null @@ -1,41 +0,0 @@ -variable "git_branch" { - type = string - default = null -} - -# Fetch all branches of a linked project -data "supabase_branch" "all" { - parent_project_ref = var.linked_project -} - -# Import an existing branch database -import { - for_each = { - for b in data.supabase_branch.all.branches : - b.git_branch => b - if b.git_branch == var.git_branch - } - to = supabase_branch.imported[0] - id = each.value.id -} - -resource "supabase_branch" "imported" { - count = length(var.git_branch[*]) - parent_project_ref = var.linked_project - git_branch = var.git_branch -} - -# Override auth settings for the current branch -resource "supabase_settings" "preview" { - count = length(var.git_branch[*]) - project_ref = supabase_branch.imported[0].database.id - - auth = jsonencode({ - site_url = "http://localhost:3001" - }) -} - -output "branch_database" { - value = one(supabase_branch.imported[*].database) - sensitive = true -} diff --git a/supabase/remotes/production.tf b/supabase/remotes/production.tf deleted file mode 100644 index 92a1b31140b..00000000000 --- a/supabase/remotes/production.tf +++ /dev/null @@ -1,15 +0,0 @@ -# Define a linked project variable as user input -variable "linked_project" { - type = string -} - -# Configure api settings for the linked project -resource "supabase_settings" "production" { - project_ref = var.linked_project - - api = jsonencode({ - db_schema = "public, storage, graphql_public" - db_extra_search_path = "public, extensions" - max_rows = 1000 - }) -} diff --git a/supabase/remotes/provider.tf b/supabase/remotes/provider.tf deleted file mode 100644 index 6fba014c65c..00000000000 --- a/supabase/remotes/provider.tf +++ /dev/null @@ -1,10 +0,0 @@ -terraform { - required_providers { - supabase = { - source = "supabase/supabase" - version = "~> 1.0" - } - } -} - -provider "supabase" {}