Compare commits

..

41 Commits

Author SHA1 Message Date
github-actions[bot]
6d8b243571 release(dashboard): 2.41.0 (#3647)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-05 08:26:31 +01:00
David Barroso
c9967b1a6d feat(dashboard): get github repositories from github itself (#3640)
Co-authored-by: David BM <correodelnino@gmail.com>
2025-11-04 16:46:01 +01:00
github-actions[bot]
7f72aadff9 release(packages/nhost-js): 4.1.0 (#3586)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-04 16:21:25 +01:00
github-actions[bot]
8faf9565bb release(services/storage): 0.9.0 (#3654)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-04 16:21:16 +01:00
github-actions[bot]
7ac3f12852 release(services/auth): 0.43.0 (#3667)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-04 16:21:10 +01:00
David Barroso
184a3ed190 feat(internal/lib): common oapi middleware for go services (#3663) 2025-11-04 16:17:41 +01:00
David Barroso
372c4e32d4 fix(ci): match the version exactly to avoid matching on pre-releases (#3666) 2025-11-04 15:54:01 +01:00
David Barroso
a68d261d8e fix(nhost-js): improvements to Session guard to avoid conflict with ProviderSession (#3662) 2025-11-04 10:53:37 +01:00
David Barroso
55bda3f56b fix(auth): dont mutate client URL (#3660) 2025-11-03 10:56:14 +01:00
David Barroso
2311e1dd77 feat(auth): if the callback state is wrong send back to the redirectTo as provider_state (#3649) 2025-10-31 12:13:35 +01:00
David Barroso
824ee142c4 chore(nixops): set system libraries consistently on darwin (#3656) 2025-10-31 11:06:38 +01:00
David Barroso
c662d063a7 chore(nixops): bump go to 1.25.3 and nixpkgs due to CVEs (#3652) 2025-10-30 16:37:52 +01:00
David Barroso
b518132349 chore(nhost-js): regenerate types (#3648) 2025-10-29 12:50:22 +01:00
David BM
b677d3768f fix(dashboard): update SQL editor to use correct hasura migrations API URL (#3645) 2025-10-28 15:58:25 +01:00
David Barroso
51ec151752 feat(auth): added endpoints to retrieve and refresh oauth2 providers' tokens (#3614) 2025-10-28 12:50:30 +01:00
David Barroso
223322d654 fix(ci): run pull_request_target workflows against PR (#3646) 2025-10-28 11:51:55 +01:00
David Barroso
add2c20c95 chore(nixops): bump nhost-cli (#3641) 2025-10-28 10:05:47 +01:00
github-actions[bot]
961bc5feea release(cli): 1.34.4 (#3644)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-28 09:46:18 +01:00
David Barroso
0ca89974b9 fix(cli): update NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL correctly (#3643) 2025-10-28 09:44:14 +01:00
github-actions[bot]
e8d52859a3 release(cli): 1.34.3 (#3624)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-27 16:05:01 +01:00
David Barroso
67740ebe3d chore(cli): bump nhost/dashboard to 2.40.0 (#3629)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-27 16:03:07 +01:00
github-actions[bot]
d6f7b01aee release(dashboard): 2.40.0 (#3631)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-27 15:23:14 +01:00
dependabot[bot]
0fc65df78d chore(ci): bump actions/upload-artifact from 4 to 5 (#3638)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 09:50:12 +01:00
dependabot[bot]
52e3db7f61 chore(ci): bump actions/download-artifact from 5 to 6 (#3639)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 08:51:05 +01:00
David Barroso
235449d68c chore(docs): update guidelines on the use of AI for contributions (#3637) 2025-10-27 08:46:10 +01:00
Jason Overmier
323834d212 feat(dashboard): allow configuring CSP header (#3627)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2025-10-23 12:05:04 +02:00
David Barroso
f7bd250f73 chore(ci): changed pull_request to pull_request_target for access to secrets (#3632) 2025-10-23 11:15:29 +02:00
David BM
579f9dbf31 chore(dashboard): various improvements to support ticket page (#3630)
Co-authored-by: robertkasza <robert.kasza@bishop-co.com>
2025-10-23 09:38:45 +02:00
github-actions[bot]
9f2b93d44b release(dashboard): 2.39.0 (#3600)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-22 10:59:29 +02:00
David Barroso
1aeef26ec6 feat(dashboard): move zendesk request to API route (#3628) 2025-10-22 10:54:42 +02:00
David Barroso
749bb4e637 feat(nhost-js): added various middlewares to work with headers and customizable createNhostClient (#3612) 2025-10-22 10:32:23 +02:00
David Barroso
accabc83f7 chore(cli): update schema (#3622) 2025-10-21 16:53:07 +02:00
David Barroso
8c127d7b6b chore(docs): fix broken link in openapi spec and minor mistakes in postmark integration info (#3621) 2025-10-21 12:32:44 +02:00
David BM
f9c614ef99 chore(deps): update Vite to address security advisory (#3620) 2025-10-21 12:18:29 +02:00
David Barroso
1d183f7fc4 feat(auth): encrypt TOTP secret (#3619) 2025-10-21 09:04:31 +02:00
github-actions[bot]
46e740f060 release(cli): 1.34.2 (#3603)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-20 14:30:47 +02:00
David Barroso
0d30ab4eec chore(cli): update schema (#3613) 2025-10-20 14:27:49 +02:00
github-actions[bot]
d5fd3cb59c release(services/auth): 0.42.4 (#3618)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-20 13:25:21 +02:00
David Barroso
f36d360b9e fix(auth): apply relationships on new projects (#3617) 2025-10-20 13:23:33 +02:00
github-actions[bot]
61af5087fd release(services/auth): 0.42.3 (#3608)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-20 12:42:03 +02:00
David Barroso
7429d8ae3f fix(auth): always apply expected metadata (#3616) 2025-10-20 12:37:52 +02:00
313 changed files with 10344 additions and 6477 deletions

View File

@@ -7,6 +7,8 @@ assignees: ''
--- ---
> **Note:** Bug reports that are clearly AI-generated will not be accepted and will be closed immediately. Please write your bug report in your own words.
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.

View File

@@ -7,6 +7,8 @@ assignees: ''
--- ---
> **Note:** Feature requests that are clearly AI-generated will not be accepted and will be closed immediately. Please write your feature request in your own words.
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

View File

@@ -8,6 +8,8 @@
--- Delete everything below this line before submitting your PR --- --- Delete everything below this line before submitting your PR ---
> **Note on AI-assisted contributions:** Contributions with the help of AI are permitted, but you are ultimately responsible for the quality of your submission and for ensuring it follows our contributing guidelines. **The PR description must be written in your own words and be clear and concise**. Please ensure you remove any superfluous code comments introduced by AI tools before submitting. PRs that clearly violate this rule will be closed without further review.
### PR title format ### PR title format
The PR title must follow the following pattern: The PR title must follow the following pattern:
@@ -30,6 +32,7 @@ Where `PKG` is:
- `deps`: For changes to dependencies - `deps`: For changes to dependencies
- `docs`: For changes to the documentation - `docs`: For changes to the documentation
- `examples`: For changes to the examples - `examples`: For changes to the examples
- `internal/lib`: For changes to Nhost's common libraries (internal)
- `mintlify-openapi`: For changes to the Mintlify OpenAPI tool - `mintlify-openapi`: For changes to the Mintlify OpenAPI tool
- `nhost-js`: For changes to the Nhost JavaScript SDK - `nhost-js`: For changes to the Nhost JavaScript SDK
- `nixops`: For changes to the NixOps - `nixops`: For changes to the NixOps

View File

@@ -17,7 +17,7 @@ runs:
# Define valid types and packages # Define valid types and packages
VALID_TYPES="feat|fix|chore" VALID_TYPES="feat|fix|chore"
VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|mintlify-openapi|nhost-js|nixops|storage" VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|internal\/lib|mintlify-openapi|nhost-js|nixops|storage"
# Check if title matches the pattern TYPE(PKG): SUMMARY # Check if title matches the pattern TYPE(PKG): SUMMARY
if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then

View File

@@ -1,8 +1,7 @@
--- ---
name: "auth: check and build" name: "auth: check and build"
on: on:
# pull_request_target: pull_request_target:
pull_request:
paths: paths:
- '.github/workflows/auth_checks.yaml' - '.github/workflows/auth_checks.yaml'
- '.github/workflows/wf_check.yaml' - '.github/workflows/wf_check.yaml'
@@ -18,6 +17,7 @@ on:
- '.golangci.yaml' - '.golangci.yaml'
- 'go.mod' - 'go.mod'
- 'go.sum' - 'go.sum'
- 'internal/lib/**'
- 'vendor/**' - 'vendor/**'
# auth # auth
@@ -49,7 +49,7 @@ jobs:
with: with:
NAME: auth NAME: auth
PATH: services/auth PATH: services/auth
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -64,7 +64,7 @@ jobs:
with: with:
NAME: auth NAME: auth
PATH: services/auth PATH: services/auth
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
DOCKER: true DOCKER: true
secrets: secrets:

View File

@@ -40,7 +40,7 @@ jobs:
cd ${{ matrix.project }} cd ${{ matrix.project }}
TAG_NAME=$(make release-tag-name) TAG_NAME=$(make release-tag-name)
VERSION=$(nix develop .\#cliff -c make changelog-next-version) VERSION=$(nix develop .\#cliff -c make changelog-next-version)
if git tag | grep -q "$TAG_NAME@$VERSION"; then if git tag | grep -qx "$TAG_NAME@$VERSION"; then
echo "Tag $TAG_NAME@$VERSION already exists, skipping release preparation" echo "Tag $TAG_NAME@$VERSION already exists, skipping release preparation"
else else
echo "Tag $TAG_NAME@$VERSION does not exist, proceeding with release preparation" echo "Tag $TAG_NAME@$VERSION does not exist, proceeding with release preparation"

View File

@@ -1,8 +1,7 @@
--- ---
name: "cli: check and build" name: "cli: check and build"
on: on:
# pull_request_target: pull_request_target:
pull_request:
paths: paths:
- '.github/workflows/cli_checks.yaml' - '.github/workflows/cli_checks.yaml'
- '.github/workflows/wf_check.yaml' - '.github/workflows/wf_check.yaml'
@@ -50,7 +49,7 @@ jobs:
with: with:
NAME: cli NAME: cli
PATH: cli PATH: cli
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -65,7 +64,7 @@ jobs:
with: with:
NAME: cli NAME: cli
PATH: cli PATH: cli
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
DOCKER: true DOCKER: true
secrets: secrets:
@@ -81,7 +80,7 @@ jobs:
with: with:
NAME: cli NAME: cli
PATH: cli PATH: cli
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}

View File

@@ -63,7 +63,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Get artifacts" - name: "Get artifacts"
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
path: ~/artifacts path: ~/artifacts

View File

@@ -1,8 +1,7 @@
--- ---
name: "codegen: check and build" name: "codegen: check and build"
on: on:
# pull_request_target: pull_request_target:
pull_request:
paths: paths:
- '.github/workflows/wf_check.yaml' - '.github/workflows/wf_check.yaml'
- '.github/workflows/codegen_checks.yaml' - '.github/workflows/codegen_checks.yaml'
@@ -48,7 +47,7 @@ jobs:
with: with:
NAME: codegen NAME: codegen
PATH: tools/codegen PATH: tools/codegen
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -62,7 +61,7 @@ jobs:
with: with:
NAME: codegen NAME: codegen
PATH: tools/codegen PATH: tools/codegen
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
DOCKER: false DOCKER: false
secrets: secrets:

View File

@@ -1,7 +1,7 @@
--- ---
name: "dashboard: check and build" name: "dashboard: check and build"
on: on:
pull_request: pull_request_target:
paths: paths:
- '.github/workflows/wf_build_artifacts.yaml' - '.github/workflows/wf_build_artifacts.yaml'
- '.github/workflows/wf_check.yaml' - '.github/workflows/wf_check.yaml'
@@ -54,7 +54,7 @@ jobs:
- check-permissions - check-permissions
with: with:
NAME: dashboard NAME: dashboard
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
ENVIRONMENT: preview ENVIRONMENT: preview
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -73,7 +73,7 @@ jobs:
with: with:
NAME: dashboard NAME: dashboard
PATH: dashboard PATH: dashboard
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
DOCKER: true DOCKER: true
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]' OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'
@@ -91,7 +91,7 @@ jobs:
with: with:
NAME: dashboard NAME: dashboard
PATH: dashboard PATH: dashboard
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }} NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
@@ -107,7 +107,7 @@ jobs:
with: with:
NAME: dashboard NAME: dashboard
PATH: dashboard PATH: dashboard
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
NHOST_TEST_DASHBOARD_URL: ${{ needs.deploy-vercel.outputs.preview-url }} NHOST_TEST_DASHBOARD_URL: ${{ needs.deploy-vercel.outputs.preview-url }}
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }} NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
NHOST_TEST_ORGANIZATION_NAME: ${{ vars.NHOST_TEST_ORGANIZATION_NAME }} NHOST_TEST_ORGANIZATION_NAME: ${{ vars.NHOST_TEST_ORGANIZATION_NAME }}

View File

@@ -148,7 +148,7 @@ jobs:
rm playwright-report.tar.gz rm playwright-report.tar.gz
- name: Upload encrypted Playwright report - name: Upload encrypted Playwright report
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
if: failure() if: failure()
with: with:
name: encrypted-playwright-report-${{ github.run_id }} name: encrypted-playwright-report-${{ github.run_id }}

View File

@@ -1,7 +1,7 @@
--- ---
name: "docs: check and build" name: "docs: check and build"
on: on:
pull_request: pull_request_target:
paths: paths:
- '.github/workflows/wf_check.yaml' - '.github/workflows/wf_check.yaml'
- '.github/workflows/dashboard_checks.yaml' - '.github/workflows/dashboard_checks.yaml'
@@ -62,7 +62,7 @@ jobs:
with: with:
NAME: docs NAME: docs
PATH: docs PATH: docs
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }} NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}

View File

@@ -1,8 +1,7 @@
--- ---
name: "examples/demos: check and build" name: "examples/demos: check and build"
on: on:
# pull_request_target: pull_request_target:
pull_request:
paths: paths:
- '.github/workflows/wf_check.yaml' - '.github/workflows/wf_check.yaml'
- '.github/workflows/examples_demos_checks.yaml' - '.github/workflows/examples_demos_checks.yaml'
@@ -64,7 +63,7 @@ jobs:
with: with:
NAME: demos NAME: demos
PATH: examples/demos PATH: examples/demos
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -78,7 +77,7 @@ jobs:
with: with:
NAME: demos NAME: demos
PATH: examples/demos PATH: examples/demos
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
DOCKER: false DOCKER: false
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]' OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'

View File

@@ -1,8 +1,7 @@
--- ---
name: "examples/guides: check and build" name: "examples/guides: check and build"
on: on:
# pull_request_target: pull_request_target:
pull_request:
paths: paths:
- '.github/workflows/wf_check.yaml' - '.github/workflows/wf_check.yaml'
- '.github/workflows/examples_guides_checks.yaml' - '.github/workflows/examples_guides_checks.yaml'
@@ -64,7 +63,7 @@ jobs:
with: with:
NAME: guides NAME: guides
PATH: examples/guides PATH: examples/guides
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -78,7 +77,7 @@ jobs:
with: with:
NAME: guides NAME: guides
PATH: examples/guides PATH: examples/guides
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
DOCKER: false DOCKER: false
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]' OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'

View File

@@ -1,8 +1,7 @@
--- ---
name: "examples/tutorials: check and build" name: "examples/tutorials: check and build"
on: on:
# pull_request_target: pull_request_target:
pull_request:
paths: paths:
- '.github/workflows/wf_check.yaml' - '.github/workflows/wf_check.yaml'
- '.github/workflows/examples_tutorials_checks.yaml' - '.github/workflows/examples_tutorials_checks.yaml'
@@ -64,7 +63,7 @@ jobs:
with: with:
NAME: tutorials NAME: tutorials
PATH: examples/tutorials PATH: examples/tutorials
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -78,7 +77,7 @@ jobs:
with: with:
NAME: tutorials NAME: tutorials
PATH: examples/tutorials PATH: examples/tutorials
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
DOCKER: false DOCKER: false
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]' OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'

View File

@@ -1,7 +1,7 @@
--- ---
name: "gen: AI review" name: "gen: AI review"
on: on:
pull_request: pull_request_target:
types: [opened, reopened, ready_for_review] types: [opened, reopened, ready_for_review]
issue_comment: issue_comment:
jobs: jobs:

View File

@@ -1,8 +1,7 @@
--- ---
name: "nhost-js: check and build" name: "nhost-js: check and build"
on: on:
# pull_request_target: pull_request_target:
pull_request:
paths: paths:
- '.github/workflows/wf_check.yaml' - '.github/workflows/wf_check.yaml'
- '.github/workflows/nhost-js_checks.yaml' - '.github/workflows/nhost-js_checks.yaml'
@@ -65,7 +64,7 @@ jobs:
with: with:
NAME: nhost-js NAME: nhost-js
PATH: packages/nhost-js PATH: packages/nhost-js
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -79,7 +78,7 @@ jobs:
with: with:
NAME: nhost-js NAME: nhost-js
PATH: packages/nhost-js PATH: packages/nhost-js
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
DOCKER: false DOCKER: false
secrets: secrets:

View File

@@ -1,8 +1,7 @@
--- ---
name: "nixops: check and build" name: "nixops: check and build"
on: on:
# pull_request_target: pull_request_target:
pull_request:
paths: paths:
- '.github/workflows/wf_check.yaml' - '.github/workflows/wf_check.yaml'
- '.github/workflows/nixops_checks.yaml' - '.github/workflows/nixops_checks.yaml'
@@ -40,7 +39,7 @@ jobs:
with: with:
NAME: nixops NAME: nixops
PATH: nixops PATH: nixops
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -54,7 +53,7 @@ jobs:
with: with:
NAME: nixops NAME: nixops
PATH: nixops PATH: nixops
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
DOCKER: true DOCKER: true
secrets: secrets:

View File

@@ -1,8 +1,7 @@
--- ---
name: "storage: check and build" name: "storage: check and build"
on: on:
# pull_request_target: pull_request_target:
pull_request:
paths: paths:
- '.github/workflows/storage_checks.yaml' - '.github/workflows/storage_checks.yaml'
- '.github/workflows/wf_check.yaml' - '.github/workflows/wf_check.yaml'
@@ -18,6 +17,7 @@ on:
- '.golangci.yaml' - '.golangci.yaml'
- 'go.mod' - 'go.mod'
- 'go.sum' - 'go.sum'
- 'internal/lib/**'
- 'vendor/**' - 'vendor/**'
# storage # storage
@@ -49,7 +49,7 @@ jobs:
with: with:
NAME: storage NAME: storage
PATH: services/storage PATH: services/storage
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets: secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }} AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -64,7 +64,7 @@ jobs:
with: with:
NAME: storage NAME: storage
PATH: services/storage PATH: services/storage
GIT_REF: ${{ github.sha }} GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
DOCKER: true DOCKER: true
secrets: secrets:

View File

@@ -85,7 +85,7 @@ jobs:
zip -r result.zip result zip -r result.zip result
- name: "Push artifact to artifact repository" - name: "Push artifact to artifact repository"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: ${{ inputs.NAME }}-artifact-${{ steps.vars.outputs.ARCH }}-${{ steps.vars.outputs.VERSION }} name: ${{ inputs.NAME }}-artifact-${{ steps.vars.outputs.ARCH }}-${{ steps.vars.outputs.VERSION }}
path: ${{ inputs.PATH }}/result.zip path: ${{ inputs.PATH }}/result.zip
@@ -100,7 +100,7 @@ jobs:
if: ${{ ( inputs.DOCKER ) }} if: ${{ ( inputs.DOCKER ) }}
- name: "Push docker image to artifact repository" - name: "Push docker image to artifact repository"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: ${{ inputs.NAME }}-docker-image-${{ steps.vars.outputs.ARCH }}-${{ steps.vars.outputs.VERSION }} name: ${{ inputs.NAME }}-docker-image-${{ steps.vars.outputs.ARCH }}-${{ steps.vars.outputs.VERSION }}
path: ${{ inputs.PATH }}/result path: ${{ inputs.PATH }}/result

View File

@@ -44,7 +44,7 @@ jobs:
echo "VERSION=$(make get-version VER=${{ inputs.VERSION }})" >> $GITHUB_OUTPUT echo "VERSION=$(make get-version VER=${{ inputs.VERSION }})" >> $GITHUB_OUTPUT
- name: "Get artifacts" - name: "Get artifacts"
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
path: ~/artifacts path: ~/artifacts

View File

@@ -55,7 +55,7 @@ jobs:
echo "VERSION=$(make get-version VER=${{ inputs.VERSION }})" >> $GITHUB_OUTPUT echo "VERSION=$(make get-version VER=${{ inputs.VERSION }})" >> $GITHUB_OUTPUT
- name: "Get artifacts" - name: "Get artifacts"
uses: actions/download-artifact@v5 uses: actions/download-artifact@v6
with: with:
path: ~/artifacts path: ~/artifacts

View File

@@ -16,6 +16,15 @@ Contributions are made to Nhost repos via Issues and Pull Requests (PRs). A few
- We work hard to make sure issues are handled on time, but it could take a while to investigate the root cause depending on the impact. A friendly ping in the comment thread to the submitter or a contributor can help draw attention if your issue is blocking. - We work hard to make sure issues are handled on time, but it could take a while to investigate the root cause depending on the impact. A friendly ping in the comment thread to the submitter or a contributor can help draw attention if your issue is blocking.
- If you've never contributed before, see [the first-timer's guide](https://github.com/firstcontributions/first-contributions) for resources and tips on getting started. - If you've never contributed before, see [the first-timer's guide](https://github.com/firstcontributions/first-contributions) for resources and tips on getting started.
### AI-Assisted Contributions
We have specific policies regarding AI-assisted contributions:
- **Issues**: Bug reports and feature requests that are clearly AI-generated will not be accepted and will be closed immediately. Please write your issues in your own words to ensure they are clear, specific, and contain the necessary context.
- **Pull Requests**: Contributions with the help of AI are permitted, but you are ultimately responsible for the quality of your submission and for ensuring it follows our contributing guidelines. The PR description must be written in your own words. Additionally, please remove any superfluous code comments introduced by AI tools before submitting. PRs that clearly violate this rule will be closed without further review.
In all cases, contributors must ensure their submissions are thoughtful, well-tested, and meet the project's quality standards.
### Issues ### Issues
Issues should be used to report problems with Nhost, request a new feature, or discuss potential changes before a PR is created. Issues should be used to report problems with Nhost, request a new feature, or discuss potential changes before a PR is created.

View File

@@ -3,6 +3,7 @@
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json", "$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
"moderate": true, "moderate": true,
"allowlist": [ "allowlist": [
"GHSA-9965-vmph-33xx" // https://github.com/advisories/GHSA-9965-vmph-33xx Update package once have a fix "GHSA-9965-vmph-33xx", // https://github.com/advisories/GHSA-9965-vmph-33xx Update package once have a fix
"GHSA-7mvr-c777-76hp" // https://github.com/advisories/GHSA-7mvr-c777-76hp Update package once Nix side is also updated
] ]
} }

View File

@@ -2,6 +2,26 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [cli@1.34.4] - 2025-10-28
### 🐛 Bug Fixes
- *(cli)* Update NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL correctly (#3643)
## [cli@1.34.3] - 2025-10-27
### ⚙️ Miscellaneous Tasks
- *(cli)* Update schema (#3622)
- *(cli)* Bump nhost/dashboard to 2.40.0 (#3629)
## [cli@1.34.2] - 2025-10-20
### ⚙️ Miscellaneous Tasks
- *(cli)* Minor fix to download script when specifying version (#3602)
- *(cli)* Update schema (#3613)
## [cli@1.34.1] - 2025-10-13 ## [cli@1.34.1] - 2025-10-13
### 🐛 Bug Fixes ### 🐛 Bug Fixes

View File

@@ -56,7 +56,7 @@ func CommandCloud() *cli.Command {
&cli.StringFlag{ //nolint:exhaustruct &cli.StringFlag{ //nolint:exhaustruct
Name: flagDashboardVersion, Name: flagDashboardVersion,
Usage: "Dashboard version to use", Usage: "Dashboard version to use",
Value: "nhost/dashboard:2.38.4", Value: "nhost/dashboard:2.40.0",
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"), Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
}, },
&cli.StringFlag{ //nolint:exhaustruct &cli.StringFlag{ //nolint:exhaustruct

View File

@@ -111,7 +111,7 @@ func CommandUp() *cli.Command { //nolint:funlen
&cli.StringFlag{ //nolint:exhaustruct &cli.StringFlag{ //nolint:exhaustruct
Name: flagDashboardVersion, Name: flagDashboardVersion,
Usage: "Dashboard version to use", Usage: "Dashboard version to use",
Value: "nhost/dashboard:2.38.4", Value: "nhost/dashboard:2.40.0",
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"), Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
}, },
&cli.StringFlag{ //nolint:exhaustruct &cli.StringFlag{ //nolint:exhaustruct

View File

@@ -56,6 +56,7 @@ func auth( //nolint:funlen
false, false,
false, false,
"00000000-0000-0000-0000-000000000000", "00000000-0000-0000-0000-000000000000",
"5181f67e2844e4b60d571fa346cac9c37fc00d1ff519212eae6cead138e639ba",
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get hasura env vars: %w", err) return nil, fmt.Errorf("failed to get hasura env vars: %w", err)

View File

@@ -33,6 +33,7 @@ func expectedAuth() *Service {
"AUTH_DISABLE_SIGNUP": "false", "AUTH_DISABLE_SIGNUP": "false",
"AUTH_EMAIL_PASSWORDLESS_ENABLED": "true", "AUTH_EMAIL_PASSWORDLESS_ENABLED": "true",
"AUTH_EMAIL_SIGNIN_EMAIL_VERIFIED_REQUIRED": "true", "AUTH_EMAIL_SIGNIN_EMAIL_VERIFIED_REQUIRED": "true",
"AUTH_ENCRYPTION_KEY": "5181f67e2844e4b60d571fa346cac9c37fc00d1ff519212eae6cead138e639ba",
"AUTH_GRAVATAR_DEFAULT": "gravatarDefault", "AUTH_GRAVATAR_DEFAULT": "gravatarDefault",
"AUTH_GRAVATAR_ENABLED": "true", "AUTH_GRAVATAR_ENABLED": "true",
"AUTH_GRAVATAR_RATING": "gravatarRating", "AUTH_GRAVATAR_RATING": "gravatarRating",

View File

@@ -344,7 +344,7 @@ func dashboard(
subdomain, "hasura", httpPort, useTLS, subdomain, "hasura", httpPort, useTLS,
) + "/console", ) + "/console",
"NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL": URL( "NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL": URL(
subdomain, "hasura", httpPort, useTLS), subdomain, "hasura", httpPort, useTLS) + "/apis/migrate",
"NEXT_PUBLIC_NHOST_STORAGE_URL": URL( "NEXT_PUBLIC_NHOST_STORAGE_URL": URL(
subdomain, "storage", httpPort, useTLS) + "/v1", subdomain, "storage", httpPort, useTLS) + "/v1",
}, },

View File

@@ -1,6 +1,6 @@
// Package auth provides primitives to interact with the openapi HTTP API. // Package auth provides primitives to interact with the openapi HTTP API.
// //
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.4.1 DO NOT EDIT. // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT.
package auth package auth
import ( import (

View File

@@ -1,6 +1,6 @@
// Package graphql provides primitives to interact with the openapi HTTP API. // Package graphql provides primitives to interact with the openapi HTTP API.
// //
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.4.1 DO NOT EDIT. // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT.
package graphql package graphql
import ( import (

View File

@@ -148,7 +148,7 @@ import (
#Hasura: { #Hasura: {
// Version of hasura, you can see available versions in the URL below: // Version of hasura, you can see available versions in the URL below:
// https://hub.docker.com/r/hasura/graphql-engine/tags // https://hub.docker.com/r/hasura/graphql-engine/tags
version: string | *"v2.46.0-ce" version: string | *"v2.48.5-ce"
// JWT Secrets configuration // JWT Secrets configuration
jwtSecrets: [#JWTSecret] jwtSecrets: [#JWTSecret]
@@ -223,7 +223,7 @@ import (
// Releases: // Releases:
// //
// https://github.com/nhost/hasura-storage/releases // https://github.com/nhost/hasura-storage/releases
version: string | *"0.7.2" version: string | *"0.8.2"
// Networking (custom domains at the moment) are not allowed as we need to do further // Networking (custom domains at the moment) are not allowed as we need to do further
// configurations in the CDN. We will enable it again in the future. // configurations in the CDN. We will enable it again in the future.
@@ -311,7 +311,7 @@ import (
// Releases: // Releases:
// //
// https://github.com/nhost/hasura-auth/releases // https://github.com/nhost/hasura-auth/releases
version: string | *"0.38.1" version: string | *"0.42.4"
// Resources for the service // Resources for the service
resources?: #Resources resources?: #Resources
@@ -651,6 +651,9 @@ import (
iops: uint32 | *3000 iops: uint32 | *3000
tput: uint32 | *125 tput: uint32 | *125
} }
encryptColumnKey?: string & =~"^[0-9a-fA-F]{64}$" // 32 bytes hex-encoded key
oldEncryptColumnKey?: string & =~"^[0-9a-fA-F]{64}$" // for key rotation
} }
persistentVolumesEncrypted: bool | *false persistentVolumesEncrypted: bool | *false

View File

@@ -70,18 +70,28 @@ type ConfigAIUpdateInput struct {
WebhookSecret *string `json:"webhookSecret,omitempty"` WebhookSecret *string `json:"webhookSecret,omitempty"`
} }
// Configuration for auth service
// You can find more information about the configuration here:
// https://github.com/nhost/hasura-auth/blob/main/docs/environment-variables.md
type ConfigAuth struct { type ConfigAuth struct {
ElevatedPrivileges *ConfigAuthElevatedPrivileges `json:"elevatedPrivileges,omitempty"` ElevatedPrivileges *ConfigAuthElevatedPrivileges `json:"elevatedPrivileges,omitempty"`
Method *ConfigAuthMethod `json:"method,omitempty"` Method *ConfigAuthMethod `json:"method,omitempty"`
Misc *ConfigAuthMisc `json:"misc,omitempty"` Misc *ConfigAuthMisc `json:"misc,omitempty"`
RateLimit *ConfigAuthRateLimit `json:"rateLimit,omitempty"` RateLimit *ConfigAuthRateLimit `json:"rateLimit,omitempty"`
Redirections *ConfigAuthRedirections `json:"redirections,omitempty"` Redirections *ConfigAuthRedirections `json:"redirections,omitempty"`
Resources *ConfigResources `json:"resources,omitempty"` // Resources for the service
Session *ConfigAuthSession `json:"session,omitempty"` Resources *ConfigResources `json:"resources,omitempty"`
SignUp *ConfigAuthSignUp `json:"signUp,omitempty"` Session *ConfigAuthSession `json:"session,omitempty"`
Totp *ConfigAuthTotp `json:"totp,omitempty"` SignUp *ConfigAuthSignUp `json:"signUp,omitempty"`
User *ConfigAuthUser `json:"user,omitempty"` Totp *ConfigAuthTotp `json:"totp,omitempty"`
Version *string `json:"version,omitempty"` User *ConfigAuthUser `json:"user,omitempty"`
// Version of auth, you can see available versions in the URL below:
// https://hub.docker.com/r/nhost/hasura-auth/tags
//
// Releases:
//
// https://github.com/nhost/hasura-auth/releases
Version *string `json:"version,omitempty"`
} }
type ConfigAuthElevatedPrivileges struct { type ConfigAuthElevatedPrivileges struct {
@@ -111,9 +121,11 @@ type ConfigAuthMethodAnonymousUpdateInput struct {
} }
type ConfigAuthMethodEmailPassword struct { type ConfigAuthMethodEmailPassword struct {
EmailVerificationRequired *bool `json:"emailVerificationRequired,omitempty"` EmailVerificationRequired *bool `json:"emailVerificationRequired,omitempty"`
HibpEnabled *bool `json:"hibpEnabled,omitempty"` // Disabling email+password sign in is not implmented yet
PasswordMinLength *uint32 `json:"passwordMinLength,omitempty"` // enabled: bool | *true
HibpEnabled *bool `json:"hibpEnabled,omitempty"`
PasswordMinLength *uint32 `json:"passwordMinLength,omitempty"`
} }
type ConfigAuthMethodEmailPasswordUpdateInput struct { type ConfigAuthMethodEmailPasswordUpdateInput struct {
@@ -335,8 +347,10 @@ type ConfigAuthRateLimitUpdateInput struct {
} }
type ConfigAuthRedirections struct { type ConfigAuthRedirections struct {
// AUTH_ACCESS_CONTROL_ALLOWED_REDIRECT_URLS
AllowedUrls []string `json:"allowedUrls,omitempty"` AllowedUrls []string `json:"allowedUrls,omitempty"`
ClientURL *string `json:"clientUrl,omitempty"` // AUTH_CLIENT_URL
ClientURL *string `json:"clientUrl,omitempty"`
} }
type ConfigAuthRedirectionsUpdateInput struct { type ConfigAuthRedirectionsUpdateInput struct {
@@ -350,8 +364,10 @@ type ConfigAuthSession struct {
} }
type ConfigAuthSessionAccessToken struct { type ConfigAuthSessionAccessToken struct {
// AUTH_JWT_CUSTOM_CLAIMS
CustomClaims []*ConfigAuthsessionaccessTokenCustomClaims `json:"customClaims,omitempty"` CustomClaims []*ConfigAuthsessionaccessTokenCustomClaims `json:"customClaims,omitempty"`
ExpiresIn *uint32 `json:"expiresIn,omitempty"` // AUTH_ACCESS_TOKEN_EXPIRES_IN
ExpiresIn *uint32 `json:"expiresIn,omitempty"`
} }
type ConfigAuthSessionAccessTokenUpdateInput struct { type ConfigAuthSessionAccessTokenUpdateInput struct {
@@ -360,6 +376,7 @@ type ConfigAuthSessionAccessTokenUpdateInput struct {
} }
type ConfigAuthSessionRefreshToken struct { type ConfigAuthSessionRefreshToken struct {
// AUTH_REFRESH_TOKEN_EXPIRES_IN
ExpiresIn *uint32 `json:"expiresIn,omitempty"` ExpiresIn *uint32 `json:"expiresIn,omitempty"`
} }
@@ -373,9 +390,11 @@ type ConfigAuthSessionUpdateInput struct {
} }
type ConfigAuthSignUp struct { type ConfigAuthSignUp struct {
DisableNewUsers *bool `json:"disableNewUsers,omitempty"` // AUTH_DISABLE_NEW_USERS
Enabled *bool `json:"enabled,omitempty"` DisableNewUsers *bool `json:"disableNewUsers,omitempty"`
Turnstile *ConfigAuthSignUpTurnstile `json:"turnstile,omitempty"` // Inverse of AUTH_DISABLE_SIGNUP
Enabled *bool `json:"enabled,omitempty"`
Turnstile *ConfigAuthSignUpTurnstile `json:"turnstile,omitempty"`
} }
type ConfigAuthSignUpTurnstile struct { type ConfigAuthSignUpTurnstile struct {
@@ -425,12 +444,16 @@ type ConfigAuthUser struct {
} }
type ConfigAuthUserEmail struct { type ConfigAuthUserEmail struct {
// AUTH_ACCESS_CONTROL_ALLOWED_EMAILS
Allowed []string `json:"allowed,omitempty"` Allowed []string `json:"allowed,omitempty"`
// AUTH_ACCESS_CONTROL_BLOCKED_EMAILS
Blocked []string `json:"blocked,omitempty"` Blocked []string `json:"blocked,omitempty"`
} }
type ConfigAuthUserEmailDomains struct { type ConfigAuthUserEmailDomains struct {
// AUTH_ACCESS_CONTROL_ALLOWED_EMAIL_DOMAINS
Allowed []string `json:"allowed,omitempty"` Allowed []string `json:"allowed,omitempty"`
// AUTH_ACCESS_CONTROL_BLOCKED_EMAIL_DOMAINS
Blocked []string `json:"blocked,omitempty"` Blocked []string `json:"blocked,omitempty"`
} }
@@ -446,6 +469,7 @@ type ConfigAuthUserEmailUpdateInput struct {
type ConfigAuthUserGravatar struct { type ConfigAuthUserGravatar struct {
Default *string `json:"default,omitempty"` Default *string `json:"default,omitempty"`
// AUTH_GRAVATAR_ENABLED
Enabled *bool `json:"enabled,omitempty"` Enabled *bool `json:"enabled,omitempty"`
Rating *string `json:"rating,omitempty"` Rating *string `json:"rating,omitempty"`
} }
@@ -457,8 +481,10 @@ type ConfigAuthUserGravatarUpdateInput struct {
} }
type ConfigAuthUserLocale struct { type ConfigAuthUserLocale struct {
// AUTH_LOCALE_ALLOWED_LOCALES
Allowed []string `json:"allowed,omitempty"` Allowed []string `json:"allowed,omitempty"`
Default *string `json:"default,omitempty"` // AUTH_LOCALE_DEFAULT
Default *string `json:"default,omitempty"`
} }
type ConfigAuthUserLocaleUpdateInput struct { type ConfigAuthUserLocaleUpdateInput struct {
@@ -467,8 +493,10 @@ type ConfigAuthUserLocaleUpdateInput struct {
} }
type ConfigAuthUserRoles struct { type ConfigAuthUserRoles struct {
// AUTH_USER_DEFAULT_ALLOWED_ROLES
Allowed []string `json:"allowed,omitempty"` Allowed []string `json:"allowed,omitempty"`
Default *string `json:"default,omitempty"` // AUTH_USER_DEFAULT_ROLE
Default *string `json:"default,omitempty"`
} }
type ConfigAuthUserRolesUpdateInput struct { type ConfigAuthUserRolesUpdateInput struct {
@@ -484,6 +512,7 @@ type ConfigAuthUserUpdateInput struct {
Roles *ConfigAuthUserRolesUpdateInput `json:"roles,omitempty"` Roles *ConfigAuthUserRolesUpdateInput `json:"roles,omitempty"`
} }
// AUTH_JWT_CUSTOM_CLAIMS
type ConfigAuthsessionaccessTokenCustomClaims struct { type ConfigAuthsessionaccessTokenCustomClaims struct {
Default *string `json:"default,omitempty"` Default *string `json:"default,omitempty"`
Key string `json:"key"` Key string `json:"key"`
@@ -522,8 +551,11 @@ type ConfigClaimMapUpdateInput struct {
Value *string `json:"value,omitempty"` Value *string `json:"value,omitempty"`
} }
// Resource configuration for a service
type ConfigComputeResources struct { type ConfigComputeResources struct {
CPU uint32 `json:"cpu"` // milicpus, 1000 milicpus = 1 cpu
CPU uint32 `json:"cpu"`
// MiB: 128MiB to 30GiB
Memory uint32 `json:"memory"` Memory uint32 `json:"memory"`
} }
@@ -537,17 +569,28 @@ type ConfigComputeResourcesUpdateInput struct {
Memory *uint32 `json:"memory,omitempty"` Memory *uint32 `json:"memory,omitempty"`
} }
// main entrypoint to the configuration
type ConfigConfig struct { type ConfigConfig struct {
Ai *ConfigAi `json:"ai,omitempty"` // Configuration for graphite service
Auth *ConfigAuth `json:"auth,omitempty"` Ai *ConfigAi `json:"ai,omitempty"`
Functions *ConfigFunctions `json:"functions,omitempty"` // Configuration for auth service
Global *ConfigGlobal `json:"global,omitempty"` Auth *ConfigAuth `json:"auth,omitempty"`
Graphql *ConfigGraphql `json:"graphql,omitempty"` // Configuration for functions service
Hasura *ConfigHasura `json:"hasura"` Functions *ConfigFunctions `json:"functions,omitempty"`
// Global configuration that applies to all services
Global *ConfigGlobal `json:"global,omitempty"`
// Advanced configuration for GraphQL
Graphql *ConfigGraphql `json:"graphql,omitempty"`
// Configuration for hasura
Hasura *ConfigHasura `json:"hasura"`
// Configuration for observability service
Observability *ConfigObservability `json:"observability"` Observability *ConfigObservability `json:"observability"`
Postgres *ConfigPostgres `json:"postgres"` // Configuration for postgres service
Provider *ConfigProvider `json:"provider,omitempty"` Postgres *ConfigPostgres `json:"postgres"`
Storage *ConfigStorage `json:"storage,omitempty"` // Configuration for third party providers like SMTP, SMS, etc.
Provider *ConfigProvider `json:"provider,omitempty"`
// Configuration for storage service
Storage *ConfigStorage `json:"storage,omitempty"`
} }
type ConfigConfigUpdateInput struct { type ConfigConfigUpdateInput struct {
@@ -564,7 +607,8 @@ type ConfigConfigUpdateInput struct {
} }
type ConfigEnvironmentVariable struct { type ConfigEnvironmentVariable struct {
Name string `json:"name"` Name string `json:"name"`
// Value of the environment variable
Value string `json:"value"` Value string `json:"value"`
} }
@@ -578,6 +622,7 @@ type ConfigEnvironmentVariableUpdateInput struct {
Value *string `json:"value,omitempty"` Value *string `json:"value,omitempty"`
} }
// Configuration for functions service
type ConfigFunctions struct { type ConfigFunctions struct {
Node *ConfigFunctionsNode `json:"node,omitempty"` Node *ConfigFunctionsNode `json:"node,omitempty"`
RateLimit *ConfigRateLimit `json:"rateLimit,omitempty"` RateLimit *ConfigRateLimit `json:"rateLimit,omitempty"`
@@ -606,12 +651,15 @@ type ConfigFunctionsUpdateInput struct {
Resources *ConfigFunctionsResourcesUpdateInput `json:"resources,omitempty"` Resources *ConfigFunctionsResourcesUpdateInput `json:"resources,omitempty"`
} }
// Global configuration that applies to all services
type ConfigGlobal struct { type ConfigGlobal struct {
// User-defined environment variables that are spread over all services
Environment []*ConfigGlobalEnvironmentVariable `json:"environment,omitempty"` Environment []*ConfigGlobalEnvironmentVariable `json:"environment,omitempty"`
} }
type ConfigGlobalEnvironmentVariable struct { type ConfigGlobalEnvironmentVariable struct {
Name string `json:"name"` Name string `json:"name"`
// Value of the environment variable
Value string `json:"value"` Value string `json:"value"`
} }
@@ -768,23 +816,34 @@ type ConfigGraphqlUpdateInput struct {
Security *ConfigGraphqlSecurityUpdateInput `json:"security,omitempty"` Security *ConfigGraphqlSecurityUpdateInput `json:"security,omitempty"`
} }
// Configuration for hasura service
type ConfigHasura struct { type ConfigHasura struct {
AdminSecret string `json:"adminSecret"` // Admin secret
AuthHook *ConfigHasuraAuthHook `json:"authHook,omitempty"` AdminSecret string `json:"adminSecret"`
Events *ConfigHasuraEvents `json:"events,omitempty"` AuthHook *ConfigHasuraAuthHook `json:"authHook,omitempty"`
JwtSecrets []*ConfigJWTSecret `json:"jwtSecrets,omitempty"` Events *ConfigHasuraEvents `json:"events,omitempty"`
Logs *ConfigHasuraLogs `json:"logs,omitempty"` // JWT Secrets configuration
RateLimit *ConfigRateLimit `json:"rateLimit,omitempty"` JwtSecrets []*ConfigJWTSecret `json:"jwtSecrets,omitempty"`
Resources *ConfigResources `json:"resources,omitempty"` Logs *ConfigHasuraLogs `json:"logs,omitempty"`
Settings *ConfigHasuraSettings `json:"settings,omitempty"` RateLimit *ConfigRateLimit `json:"rateLimit,omitempty"`
Version *string `json:"version,omitempty"` // Resources for the service
WebhookSecret string `json:"webhookSecret"` Resources *ConfigResources `json:"resources,omitempty"`
// Configuration for hasura services
// Reference: https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/
Settings *ConfigHasuraSettings `json:"settings,omitempty"`
// Version of hasura, you can see available versions in the URL below:
// https://hub.docker.com/r/hasura/graphql-engine/tags
Version *string `json:"version,omitempty"`
// Webhook secret
WebhookSecret string `json:"webhookSecret"`
} }
type ConfigHasuraAuthHook struct { type ConfigHasuraAuthHook struct {
Mode *string `json:"mode,omitempty"` Mode *string `json:"mode,omitempty"`
SendRequestBody *bool `json:"sendRequestBody,omitempty"` // HASURA_GRAPHQL_AUTH_HOOK_SEND_REQUEST_BODY
URL string `json:"url"` SendRequestBody *bool `json:"sendRequestBody,omitempty"`
// HASURA_GRAPHQL_AUTH_HOOK
URL string `json:"url"`
} }
type ConfigHasuraAuthHookUpdateInput struct { type ConfigHasuraAuthHookUpdateInput struct {
@@ -794,6 +853,7 @@ type ConfigHasuraAuthHookUpdateInput struct {
} }
type ConfigHasuraEvents struct { type ConfigHasuraEvents struct {
// HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE
HTTPPoolSize *uint32 `json:"httpPoolSize,omitempty"` HTTPPoolSize *uint32 `json:"httpPoolSize,omitempty"`
} }
@@ -809,16 +869,27 @@ type ConfigHasuraLogsUpdateInput struct {
Level *string `json:"level,omitempty"` Level *string `json:"level,omitempty"`
} }
// Configuration for hasura services
// Reference: https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/
type ConfigHasuraSettings struct { type ConfigHasuraSettings struct {
CorsDomain []string `json:"corsDomain,omitempty"` // HASURA_GRAPHQL_CORS_DOMAIN
DevMode *bool `json:"devMode,omitempty"` CorsDomain []string `json:"corsDomain,omitempty"`
EnableAllowList *bool `json:"enableAllowList,omitempty"` // HASURA_GRAPHQL_DEV_MODE
EnableConsole *bool `json:"enableConsole,omitempty"` DevMode *bool `json:"devMode,omitempty"`
EnableRemoteSchemaPermissions *bool `json:"enableRemoteSchemaPermissions,omitempty"` // HASURA_GRAPHQL_ENABLE_ALLOWLIST
EnabledAPIs []string `json:"enabledAPIs,omitempty"` EnableAllowList *bool `json:"enableAllowList,omitempty"`
InferFunctionPermissions *bool `json:"inferFunctionPermissions,omitempty"` // HASURA_GRAPHQL_ENABLE_CONSOLE
LiveQueriesMultiplexedRefetchInterval *uint32 `json:"liveQueriesMultiplexedRefetchInterval,omitempty"` EnableConsole *bool `json:"enableConsole,omitempty"`
StringifyNumericTypes *bool `json:"stringifyNumericTypes,omitempty"` // HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS
EnableRemoteSchemaPermissions *bool `json:"enableRemoteSchemaPermissions,omitempty"`
// HASURA_GRAPHQL_ENABLED_APIS
EnabledAPIs []string `json:"enabledAPIs,omitempty"`
// HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS
InferFunctionPermissions *bool `json:"inferFunctionPermissions,omitempty"`
// HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL
LiveQueriesMultiplexedRefetchInterval *uint32 `json:"liveQueriesMultiplexedRefetchInterval,omitempty"`
// HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES
StringifyNumericTypes *bool `json:"stringifyNumericTypes,omitempty"`
} }
type ConfigHasuraSettingsUpdateInput struct { type ConfigHasuraSettingsUpdateInput struct {
@@ -891,6 +962,7 @@ type ConfigIngressUpdateInput struct {
TLS *ConfigIngressTLSUpdateInput `json:"tls,omitempty"` TLS *ConfigIngressTLSUpdateInput `json:"tls,omitempty"`
} }
// See https://hasura.io/docs/latest/auth/authentication/jwt/
type ConfigJWTSecret struct { type ConfigJWTSecret struct {
AllowedSkew *uint32 `json:"allowed_skew,omitempty"` AllowedSkew *uint32 `json:"allowed_skew,omitempty"`
Audience *string `json:"audience,omitempty"` Audience *string `json:"audience,omitempty"`
@@ -939,11 +1011,15 @@ type ConfigObservabilityUpdateInput struct {
Grafana *ConfigGrafanaUpdateInput `json:"grafana,omitempty"` Grafana *ConfigGrafanaUpdateInput `json:"grafana,omitempty"`
} }
// Configuration for postgres service
type ConfigPostgres struct { type ConfigPostgres struct {
Pitr *ConfigPostgresPitr `json:"pitr,omitempty"` Pitr *ConfigPostgresPitr `json:"pitr,omitempty"`
// Resources for the service
Resources *ConfigPostgresResources `json:"resources"` Resources *ConfigPostgresResources `json:"resources"`
Settings *ConfigPostgresSettings `json:"settings,omitempty"` Settings *ConfigPostgresSettings `json:"settings,omitempty"`
Version *string `json:"version,omitempty"` // Version of postgres, you can see available versions in the URL below:
// https://hub.docker.com/r/nhost/postgres/tags
Version *string `json:"version,omitempty"`
} }
type ConfigPostgresPitr struct { type ConfigPostgresPitr struct {
@@ -954,6 +1030,7 @@ type ConfigPostgresPitrUpdateInput struct {
Retention *uint32 `json:"retention,omitempty"` Retention *uint32 `json:"retention,omitempty"`
} }
// Resources for the service
type ConfigPostgresResources struct { type ConfigPostgresResources struct {
Compute *ConfigResourcesCompute `json:"compute,omitempty"` Compute *ConfigResourcesCompute `json:"compute,omitempty"`
EnablePublicAccess *bool `json:"enablePublicAccess,omitempty"` EnablePublicAccess *bool `json:"enablePublicAccess,omitempty"`
@@ -1060,15 +1137,19 @@ type ConfigRateLimitUpdateInput struct {
Limit *uint32 `json:"limit,omitempty"` Limit *uint32 `json:"limit,omitempty"`
} }
// Resource configuration for a service
type ConfigResources struct { type ConfigResources struct {
Autoscaler *ConfigAutoscaler `json:"autoscaler,omitempty"` Autoscaler *ConfigAutoscaler `json:"autoscaler,omitempty"`
Compute *ConfigResourcesCompute `json:"compute,omitempty"` Compute *ConfigResourcesCompute `json:"compute,omitempty"`
Networking *ConfigNetworking `json:"networking,omitempty"` Networking *ConfigNetworking `json:"networking,omitempty"`
Replicas *uint32 `json:"replicas,omitempty"` // Number of replicas for a service
Replicas *uint32 `json:"replicas,omitempty"`
} }
type ConfigResourcesCompute struct { type ConfigResourcesCompute struct {
CPU uint32 `json:"cpu"` // milicpus, 1000 milicpus = 1 cpu
CPU uint32 `json:"cpu"`
// MiB: 128MiB to 30GiB
Memory uint32 `json:"memory"` Memory uint32 `json:"memory"`
} }
@@ -1120,7 +1201,8 @@ type ConfigRunServiceConfigWithID struct {
} }
type ConfigRunServiceImage struct { type ConfigRunServiceImage struct {
Image string `json:"image"` Image string `json:"image"`
// content of "auths", i.e., { "auths": $THIS }
PullCredentials *string `json:"pullCredentials,omitempty"` PullCredentials *string `json:"pullCredentials,omitempty"`
} }
@@ -1158,11 +1240,13 @@ type ConfigRunServicePortUpdateInput struct {
Type *string `json:"type,omitempty"` Type *string `json:"type,omitempty"`
} }
// Resource configuration for a service
type ConfigRunServiceResources struct { type ConfigRunServiceResources struct {
Autoscaler *ConfigAutoscaler `json:"autoscaler,omitempty"` Autoscaler *ConfigAutoscaler `json:"autoscaler,omitempty"`
Compute *ConfigComputeResources `json:"compute"` Compute *ConfigComputeResources `json:"compute"`
Replicas uint32 `json:"replicas"` // Number of replicas for a service
Storage []*ConfigRunServiceResourcesStorage `json:"storage,omitempty"` Replicas uint32 `json:"replicas"`
Storage []*ConfigRunServiceResourcesStorage `json:"storage,omitempty"`
} }
type ConfigRunServiceResourcesInsertInput struct { type ConfigRunServiceResourcesInsertInput struct {
@@ -1173,9 +1257,11 @@ type ConfigRunServiceResourcesInsertInput struct {
} }
type ConfigRunServiceResourcesStorage struct { type ConfigRunServiceResourcesStorage struct {
// GiB
Capacity uint32 `json:"capacity"` Capacity uint32 `json:"capacity"`
Name string `json:"name"` // name of the volume, changing it will cause data loss
Path string `json:"path"` Name string `json:"name"`
Path string `json:"path"`
} }
type ConfigRunServiceResourcesStorageInsertInput struct { type ConfigRunServiceResourcesStorageInsertInput struct {
@@ -1259,11 +1345,20 @@ type ConfigStandardOauthProviderWithScopeUpdateInput struct {
Scope []string `json:"scope,omitempty"` Scope []string `json:"scope,omitempty"`
} }
// Configuration for storage service
type ConfigStorage struct { type ConfigStorage struct {
Antivirus *ConfigStorageAntivirus `json:"antivirus,omitempty"` Antivirus *ConfigStorageAntivirus `json:"antivirus,omitempty"`
RateLimit *ConfigRateLimit `json:"rateLimit,omitempty"` RateLimit *ConfigRateLimit `json:"rateLimit,omitempty"`
Resources *ConfigResources `json:"resources,omitempty"` // Networking (custom domains at the moment) are not allowed as we need to do further
Version *string `json:"version,omitempty"` // configurations in the CDN. We will enable it again in the future.
Resources *ConfigResources `json:"resources,omitempty"`
// Version of storage service, you can see available versions in the URL below:
// https://hub.docker.com/r/nhost/hasura-storage/tags
//
// Releases:
//
// https://github.com/nhost/hasura-storage/releases
Version *string `json:"version,omitempty"`
} }
type ConfigStorageAntivirus struct { type ConfigStorageAntivirus struct {
@@ -1301,6 +1396,8 @@ type ConfigSystemConfigAuthEmailTemplates struct {
} }
type ConfigSystemConfigGraphql struct { type ConfigSystemConfigGraphql struct {
// manually enable graphi on a per-service basis
// by default it follows the plan
FeatureAdvancedGraphql *bool `json:"featureAdvancedGraphql,omitempty"` FeatureAdvancedGraphql *bool `json:"featureAdvancedGraphql,omitempty"`
} }
@@ -1718,7 +1815,8 @@ type Apps struct {
AppStates []*AppStateHistory `json:"appStates"` AppStates []*AppStateHistory `json:"appStates"`
AutomaticDeploys bool `json:"automaticDeploys"` AutomaticDeploys bool `json:"automaticDeploys"`
// An array relationship // An array relationship
Backups []*Backups `json:"backups"` Backups []*Backups `json:"backups"`
// main entrypoint to the configuration
Config *ConfigConfig `json:"config,omitempty"` Config *ConfigConfig `json:"config,omitempty"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
// An object relationship // An object relationship
@@ -2725,6 +2823,7 @@ type Deployments struct {
CommitSha string `json:"commitSHA"` CommitSha string `json:"commitSHA"`
CommitUserAvatarURL *string `json:"commitUserAvatarUrl,omitempty"` CommitUserAvatarURL *string `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *string `json:"commitUserName,omitempty"` CommitUserName *string `json:"commitUserName,omitempty"`
CreatedAt time.Time `json:"createdAt"`
DeploymentEndedAt *time.Time `json:"deploymentEndedAt,omitempty"` DeploymentEndedAt *time.Time `json:"deploymentEndedAt,omitempty"`
// An array relationship // An array relationship
DeploymentLogs []*DeploymentLogs `json:"deploymentLogs"` DeploymentLogs []*DeploymentLogs `json:"deploymentLogs"`
@@ -2767,6 +2866,7 @@ type DeploymentsBoolExp struct {
CommitSha *StringComparisonExp `json:"commitSHA,omitempty"` CommitSha *StringComparisonExp `json:"commitSHA,omitempty"`
CommitUserAvatarURL *StringComparisonExp `json:"commitUserAvatarUrl,omitempty"` CommitUserAvatarURL *StringComparisonExp `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *StringComparisonExp `json:"commitUserName,omitempty"` CommitUserName *StringComparisonExp `json:"commitUserName,omitempty"`
CreatedAt *TimestamptzComparisonExp `json:"createdAt,omitempty"`
DeploymentEndedAt *TimestamptzComparisonExp `json:"deploymentEndedAt,omitempty"` DeploymentEndedAt *TimestamptzComparisonExp `json:"deploymentEndedAt,omitempty"`
DeploymentLogs *DeploymentLogsBoolExp `json:"deploymentLogs,omitempty"` DeploymentLogs *DeploymentLogsBoolExp `json:"deploymentLogs,omitempty"`
DeploymentStartedAt *TimestamptzComparisonExp `json:"deploymentStartedAt,omitempty"` DeploymentStartedAt *TimestamptzComparisonExp `json:"deploymentStartedAt,omitempty"`
@@ -2801,6 +2901,7 @@ type DeploymentsMaxOrderBy struct {
CommitSha *OrderBy `json:"commitSHA,omitempty"` CommitSha *OrderBy `json:"commitSHA,omitempty"`
CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"` CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *OrderBy `json:"commitUserName,omitempty"` CommitUserName *OrderBy `json:"commitUserName,omitempty"`
CreatedAt *OrderBy `json:"createdAt,omitempty"`
DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"` DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"`
DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"` DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"`
DeploymentStatus *OrderBy `json:"deploymentStatus,omitempty"` DeploymentStatus *OrderBy `json:"deploymentStatus,omitempty"`
@@ -2823,6 +2924,7 @@ type DeploymentsMinOrderBy struct {
CommitSha *OrderBy `json:"commitSHA,omitempty"` CommitSha *OrderBy `json:"commitSHA,omitempty"`
CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"` CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *OrderBy `json:"commitUserName,omitempty"` CommitUserName *OrderBy `json:"commitUserName,omitempty"`
CreatedAt *OrderBy `json:"createdAt,omitempty"`
DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"` DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"`
DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"` DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"`
DeploymentStatus *OrderBy `json:"deploymentStatus,omitempty"` DeploymentStatus *OrderBy `json:"deploymentStatus,omitempty"`
@@ -2861,6 +2963,7 @@ type DeploymentsOrderBy struct {
CommitSha *OrderBy `json:"commitSHA,omitempty"` CommitSha *OrderBy `json:"commitSHA,omitempty"`
CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"` CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *OrderBy `json:"commitUserName,omitempty"` CommitUserName *OrderBy `json:"commitUserName,omitempty"`
CreatedAt *OrderBy `json:"createdAt,omitempty"`
DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"` DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"`
DeploymentLogsAggregate *DeploymentLogsAggregateOrderBy `json:"deploymentLogs_aggregate,omitempty"` DeploymentLogsAggregate *DeploymentLogsAggregateOrderBy `json:"deploymentLogs_aggregate,omitempty"`
DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"` DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"`
@@ -2892,6 +2995,7 @@ type DeploymentsStreamCursorValueInput struct {
CommitSha *string `json:"commitSHA,omitempty"` CommitSha *string `json:"commitSHA,omitempty"`
CommitUserAvatarURL *string `json:"commitUserAvatarUrl,omitempty"` CommitUserAvatarURL *string `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *string `json:"commitUserName,omitempty"` CommitUserName *string `json:"commitUserName,omitempty"`
CreatedAt *time.Time `json:"createdAt,omitempty"`
DeploymentEndedAt *time.Time `json:"deploymentEndedAt,omitempty"` DeploymentEndedAt *time.Time `json:"deploymentEndedAt,omitempty"`
DeploymentStartedAt *time.Time `json:"deploymentStartedAt,omitempty"` DeploymentStartedAt *time.Time `json:"deploymentStartedAt,omitempty"`
DeploymentStatus *string `json:"deploymentStatus,omitempty"` DeploymentStatus *string `json:"deploymentStatus,omitempty"`
@@ -6885,6 +6989,8 @@ const (
// column name // column name
DeploymentsSelectColumnCommitUserName DeploymentsSelectColumn = "commitUserName" DeploymentsSelectColumnCommitUserName DeploymentsSelectColumn = "commitUserName"
// column name // column name
DeploymentsSelectColumnCreatedAt DeploymentsSelectColumn = "createdAt"
// column name
DeploymentsSelectColumnDeploymentEndedAt DeploymentsSelectColumn = "deploymentEndedAt" DeploymentsSelectColumnDeploymentEndedAt DeploymentsSelectColumn = "deploymentEndedAt"
// column name // column name
DeploymentsSelectColumnDeploymentStartedAt DeploymentsSelectColumn = "deploymentStartedAt" DeploymentsSelectColumnDeploymentStartedAt DeploymentsSelectColumn = "deploymentStartedAt"
@@ -6918,6 +7024,7 @@ var AllDeploymentsSelectColumn = []DeploymentsSelectColumn{
DeploymentsSelectColumnCommitSha, DeploymentsSelectColumnCommitSha,
DeploymentsSelectColumnCommitUserAvatarURL, DeploymentsSelectColumnCommitUserAvatarURL,
DeploymentsSelectColumnCommitUserName, DeploymentsSelectColumnCommitUserName,
DeploymentsSelectColumnCreatedAt,
DeploymentsSelectColumnDeploymentEndedAt, DeploymentsSelectColumnDeploymentEndedAt,
DeploymentsSelectColumnDeploymentStartedAt, DeploymentsSelectColumnDeploymentStartedAt,
DeploymentsSelectColumnDeploymentStatus, DeploymentsSelectColumnDeploymentStatus,
@@ -6935,7 +7042,7 @@ var AllDeploymentsSelectColumn = []DeploymentsSelectColumn{
func (e DeploymentsSelectColumn) IsValid() bool { func (e DeploymentsSelectColumn) IsValid() bool {
switch e { switch e {
case DeploymentsSelectColumnAppID, DeploymentsSelectColumnCommitMessage, DeploymentsSelectColumnCommitSha, DeploymentsSelectColumnCommitUserAvatarURL, DeploymentsSelectColumnCommitUserName, DeploymentsSelectColumnDeploymentEndedAt, DeploymentsSelectColumnDeploymentStartedAt, DeploymentsSelectColumnDeploymentStatus, DeploymentsSelectColumnFunctionsEndedAt, DeploymentsSelectColumnFunctionsStartedAt, DeploymentsSelectColumnFunctionsStatus, DeploymentsSelectColumnID, DeploymentsSelectColumnMetadataEndedAt, DeploymentsSelectColumnMetadataStartedAt, DeploymentsSelectColumnMetadataStatus, DeploymentsSelectColumnMigrationsEndedAt, DeploymentsSelectColumnMigrationsStartedAt, DeploymentsSelectColumnMigrationsStatus: case DeploymentsSelectColumnAppID, DeploymentsSelectColumnCommitMessage, DeploymentsSelectColumnCommitSha, DeploymentsSelectColumnCommitUserAvatarURL, DeploymentsSelectColumnCommitUserName, DeploymentsSelectColumnCreatedAt, DeploymentsSelectColumnDeploymentEndedAt, DeploymentsSelectColumnDeploymentStartedAt, DeploymentsSelectColumnDeploymentStatus, DeploymentsSelectColumnFunctionsEndedAt, DeploymentsSelectColumnFunctionsStartedAt, DeploymentsSelectColumnFunctionsStatus, DeploymentsSelectColumnID, DeploymentsSelectColumnMetadataEndedAt, DeploymentsSelectColumnMetadataStartedAt, DeploymentsSelectColumnMetadataStatus, DeploymentsSelectColumnMigrationsEndedAt, DeploymentsSelectColumnMigrationsStartedAt, DeploymentsSelectColumnMigrationsStatus:
return true return true
} }
return false return false

View File

@@ -3,12 +3,13 @@ NEXT_PUBLIC_ENV=dev
NEXT_PUBLIC_NHOST_PLATFORM=false NEXT_PUBLIC_NHOST_PLATFORM=false
# Environment Variables for Self Hosting and Local Development # Environment Variables for Self Hosting and Local Development
NEXT_PUBLIC_NHOST_AUTH_URL=https://local.auth.nhost.local.run/v1 NEXT_PUBLIC_NHOST_AUTH_URL=https://local.auth.local.nhost.run/v1
NEXT_PUBLIC_NHOST_CONFIGSERVER_URL=https://local.dashboard.local.nhost.run/v1/configserver/graphql
NEXT_PUBLIC_NHOST_FUNCTIONS_URL=https://local.functions.local.nhost.run/v1 NEXT_PUBLIC_NHOST_FUNCTIONS_URL=https://local.functions.local.nhost.run/v1
NEXT_PUBLIC_NHOST_GRAPHQL_URL=https://local.graphql.local.nhost.run/v1 NEXT_PUBLIC_NHOST_GRAPHQL_URL=https://local.graphql.local.nhost.run/v1
NEXT_PUBLIC_NHOST_STORAGE_URL=https://local.storage.local.nhost.run/v1 NEXT_PUBLIC_NHOST_STORAGE_URL=https://local.storage.local.nhost.run/v1
NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL=https://local.hasura.local.nhost.run NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL=https://local.hasura.local.nhost.run/console
NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL=https://local.hasura.local.nhost.run/v1/migrations NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL=https://local.hasura.local.nhost.run/apis/migrate
NEXT_PUBLIC_NHOST_HASURA_API_URL=https://local.hasura.local.nhost.run NEXT_PUBLIC_NHOST_HASURA_API_URL=https://local.hasura.local.nhost.run
# Environment Variables when running the Nhost Dashboard against the Nhost Backend # Environment Variables when running the Nhost Dashboard against the Nhost Backend
@@ -18,13 +19,13 @@ NEXT_PUBLIC_ANALYTICS_WRITE_KEY=<analytics_write_key>
NEXT_PUBLIC_SEGMENT_CDN_URL=<segment_cdn_url> NEXT_PUBLIC_SEGMENT_CDN_URL=<segment_cdn_url>
NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET=<nhost_bragi_websocket> NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET=<nhost_bragi_websocket>
NEXT_PUBLIC_ZENDESK_URL= NEXT_ZENDESK_URL=
NEXT_PUBLIC_ZENDESK_API_KEY= NEXT_ZENDESK_API_KEY=
NEXT_PUBLIC_ZENDESK_USER_EMAIL= NEXT_ZENDESK_USER_EMAIL=
CODEGEN_GRAPHQL_URL=https://local.graphql.local.nhost.run/v1 CODEGEN_GRAPHQL_URL=https://local.graphql.local.nhost.run/v1
CODEGEN_HASURA_ADMIN_SECRET=nhost-admin-secret CODEGEN_HASURA_ADMIN_SECRET=nhost-admin-secret
NEXT_PUBLIC_TURNSTILE_SITE_KEY=FIXME NEXT_PUBLIC_TURNSTILE_SITE_KEY=FIXME
NEXT_PUBLIC_SOC2_REPORT_FILE_ID= NEXT_PUBLIC_SOC2_REPORT_FILE_ID=

View File

@@ -1,7 +1,47 @@
## [@nhost/dashboard@2.41.0] - 2025-11-04
### 🚀 Features
- *(auth)* Added endpoints to retrieve and refresh oauth2 providers' tokens (#3614)
- *(dashboard)* Get github repositories from github itself (#3640)
### 🐛 Bug Fixes
- *(dashboard)* Update SQL editor to use correct hasura migrations API URL (#3645)
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [@nhost/dashboard@2.40.0] - 2025-10-27
### 🚀 Features
- *(dashboard)* Allow configuring CSP header (#3627)
### ⚙️ Miscellaneous Tasks
- *(dashboard)* Various improvements to support ticket page (#3630)
## [@nhost/dashboard@2.39.0] - 2025-10-22
### 🚀 Features
- *(dashboard)* Move zendesk request to API route (#3628)
### 🐛 Bug Fixes
- *(dashboard)* Fix flaky e2e tests (#3536)
- *(dashboard)* Run audit and lint in dashboard (#3578)
### ⚙️ Miscellaneous Tasks
- *(dashboard)* Cleanup e2e remote schemas test before run (#3581)
## [@nhost/dashboard@2.38.4] - 2025-10-09 ## [@nhost/dashboard@2.38.4] - 2025-10-09
### 🐛 Bug Fixes ### 🐛 Bug Fixes

View File

@@ -82,6 +82,15 @@ This will connect the Nhost Dashboard to your locally running Nhost backend.
| `NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL` | The URL of Hasura's Migrations service. When working locally, point it to the Migrations service started by the CLI. | | `NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL` | The URL of Hasura's Migrations service. When working locally, point it to the Migrations service started by the CLI. |
| `NEXT_PUBLIC_NHOST_HASURA_API_URL` | The URL of Hasura's Schema and Metadata API. When working locally, point it to the Schema and Metadata API started by the CLI. When self-hosting, point it to the self-hosted Schema and Metadata API. | | `NEXT_PUBLIC_NHOST_HASURA_API_URL` | The URL of Hasura's Schema and Metadata API. When working locally, point it to the Schema and Metadata API started by the CLI. When self-hosting, point it to the self-hosted Schema and Metadata API. |
### Content Security Policy (CSP) Configuration
The dashboard supports build-time CSP configuration to enable self-hosted deployments on custom domains.
| Name | Description |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CSP_MODE` | Controls CSP behavior. Options: `nhost` (default, uses Nhost Cloud CSP), `disabled` (no CSP headers), `custom` (use custom CSP via `CSP_HEADER`). For self-hosted deployments on custom domains, set to `disabled` or `custom`. |
| `CSP_HEADER` | Custom Content Security Policy header value. Only used when `CSP_MODE=custom`. Should be a complete CSP string (e.g., `default-src 'self'; script-src 'self' 'unsafe-eval'; ...`). |
### Other Environment Variables ### Other Environment Variables
| Name | Description | | Name | Description |

View File

@@ -4,21 +4,31 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
}); });
const { version } = require('./package.json'); const { version } = require('./package.json');
const cspHeader = ` function getCspHeader() {
default-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run; switch (process.env.CSP_MODE) {
script-src 'self' 'unsafe-eval' cdn.segment.com js.stripe.com challenges.cloudflare.com googletagmanager.com; case 'disabled':
connect-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run discord.com api.segment.io api.segment.com cdn.segment.com nhost.zendesk.com; return null;
style-src 'self' 'unsafe-inline'; case 'custom':
img-src 'self' blob: data: github.com avatars.githubusercontent.com s.gravatar.com *.nhost.run nhost.run; return process.env.CSP_HEADER || null;
font-src 'self' data:; case 'nhost':
object-src 'none'; default:
base-uri 'self'; return [
form-action 'self'; "default-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run",
frame-ancestors 'none'; "script-src 'self' 'unsafe-eval' cdn.segment.com js.stripe.com challenges.cloudflare.com googletagmanager.com",
frame-src 'self' js.stripe.com challenges.cloudflare.com; "connect-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run discord.com api.segment.io api.segment.com cdn.segment.com nhost.zendesk.com api.github.com",
block-all-mixed-content; "style-src 'self' 'unsafe-inline'",
upgrade-insecure-requests; "img-src 'self' blob: data: github.com avatars.githubusercontent.com s.gravatar.com *.nhost.run nhost.run",
`; "font-src 'self' data:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"frame-src 'self' js.stripe.com challenges.cloudflare.com",
"block-all-mixed-content",
"upgrade-insecure-requests",
].join('; ') + ';';
}
}
module.exports = withBundleAnalyzer({ module.exports = withBundleAnalyzer({
reactStrictMode: false, reactStrictMode: false,
@@ -34,13 +44,19 @@ module.exports = withBundleAnalyzer({
dirs: ['src'], dirs: ['src'],
}, },
async headers() { async headers() {
const cspHeader = getCspHeader();
if (!cspHeader) {
return []; // No CSP headers
}
return [ return [
{ {
source: '/(.*)', source: '/:path*',
headers: [ headers: [
{ {
key: 'Content-Security-Policy', key: 'Content-Security-Policy',
value: cspHeader.replace(/\s+/g, ' ').trim(), value: cspHeader,
}, },
{ {
key: 'X-Frame-Options', key: 'X-Frame-Options',
@@ -110,4 +126,4 @@ module.exports = withBundleAnalyzer({
}, },
]; ];
}, },
}); });

View File

@@ -196,7 +196,7 @@
"tailwindcss": "^3.4.12", "tailwindcss": "^3.4.12",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths-webpack-plugin": "^4.1.0", "tsconfig-paths-webpack-plugin": "^4.1.0",
"vite": "^5.4.20", "vite": "^5.4.21",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^3.2.4" "vitest": "^3.2.4"
}, },

View File

@@ -415,7 +415,7 @@ importers:
version: 6.21.0(eslint@8.57.0)(typescript@5.8.3) version: 6.21.0(eslint@8.57.0)(typescript@5.8.3)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^4.7.0 specifier: ^4.7.0
version: 4.7.0(vite@5.4.20(@types/node@20.14.8)(terser@5.44.0)) version: 4.7.0(vite@5.4.21(@types/node@20.14.8)(terser@5.44.0))
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: ^3.2.4 specifier: ^3.2.4
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.14.8)(jsdom@22.1.0)(msw@2.11.4(@types/node@20.14.8)(typescript@5.8.3))(terser@5.44.0)) version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.14.8)(jsdom@22.1.0)(msw@2.11.4(@types/node@20.14.8)(typescript@5.8.3))(terser@5.44.0))
@@ -525,11 +525,11 @@ importers:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.2.0 version: 4.2.0
vite: vite:
specifier: ^5.4.20 specifier: ^5.4.21
version: 5.4.20(@types/node@20.14.8)(terser@5.44.0) version: 5.4.21(@types/node@20.14.8)(terser@5.44.0)
vite-tsconfig-paths: vite-tsconfig-paths:
specifier: ^4.3.2 specifier: ^4.3.2
version: 4.3.2(typescript@5.8.3)(vite@5.4.20(@types/node@20.14.8)(terser@5.44.0)) version: 4.3.2(typescript@5.8.3)(vite@5.4.21(@types/node@20.14.8)(terser@5.44.0))
vitest: vitest:
specifier: ^3.2.4 specifier: ^3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.8)(jsdom@22.1.0)(msw@2.11.4(@types/node@20.14.8)(typescript@5.8.3))(terser@5.44.0) version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.8)(jsdom@22.1.0)(msw@2.11.4(@types/node@20.14.8)(typescript@5.8.3))(terser@5.44.0)
@@ -8576,13 +8576,13 @@ packages:
vite-tsconfig-paths@4.3.2: vite-tsconfig-paths@4.3.2:
resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==}
peerDependencies: peerDependencies:
vite: '*' vite: '>=4.5.14'
peerDependenciesMeta: peerDependenciesMeta:
vite: vite:
optional: true optional: true
vite@5.4.20: vite@5.4.21:
resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -13299,7 +13299,7 @@ snapshots:
'@ungap/structured-clone@1.2.0': {} '@ungap/structured-clone@1.2.0': {}
'@vitejs/plugin-react@4.7.0(vite@5.4.20(@types/node@20.14.8)(terser@5.44.0))': '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@20.14.8)(terser@5.44.0))':
dependencies: dependencies:
'@babel/core': 7.28.4 '@babel/core': 7.28.4
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4)
@@ -13307,7 +13307,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.27 '@rolldown/pluginutils': 1.0.0-beta.27
'@types/babel__core': 7.20.5 '@types/babel__core': 7.20.5
react-refresh: 0.17.0 react-refresh: 0.17.0
vite: 5.4.20(@types/node@20.14.8)(terser@5.44.0) vite: 5.4.21(@types/node@20.14.8)(terser@5.44.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -13338,14 +13338,14 @@ snapshots:
chai: 5.3.3 chai: 5.3.3
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(msw@2.11.4(@types/node@20.14.8)(typescript@5.8.3))(vite@5.4.20(@types/node@20.14.8)(terser@5.44.0))': '@vitest/mocker@3.2.4(msw@2.11.4(@types/node@20.14.8)(typescript@5.8.3))(vite@5.4.21(@types/node@20.14.8)(terser@5.44.0))':
dependencies: dependencies:
'@vitest/spy': 3.2.4 '@vitest/spy': 3.2.4
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.19 magic-string: 0.30.19
optionalDependencies: optionalDependencies:
msw: 2.11.4(@types/node@20.14.8)(typescript@5.8.3) msw: 2.11.4(@types/node@20.14.8)(typescript@5.8.3)
vite: 5.4.20(@types/node@20.14.8)(terser@5.44.0) vite: 5.4.21(@types/node@20.14.8)(terser@5.44.0)
'@vitest/pretty-format@3.2.4': '@vitest/pretty-format@3.2.4':
dependencies: dependencies:
@@ -18484,7 +18484,7 @@ snapshots:
debug: 4.4.3 debug: 4.4.3
es-module-lexer: 1.7.0 es-module-lexer: 1.7.0
pathe: 2.0.3 pathe: 2.0.3
vite: 5.4.20(@types/node@20.14.8)(terser@5.44.0) vite: 5.4.21(@types/node@20.14.8)(terser@5.44.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- less - less
@@ -18496,18 +18496,18 @@ snapshots:
- supports-color - supports-color
- terser - terser
vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@5.4.20(@types/node@20.14.8)(terser@5.44.0)): vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@5.4.21(@types/node@20.14.8)(terser@5.44.0)):
dependencies: dependencies:
debug: 4.4.1 debug: 4.4.1
globrex: 0.1.2 globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.8.3) tsconfck: 3.1.6(typescript@5.8.3)
optionalDependencies: optionalDependencies:
vite: 5.4.20(@types/node@20.14.8)(terser@5.44.0) vite: 5.4.21(@types/node@20.14.8)(terser@5.44.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- typescript - typescript
vite@5.4.20(@types/node@20.14.8)(terser@5.44.0): vite@5.4.21(@types/node@20.14.8)(terser@5.44.0):
dependencies: dependencies:
esbuild: 0.25.10 esbuild: 0.25.10
postcss: 8.5.3 postcss: 8.5.3
@@ -18521,7 +18521,7 @@ snapshots:
dependencies: dependencies:
'@types/chai': 5.2.2 '@types/chai': 5.2.2
'@vitest/expect': 3.2.4 '@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(msw@2.11.4(@types/node@20.14.8)(typescript@5.8.3))(vite@5.4.20(@types/node@20.14.8)(terser@5.44.0)) '@vitest/mocker': 3.2.4(msw@2.11.4(@types/node@20.14.8)(typescript@5.8.3))(vite@5.4.21(@types/node@20.14.8)(terser@5.44.0))
'@vitest/pretty-format': 3.2.4 '@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4 '@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4 '@vitest/snapshot': 3.2.4
@@ -18539,7 +18539,7 @@ snapshots:
tinyglobby: 0.2.15 tinyglobby: 0.2.15
tinypool: 1.1.1 tinypool: 1.1.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vite: 5.4.20(@types/node@20.14.8)(terser@5.44.0) vite: 5.4.21(@types/node@20.14.8)(terser@5.44.0)
vite-node: 3.2.4(@types/node@20.14.8)(terser@5.44.0) vite-node: 3.2.4(@types/node@20.14.8)(terser@5.44.0)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:

View File

@@ -166,6 +166,12 @@ rec {
''; '';
}; };
packageWithDisabledCSP = package.overrideAttrs (oldAttrs: {
configurePhase = oldAttrs.configurePhase + ''
export CSP_MODE=disabled
'';
});
dockerImage = pkgs.runCommand "image-as-dir" { } '' dockerImage = pkgs.runCommand "image-as-dir" { } ''
${(nix2containerPkgs.nix2container.buildImage { ${(nix2containerPkgs.nix2container.buildImage {
inherit name created; inherit name created;
@@ -175,7 +181,7 @@ rec {
copyToRoot = pkgs.buildEnv { copyToRoot = pkgs.buildEnv {
name = "image"; name = "image";
paths = [ paths = [
package packageWithDisabledCSP
(pkgs.writeTextFile { (pkgs.writeTextFile {
name = "tmp-file"; name = "tmp-file";
text = '' text = ''

View File

@@ -127,14 +127,14 @@ export default function AuthenticatedLayout({
className="relative flex h-full flex-row overflow-hidden" className="relative flex h-full flex-row overflow-hidden"
ref={setMainNavContainer} ref={setMainNavContainer}
> >
{mainNavPinned && isMdOrLarger && <PinnedMainNav />} {withMainNav && mainNavPinned && isMdOrLarger && <PinnedMainNav />}
<div <div
className={cn('relative flex h-full w-full flex-row bg-accent', { className={cn('relative flex h-full w-full flex-row bg-accent', {
'overflow-x-auto': mainNavPinned && isMdOrLarger && withMainNav, 'overflow-x-auto': mainNavPinned && isMdOrLarger && withMainNav,
})} })}
> >
{(!mainNavPinned || !isMdOrLarger) && ( {withMainNav && (!mainNavPinned || !isMdOrLarger) && (
<div className="flex h-full w-6 justify-center"> <div className="flex h-full w-6 justify-center">
<MainNav container={mainNavContainer} /> <MainNav container={mainNavContainer} />
</div> </div>

View File

@@ -23,7 +23,7 @@ export default function SocialProvidersSettings() {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
return nhost.auth.signInProviderURL('github', { return nhost.auth.signInProviderURL('github', {
connect: token, connect: token,
redirectTo: `${window.location.origin}/account`, redirectTo: `${window.location.origin}/account?signinProvider=github`,
}); });
} }
return ''; return '';

View File

@@ -3,18 +3,21 @@ import {
useGithubAuthentication, useGithubAuthentication,
type UseGithubAuthenticationHookProps, type UseGithubAuthenticationHookProps,
} from '@/features/auth/AuthProviders/Github/hooks/useGithubAuthentication'; } from '@/features/auth/AuthProviders/Github/hooks/useGithubAuthentication';
import { cn } from '@/lib/utils';
import { SiGithub } from '@icons-pack/react-simple-icons'; import { SiGithub } from '@icons-pack/react-simple-icons';
interface Props extends UseGithubAuthenticationHookProps { interface Props extends UseGithubAuthenticationHookProps {
buttonText?: string; buttonText?: string;
withAnonId?: boolean; withAnonId?: boolean;
redirectTo?: string; redirectTo?: string;
className?: string;
} }
function GithubAuthButton({ function GithubAuthButton({
buttonText = 'Continue with GitHub', buttonText = 'Continue with GitHub',
withAnonId = false, withAnonId = false,
redirectTo, redirectTo,
className,
}: Props) { }: Props) {
const { mutate: signInWithGithub, isLoading } = useGithubAuthentication({ const { mutate: signInWithGithub, isLoading } = useGithubAuthentication({
withAnonId, withAnonId,
@@ -22,7 +25,10 @@ function GithubAuthButton({
}); });
return ( return (
<Button <Button
className="gap-2 !bg-white text-sm+ !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60" className={cn(
'gap-2 !bg-white text-sm+ !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60',
className,
)}
disabled={isLoading} disabled={isLoading}
loading={isLoading} loading={isLoading}
onClick={() => signInWithGithub()} onClick={() => signInWithGithub()}

View File

@@ -30,8 +30,8 @@ function useGithubAuthentication({
}; };
} }
const redirectURl = nhost.auth.signInProviderURL('github', options); const redirectURL = nhost.auth.signInProviderURL('github', options);
window.location.href = redirectURl; window.location.href = redirectURL;
}, },
{ {
onError: () => { onError: () => {

View File

@@ -2,7 +2,7 @@ import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/component
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName'; import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
function SignInWithGithub() { function SignInWithGithub() {
const redirectTo = useHostName(); const redirectTo = `${useHostName()}?signinProvider=github`;
return ( return (
<GithubAuthButton <GithubAuthButton
redirectTo={redirectTo} redirectTo={redirectTo}

View File

@@ -1,8 +1,11 @@
import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton'; import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton';
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
function SignUpWithGithub() { function SignUpWithGithub() {
const redirectTo = `${useHostName()}?signinProvider=github`;
return ( return (
<GithubAuthButton <GithubAuthButton
redirectTo={redirectTo}
buttonText="Sign Up with GitHub" buttonText="Sign Up with GitHub"
errorText="An error occurred while trying to sign up using GitHub. Please try again." errorText="An error occurred while trying to sign up using GitHub. Please try again."
/> />

View File

@@ -176,7 +176,7 @@ export default function AppleProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/products/auth/social/sign-in-apple" docsLink="https://docs.nhost.io/products/auth/providers/sign-in-apple"
docsTitle="how to sign in users with Apple" docsTitle="how to sign in users with Apple"
icon={ icon={
theme.palette.mode === 'dark' theme.palette.mode === 'dark'

View File

@@ -141,7 +141,7 @@ export default function DiscordProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/products/auth/social/sign-in-discord" docsLink="https://docs.nhost.io/products/auth/providers/sign-in-discord"
docsTitle="how to sign in users with Discord" docsTitle="how to sign in users with Discord"
icon="/assets/brands/discord.svg" icon="/assets/brands/discord.svg"
switchId="enabled" switchId="enabled"

View File

@@ -142,7 +142,7 @@ export default function FacebookProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/products/auth/social/sign-in-facebook" docsLink="https://docs.nhost.io/products/auth/providers/sign-in-facebook"
docsTitle="how to sign in users with Facebook" docsTitle="how to sign in users with Facebook"
icon="/assets/brands/facebook.svg" icon="/assets/brands/facebook.svg"
switchId="enabled" switchId="enabled"

View File

@@ -144,7 +144,7 @@ export default function GitHubProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/products/auth/social/sign-in-github" docsLink="https://docs.nhost.io/products/auth/providers/sign-in-github"
docsTitle="how to sign in users with GitHub" docsTitle="how to sign in users with GitHub"
icon={ icon={
theme.palette.mode === 'dark' theme.palette.mode === 'dark'

View File

@@ -162,7 +162,7 @@ export default function GoogleProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/products/auth/social/sign-in-google" docsLink="https://docs.nhost.io/products/auth/providers/sign-in-google"
docsTitle="how to sign in users with Google" docsTitle="how to sign in users with Google"
icon="/assets/brands/google.svg" icon="/assets/brands/google.svg"
switchId="enabled" switchId="enabled"

View File

@@ -142,7 +142,7 @@ export default function LinkedInProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/products/auth/social/sign-in-linkedin" docsLink="https://docs.nhost.io/products/auth/providers/sign-in-linkedin"
docsTitle="how to sign in users with LinkedIn" docsTitle="how to sign in users with LinkedIn"
icon="/assets/brands/linkedin.svg" icon="/assets/brands/linkedin.svg"
switchId="enabled" switchId="enabled"

View File

@@ -142,7 +142,7 @@ export default function SpotifyProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/products/auth/social/sign-in-spotify" docsLink="https://docs.nhost.io/products/auth/providers/sign-in-spotify"
docsTitle="how to sign in users with Spotify" docsTitle="how to sign in users with Spotify"
icon="/assets/brands/spotify.svg" icon="/assets/brands/spotify.svg"
switchId="enabled" switchId="enabled"

View File

@@ -144,7 +144,7 @@ export default function TwitchProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/products/auth/social/sign-in-twitch" docsLink="https://docs.nhost.io/products/auth/providers/sign-in-twitch"
docsTitle="how to sign in users with Twitch" docsTitle="how to sign in users with Twitch"
icon={ icon={
theme.palette.mode === 'dark' theme.palette.mode === 'dark'

View File

@@ -177,7 +177,7 @@ export default function WorkOsProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/products/auth/social/sign-in-workos" docsLink="https://docs.nhost.io/products/auth/providers/sign-in-workos"
docsTitle="how to sign in users with WorkOS" docsTitle="how to sign in users with WorkOS"
icon="/assets/brands/workos.svg" icon="/assets/brands/workos.svg"
switchId="enabled" switchId="enabled"

View File

@@ -3,7 +3,7 @@ import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/gen
import { useDatabaseQuery } from '@/features/orgs/projects/database/dataGrid/hooks/useDatabaseQuery'; import { useDatabaseQuery } from '@/features/orgs/projects/database/dataGrid/hooks/useDatabaseQuery';
import { useProject } from '@/features/orgs/projects/hooks/useProject'; import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { getToastStyleProps } from '@/utils/constants/settings'; import { getToastStyleProps } from '@/utils/constants/settings';
import { getHasuraAdminSecret } from '@/utils/env'; import { getHasuraAdminSecret, getHasuraMigrationsApiUrl } from '@/utils/env';
import { parseIdentifiersFromSQL } from '@/utils/sql'; import { parseIdentifiersFromSQL } from '@/utils/sql';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
@@ -53,7 +53,10 @@ export default function useRunSQL(
isCascade: boolean, isCascade: boolean,
) => { ) => {
try { try {
const migrationApiResponse = await fetch(`${appUrl}/apis/migrate`, { const url = isPlatform
? `${appUrl}/apis/migrate`
: getHasuraMigrationsApiUrl();
const migrationApiResponse = await fetch(url, {
method: 'POST', method: 'POST',
headers: { 'x-hasura-admin-secret': adminSecret }, headers: { 'x-hasura-admin-secret': adminSecret },
body: JSON.stringify({ body: JSON.stringify({

View File

@@ -1,3 +1,4 @@
import { ErrorMessage } from '@/components/presentational/ErrorMessage';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary'; import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Avatar } from '@/components/ui/v2/Avatar'; import { Avatar } from '@/components/ui/v2/Avatar';
@@ -11,14 +12,33 @@ import { Link } from '@/components/ui/v2/Link';
import { List } from '@/components/ui/v2/List'; import { List } from '@/components/ui/v2/List';
import { ListItem } from '@/components/ui/v2/ListItem'; import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { GithubAuthButton } from '@/features/auth/AuthProviders/Github/components/GithubAuthButton';
import { useHostName } from '@/features/orgs/projects/common/hooks/useHostName';
import { EditRepositorySettings } from '@/features/orgs/projects/git/common/components/EditRepositorySettings'; import { EditRepositorySettings } from '@/features/orgs/projects/git/common/components/EditRepositorySettings';
import { useGetGithubRepositoriesQuery } from '@/generated/graphql'; import {
getGitHubToken,
saveGitHubToken,
} from '@/features/orgs/projects/git/common/utils';
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useGetAuthUserProvidersQuery } from '@/generated/graphql';
import { useAccessToken } from '@/hooks/useAccessToken';
import { GitHubAPIError, listGitHubInstallationRepos } from '@/lib/github';
import { isEmptyValue } from '@/lib/utils';
import { getToastStyleProps } from '@/utils/constants/settings';
import { nhost } from '@/utils/nhost';
import { Divider } from '@mui/material'; import { Divider } from '@mui/material';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import NavLink from 'next/link';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import { Fragment, useEffect, useMemo, useState } from 'react'; import { Fragment, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
export type ConnectGitHubModalState = 'CONNECTING' | 'EDITING'; export type ConnectGitHubModalState =
| 'CONNECTING'
| 'EDITING'
| 'EXPIRED_GITHUB_SESSION'
| 'GITHUB_CONNECTION_REQUIRED';
export interface ConnectGitHubModalProps { export interface ConnectGitHubModalProps {
/** /**
@@ -28,18 +48,153 @@ export interface ConnectGitHubModalProps {
close?: VoidFunction; close?: VoidFunction;
} }
interface GitHubData {
githubAppInstallations: Array<{
id: number;
accountLogin?: string;
accountAvatarUrl?: string;
}>;
githubRepositories: Array<{
id: number;
node_id: string;
name: string;
fullName: string;
githubAppInstallation: {
accountLogin?: string;
accountAvatarUrl?: string;
};
}>;
}
export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) { export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
const [ConnectGitHubModalState, setConnectGitHubModalState] = const [ConnectGitHubModalState, setConnectGitHubModalState] =
useState<ConnectGitHubModalState>('CONNECTING'); useState<ConnectGitHubModalState>('CONNECTING');
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null); const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null);
const [githubData, setGithubData] = useState<GitHubData | null>(null);
const [loading, setLoading] = useState(true);
const { project, loading: loadingProject } = useProject();
const { org, loading: loadingOrg } = useCurrentOrg();
const hostname = useHostName();
const token = useAccessToken();
const {
data,
loading: loadingGithubConnected,
error: errorGithubConnected,
} = useGetAuthUserProvidersQuery();
const { data, loading, error, startPolling } = const githubProvider = data?.authUserProviders?.find(
useGetGithubRepositoriesQuery(); (item) => item.providerId === 'github',
);
const getGitHubConnectUrl = () => {
if (typeof window !== 'undefined') {
return nhost.auth.signInProviderURL('github', {
connect: token,
redirectTo: `${window.location.origin}?signinProvider=github&state=signin-refresh:${org.slug}:${project?.subdomain}`,
});
}
return '';
};
useEffect(() => { useEffect(() => {
startPolling(2000); if (loadingGithubConnected) {
}, [startPolling]); return;
}
const fetchGitHubData = async () => {
try {
setLoading(true);
if (isEmptyValue(githubProvider)) {
setConnectGitHubModalState('GITHUB_CONNECTION_REQUIRED');
setLoading(false);
return;
}
const githubToken = getGitHubToken();
if (
!githubToken?.authUserProviderId ||
githubProvider!.id !== githubToken.authUserProviderId
) {
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
setLoading(false);
return;
}
const { refreshToken, expiresAt: expiresAtString } = githubToken;
let accessToken = githubToken?.accessToken;
const expiresAt = new Date(expiresAtString).getTime();
const currentTime = Date.now();
const expiresAtMargin = 60 * 1000;
if (expiresAt - currentTime < expiresAtMargin) {
if (!refreshToken) {
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
setLoading(false);
return;
}
const refreshResponse = await nhost.auth.refreshProviderToken(
'github',
{ refreshToken },
);
if (!refreshResponse.body) {
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
setLoading(false);
return;
}
saveGitHubToken({
...refreshResponse.body,
authUserProviderId: githubProvider!.id,
});
accessToken = refreshResponse.body.accessToken;
}
const installations = await listGitHubInstallationRepos(accessToken);
const transformedData = {
githubAppInstallations: installations.map((item) => ({
id: item.installation.id,
accountLogin: item.installation.account?.login,
accountAvatarUrl: item.installation.account?.avatar_url,
})),
githubRepositories: installations.flatMap((item) =>
item.repositories.map((repo) => ({
id: repo.id,
node_id: repo.node_id,
name: repo.name,
fullName: repo.full_name,
githubAppInstallation: {
accountLogin: item.installation.account?.login,
accountAvatarUrl: item.installation.account?.avatar_url,
},
})),
),
};
setGithubData(transformedData);
setLoading(false);
} catch (err) {
console.error('Error fetching GitHub data:', err);
if (err instanceof GitHubAPIError && err.status === 401) {
setConnectGitHubModalState('EXPIRED_GITHUB_SESSION');
setLoading(false);
return;
}
toast.error(err?.message, getToastStyleProps());
close?.();
}
};
fetchGitHubData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [githubProvider, loadingGithubConnected]);
const handleSelectAnotherRepository = () => { const handleSelectAnotherRepository = () => {
setSelectedRepoId(null); setSelectedRepoId(null);
@@ -56,13 +211,91 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]); useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
if (error) { if (errorGithubConnected instanceof Error) {
throw error; return (
<div className="px-1 md:w-[653px]">
<div className="flex flex-col">
<div className="mx-auto text-center">
<div className="mx-auto h-8 w-8">
<GitHubIcon className="h-8 w-8" />
</div>
</div>
<div className="flex flex-col gap-2">
<Text className="mt-2.5 text-center text-lg font-medium">
Error fetching GitHub data
</Text>
<ErrorMessage>{errorGithubConnected.message}</ErrorMessage>
</div>
</div>
</div>
);
} }
if (loading) { if (loading || loadingProject || loadingOrg || loadingGithubConnected) {
return ( return (
<ActivityIndicator delay={500} label="Loading GitHub repositories..." /> <div className="px-1 md:w-[653px]">
<div className="flex flex-col">
<div className="mx-auto text-center">
<div className="mx-auto h-8 w-8">
<GitHubIcon className="h-8 w-8" />
</div>
</div>
<div>
<Text className="mt-2.5 text-center text-lg font-medium">
Loading repositories...
</Text>
<Text className="text-center text-xs font-normal" color="secondary">
Fetching your GitHub repositories
</Text>
<div className="mb-2 mt-6 flex w-full">
<Input placeholder="Search..." fullWidth disabled value="" />
</div>
</div>
<div className="flex h-import items-center justify-center border-y">
<ActivityIndicator delay={0} label="" />
</div>
</div>
</div>
);
}
if (ConnectGitHubModalState === 'GITHUB_CONNECTION_REQUIRED') {
return (
<div className="flex flex-col items-center justify-center gap-5 px-1 py-1 md:w-[653px]">
<p className="text-center text-foreground">
You need to connect your GitHub account to continue.
</p>
<NavLink
href={getGitHubConnectUrl()}
passHref
rel="noreferrer noopener"
legacyBehavior
>
<Button
className="w-full max-w-72"
variant="outlined"
color="secondary"
startIcon={<GitHubIcon />}
>
Connect to GitHub
</Button>
</NavLink>
</div>
);
}
if (ConnectGitHubModalState === 'EXPIRED_GITHUB_SESSION') {
return (
<div className="flex w-full flex-col items-center justify-center gap-5 px-1 py-1 md:w-[653px]">
<p className="text-center text-foreground">
Please sign in with GitHub to continue.
</p>
<GithubAuthButton
redirectTo={`${hostname}?signinProvider=github&state=signin-refresh:${org.slug}:${project!.subdomain}`}
buttonText="Sign in with GitHub"
className="w-full max-w-72 gap-2 !bg-primary !text-white disabled:!text-white disabled:!text-opacity-60 dark:!bg-white dark:!text-black dark:disabled:!text-black"
/>
</div>
); );
} }
@@ -78,25 +311,27 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
); );
} }
const { githubAppInstallations } = data || {}; const { githubAppInstallations } = githubData || {};
const filteredGitHubAppInstallations = data?.githubAppInstallations.filter( const filteredGitHubAppInstallations =
(githubApp) => !!githubApp.accountLogin, githubData?.githubAppInstallations.filter(
); (githubApp) => !!githubApp.accountLogin,
);
const filteredGitHubRepositories = data?.githubRepositories.filter( const filteredGitHubRepositories = githubData?.githubRepositories.filter(
(repo) => !!repo.githubAppInstallation, (repo) => !!repo.githubAppInstallation,
); );
const filteredGitHubAppInstallationsNullValues = const filteredGitHubAppInstallationsNullValues =
data?.githubAppInstallations.filter((githubApp) => !!githubApp.accountLogin) githubData?.githubAppInstallations.filter(
.length === 0; (githubApp) => !!githubApp.accountLogin,
).length === 0;
const faultyGitHubInstallation = const faultyGitHubInstallation =
githubAppInstallations?.length === 0 || githubAppInstallations?.length === 0 ||
filteredGitHubAppInstallationsNullValues; filteredGitHubAppInstallationsNullValues;
const noRepositoriesAdded = data?.githubRepositories.length === 0; const noRepositoriesAdded = githubData?.githubRepositories.length === 0;
if (faultyGitHubInstallation) { if (faultyGitHubInstallation) {
return ( return (
@@ -115,11 +350,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
</div> </div>
<Button <Button
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL} href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
// Both `target` and `rel` are available when `href` is set. This is
// a limitation of MUI.
// @ts-ignore
target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
endIcon={<ArrowSquareOutIcon className="h-4 w-4" />} endIcon={<ArrowSquareOutIcon className="h-4 w-4" />}
> >
@@ -179,8 +410,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
</List> </List>
<Link <Link
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL} href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
underline="hover" underline="hover"
className="grid grid-flow-col items-center justify-start gap-1" className="grid grid-flow-col items-center justify-start gap-1"
@@ -199,8 +429,8 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
className="text-center text-xs font-normal" className="text-center text-xs font-normal"
color="secondary" color="secondary"
> >
Showing repositories from {data?.githubAppInstallations.length}{' '} Showing repositories from{' '}
GitHub account(s) {githubData?.githubAppInstallations.length} GitHub account(s)
</Text> </Text>
<div className="mb-2 mt-6 flex w-full"> <div className="mb-2 mt-6 flex w-full">
<Input <Input
@@ -226,7 +456,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
<Button <Button
variant="borderless" variant="borderless"
color="primary" color="primary"
onClick={() => setSelectedRepoId(repo.id)} onClick={() => setSelectedRepoId(repo.node_id)}
> >
Connect Connect
</Button> </Button>
@@ -268,8 +498,7 @@ export default function ConnectGitHubModal({ close }: ConnectGitHubModalProps) {
Do you miss a repository, or do you need to connect another GitHub Do you miss a repository, or do you need to connect another GitHub
account?{' '} account?{' '}
<Link <Link
href={process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL} href={`${process.env.NEXT_PUBLIC_GITHUB_APP_INSTALL_URL}?state=install-github-app:${org.slug}:${project!.subdomain}`}
target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
className="text-xs font-medium" className="text-xs font-medium"
underline="hover" underline="hover"

View File

@@ -6,7 +6,7 @@ import { FormProvider, useForm } from 'react-hook-form';
export interface EditRepositorySettingsProps { export interface EditRepositorySettingsProps {
close?: () => void; close?: () => void;
openConnectGithubModal?: () => void; openConnectGithubModal?: () => void;
selectedRepoId?: string; selectedRepoId: string;
connectGithubModalState?: ConnectGitHubModalState; connectGithubModalState?: ConnectGitHubModalState;
handleSelectAnotherRepository?: () => void; handleSelectAnotherRepository?: () => void;
} }

View File

@@ -6,14 +6,14 @@ import { Text } from '@/components/ui/v2/Text';
import { EditRepositoryAndBranchSettings } from '@/features/orgs/projects/git/common/components/EditRepositoryAndBranchSettings'; import { EditRepositoryAndBranchSettings } from '@/features/orgs/projects/git/common/components/EditRepositoryAndBranchSettings';
import type { EditRepositorySettingsFormData } from '@/features/orgs/projects/git/common/components/EditRepositorySettings'; import type { EditRepositorySettingsFormData } from '@/features/orgs/projects/git/common/components/EditRepositorySettings';
import { useProject } from '@/features/orgs/projects/hooks/useProject'; import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useUpdateApplicationMutation } from '@/generated/graphql'; import { useConnectGithubRepoMutation } from '@/generated/graphql';
import { analytics } from '@/lib/segment'; import { analytics } from '@/lib/segment';
import { discordAnnounce } from '@/utils/discordAnnounce'; import { discordAnnounce } from '@/utils/discordAnnounce';
import { triggerToast } from '@/utils/toast'; import { triggerToast } from '@/utils/toast';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
export interface EditRepositorySettingsModalProps { export interface EditRepositorySettingsModalProps {
selectedRepoId?: string; selectedRepoId: string;
close?: () => void; close?: () => void;
handleSelectAnotherRepository?: () => void; handleSelectAnotherRepository?: () => void;
} }
@@ -33,45 +33,29 @@ export default function EditRepositorySettingsModal({
const { project, refetch: refetchProject } = useProject(); const { project, refetch: refetchProject } = useProject();
const [updateApp, { loading }] = useUpdateApplicationMutation(); const [connectGithubRepo, { loading }] = useConnectGithubRepoMutation();
const handleEditGitHubIntegration = async ( const handleEditGitHubIntegration = async (
data: EditRepositorySettingsFormData, data: EditRepositorySettingsFormData,
) => { ) => {
try { try {
if (!project?.githubRepository || selectedRepoId) { await connectGithubRepo({
await updateApp({ variables: {
variables: { appID: project?.id,
appId: project?.id, githubNodeID: selectedRepoId,
app: { productionBranch: data.productionBranch,
githubRepositoryId: selectedRepoId, baseFolder: data.repoBaseFolder,
repositoryProductionBranch: data.productionBranch, },
nhostBaseFolder: data.repoBaseFolder, });
},
},
});
if (selectedRepoId) { analytics.track('Project Connected to GitHub', {
analytics.track('Project Connected to GitHub', { projectId: project?.id,
projectId: project?.id, projectName: project?.name,
projectName: project?.name, projectSubdomain: project?.subdomain,
projectSubdomain: project?.subdomain, repositoryId: selectedRepoId,
repositoryId: selectedRepoId, productionBranch: data.productionBranch,
productionBranch: data.productionBranch, baseFolder: data.repoBaseFolder,
baseFolder: data.repoBaseFolder, });
});
}
} else {
await updateApp({
variables: {
appId: project.id,
app: {
repositoryProductionBranch: data.productionBranch,
nhostBaseFolder: data.repoBaseFolder,
},
},
});
}
await refetchProject(); await refetchProject();

View File

@@ -0,0 +1,13 @@
mutation ConnectGithubRepo(
$appID: uuid!
$githubNodeID: String!
$productionBranch: String!
$baseFolder: String!
) {
connectGithubRepo(
appID: $appID
githubNodeID: $githubNodeID
productionBranch: $productionBranch
baseFolder: $baseFolder
)
}

View File

@@ -2,12 +2,12 @@ import { useDialog } from '@/components/common/DialogProvider';
import { ConnectGitHubModal } from '@/features/orgs/projects/git/common/components/ConnectGitHubModal'; import { ConnectGitHubModal } from '@/features/orgs/projects/git/common/components/ConnectGitHubModal';
export default function useGitHubModal() { export default function useGitHubModal() {
const { openAlertDialog } = useDialog(); const { openAlertDialog, closeAlertDialog } = useDialog();
function openGitHubModal() { function openGitHubModal() {
openAlertDialog({ openAlertDialog({
title: 'Connect GitHub Repository', title: 'Connect GitHub Repository',
payload: <ConnectGitHubModal />, payload: <ConnectGitHubModal close={closeAlertDialog} />,
props: { props: {
hidePrimaryAction: true, hidePrimaryAction: true,
hideSecondaryAction: true, hideSecondaryAction: true,

View File

@@ -0,0 +1,23 @@
import { isNotEmptyValue } from '@/lib/utils';
import type { ProviderSession } from '@nhost/nhost-js/auth';
const githubProviderTokenKey = 'nhost_provider_tokens_github';
export type GitHubProviderToken = ProviderSession & {
authUserProviderId?: string;
};
export function saveGitHubToken(token: GitHubProviderToken) {
localStorage.setItem(githubProviderTokenKey, JSON.stringify(token));
}
export function getGitHubToken() {
const token = localStorage.getItem(githubProviderTokenKey);
return isNotEmptyValue(token)
? (JSON.parse(token) as GitHubProviderToken)
: null;
}
export function clearGitHubToken() {
localStorage.removeItem(githubProviderTokenKey);
}

View File

@@ -0,0 +1 @@
export * from './githubTokens';

View File

@@ -0,0 +1,99 @@
/**
* Custom error class for GitHub API errors that preserves HTTP status codes
*/
export class GitHubAPIError extends Error {
constructor(
message: string,
public status: number,
public statusText: string,
) {
super(message);
this.name = 'GitHubAPIError';
}
}
interface GitHubAppInstallation {
id: number;
account?: {
login: string;
avatar_url: string;
[key: string]: unknown;
}
[key: string]: unknown;
}
/**
* Lists all GitHub App installations accessible to the user
* @param accessToken - The GitHub OAuth access token
* @returns Array of app installations
*/
export async function listGitHubAppInstallations(accessToken: string): Promise<GitHubAppInstallation[]> {
const response = await fetch('https://api.github.com/user/installations', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
cache: 'no-cache',
});
if (!response.ok) {
throw new GitHubAPIError(
`Failed to list installations: ${response.statusText}`,
response.status,
response.statusText
);
}
const data = await response.json();
return data.installations;
}
interface GitHubRepo {
id: number;
node_id: string;
name: string;
full_name: string;
[key: string]: unknown;
}
/**
* Lists all repositories accessible through GitHub App installations
* @param accessToken - The GitHub OAuth access token
* @returns Array of repositories grouped by installation
*/
export async function listGitHubInstallationRepos(accessToken: string) {
const installations = await listGitHubAppInstallations(accessToken);
const reposByInstallation = await Promise.all(
installations.map(async (installation) => {
const response = await fetch(
`https://api.github.com/user/installations/${installation.id}/repositories`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
cache: 'no-cache',
}
);
if (!response.ok) {
throw new GitHubAPIError(
`Failed to list repos for installation ${installation.id}: ${response.statusText}`,
response.status,
response.statusText
);
}
const data = await response.json();
return {
installation,
repositories: data.repositories as GitHubRepo[],
};
})
);
return reposByInstallation;
}

View File

@@ -77,12 +77,12 @@ function MyApp({
<CacheProvider value={emotionCache}> <CacheProvider value={emotionCache}>
<NhostProvider nhost={nhost}> <NhostProvider nhost={nhost}>
<AuthProvider> <NhostApolloProvider
<NhostApolloProvider fetchPolicy="cache-and-network"
fetchPolicy="cache-and-network" nhost={nhost}
nhost={nhost} connectToDevTools={process.env.NEXT_PUBLIC_ENV === 'dev'}
connectToDevTools={process.env.NEXT_PUBLIC_ENV === 'dev'} >
> <AuthProvider>
<UIProvider> <UIProvider>
<Toaster position="bottom-center" /> <Toaster position="bottom-center" />
<ThemeProvider <ThemeProvider
@@ -106,8 +106,8 @@ function MyApp({
</RetryableErrorBoundary> </RetryableErrorBoundary>
</ThemeProvider> </ThemeProvider>
</UIProvider> </UIProvider>
</NhostApolloProvider> </AuthProvider>
</AuthProvider> </NhostApolloProvider>
</NhostProvider> </NhostProvider>
</CacheProvider> </CacheProvider>
</QueryClientProvider> </QueryClientProvider>

View File

@@ -0,0 +1,165 @@
import { nhostRoutesClient } from '@/utils/nhost';
import type { NextApiRequest, NextApiResponse } from 'next';
export type CreateTicketRequest = {
project: string;
services: Array<{ label: string; value: string }>;
priority: string;
subject: string;
description: string;
userName: string;
userEmail: string;
};
export type CreateTicketResponse = {
success: boolean;
error?: string;
};
type GetProjectResponse = {
apps: Array<{
id: string;
}>;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<CreateTicketResponse>,
) {
if (req.method !== 'POST') {
return res
.status(405)
.json({ success: false, error: 'Method not allowed' });
}
try {
const {
project,
services,
priority,
subject,
description,
userName,
userEmail,
} = req.body as CreateTicketRequest;
// Validate required environment variables
if (
!process.env.NEXT_ZENDESK_USER_EMAIL ||
!process.env.NEXT_ZENDESK_API_KEY ||
!process.env.NEXT_ZENDESK_URL
) {
return res.status(500).json({
success: false,
error: 'Zendesk configuration is missing',
});
}
// Validate required fields
if (
!project ||
!services ||
!priority ||
!subject ||
!description ||
!userName ||
!userEmail
) {
return res.status(400).json({
success: false,
error: 'Missing required fields',
});
}
const token = req.headers.authorization?.split(' ')[1];
try {
// we use this to verify the owner of the JWT token has access to the project
const resp = await nhostRoutesClient.graphql.request<GetProjectResponse>(
{
query: `query GetProject($subdomain: String!){
apps(where: {subdomain: {_eq: $subdomain}}) {
id
}
}`,
variables: {
subdomain: project,
},
},
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (resp.body.data?.apps.length !== 1) {
return res.status(400).json({
success: false,
error: 'Invalid project subdomain',
});
}
} catch (error) {
return res.status(400).json({
success: false,
error: 'Invalid project subdomain',
});
}
const auth = btoa(
`${process.env.NEXT_ZENDESK_USER_EMAIL}/token:${process.env.NEXT_ZENDESK_API_KEY}`,
);
const response = await fetch(
`${process.env.NEXT_ZENDESK_URL}/api/v2/requests.json`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${auth}`,
},
body: JSON.stringify({
request: {
subject,
comment: {
body: description,
},
priority,
requester: {
name: userName,
email: userEmail,
},
custom_fields: [
// these custom field IDs come from zendesk
{
id: 19502784542098,
value: project,
},
{
id: 19922709880978,
value: services.map((service) => service.value?.toLowerCase()),
},
],
},
}),
},
);
if (!response.ok) {
const errorText = await response.text();
console.error('Zendesk API error:', errorText);
return res.status(response.status).json({
success: false,
error: `Failed to create ticket: ${response.statusText}`,
});
}
return res.status(200).json({ success: true });
} catch (error) {
console.error('Error creating ticket:', error);
return res.status(500).json({
success: false,
error: 'An unexpected error occurred',
});
}
}

View File

@@ -1,87 +0,0 @@
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { nhost } from '@/utils/nhost';
import { useAuth } from '@/providers/Auth';
import { useRouter } from 'next/router';
import type { ComponentType } from 'react';
import { useEffect, useState } from 'react';
export function authProtected<P extends JSX.IntrinsicAttributes>(
Comp: ComponentType<P>,
) {
return function AuthProtected(props: P) {
const router = useRouter();
const { isAuthenticated, isLoading } = useAuth();
useEffect(() => {
if (isLoading || isAuthenticated) {
return;
}
router.push('/signin');
}, [isLoading, isAuthenticated, router]);
if (isLoading) {
return <LoadingScreen />;
}
return <Comp {...props} />;
};
}
function Page() {
const [state, setState] = useState({
error: null,
loading: true,
});
const router = useRouter();
const { installation_id: installationId } = router.query;
useEffect(() => {
async function installGithubApp() {
try {
await nhost.functions.fetch('/client/github-app-installation', {
method: 'POST',
body: JSON.stringify({
installationId,
}),
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
setState({
error,
loading: false,
});
return;
}
setState({
error: null,
loading: false,
});
window.close();
}
// run in async manner
installGithubApp();
}, [installationId]);
if (state.loading) {
return <ActivityIndicator delay={500} label="Loading..." />;
}
if (state.error) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw state.error;
}
return <div>GitHub connection completed. You can close this tab.</div>;
}
export default authProtected(Page);

View File

@@ -1,12 +1,38 @@
import { Container } from '@/components/layout/Container'; import { Container } from '@/components/layout/Container';
import { OrgLayout } from '@/features/orgs/layout/OrgLayout'; import { OrgLayout } from '@/features/orgs/layout/OrgLayout';
import { SettingsLayout } from '@/features/orgs/layout/SettingsLayout'; import { SettingsLayout } from '@/features/orgs/layout/SettingsLayout';
import { useGitHubModal } from '@/features/orgs/projects/git/common/hooks/useGitHubModal';
import { BaseDirectorySettings } from '@/features/orgs/projects/git/settings/components/BaseDirectorySettings'; import { BaseDirectorySettings } from '@/features/orgs/projects/git/settings/components/BaseDirectorySettings';
import { DeploymentBranchSettings } from '@/features/orgs/projects/git/settings/components/DeploymentBranchSettings'; import { DeploymentBranchSettings } from '@/features/orgs/projects/git/settings/components/DeploymentBranchSettings';
import { GitConnectionSettings } from '@/features/orgs/projects/git/settings/components/GitConnectionSettings'; import { GitConnectionSettings } from '@/features/orgs/projects/git/settings/components/GitConnectionSettings';
import type { ReactElement } from 'react'; import { useRouter } from 'next/router';
import { useCallback, useEffect, type ReactElement } from 'react';
export default function GitSettingsPage() { export default function GitSettingsPage() {
const router = useRouter();
const { pathname, replace, isReady: isRouterReady } = router;
const { 'github-modal': githubModal, ...remainingQuery } = router.query;
const { openGitHubModal } = useGitHubModal();
const removeQueryParamsFromURL = useCallback(() => {
replace({ pathname, query: remainingQuery }, undefined, {
shallow: true,
});
}, [replace, remainingQuery, pathname]);
useEffect(() => {
if (!isRouterReady) {
return;
}
if (typeof githubModal === 'string') {
removeQueryParamsFromURL();
openGitHubModal();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [githubModal, isRouterReady]);
return ( return (
<Container <Container
className="grid max-w-5xl grid-flow-row gap-y-6 bg-transparent" className="grid max-w-5xl grid-flow-row gap-y-6 bg-transparent"

View File

@@ -12,7 +12,9 @@ function SupportPage() {
return ( return (
<Box className="h-full overflow-auto pb-4"> <Box className="h-full overflow-auto pb-4">
<Box className="flex w-full justify-start border-b-1 px-4 py-3"> <Box className="flex w-full justify-start border-b-1 px-4 py-3">
<Logo className="w-6 cursor-pointer" /> <Link href="https://app.nhost.io" rel="noopener noreferrer">
<Logo className="w-6" />
</Link>
</Box> </Box>
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">

View File

@@ -5,11 +5,11 @@ import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
import { Box } from '@/components/ui/v2/Box'; import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button'; import { Button } from '@/components/ui/v2/Button';
import { Divider } from '@/components/ui/v2/Divider'; import { Divider } from '@/components/ui/v2/Divider';
import { EnvelopeIcon } from '@/components/ui/v2/icons/EnvelopeIcon';
import { Input, inputClasses } from '@/components/ui/v2/Input'; import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Option } from '@/components/ui/v2/Option'; import { Option } from '@/components/ui/v2/Option';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast'; import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useAccessToken } from '@/hooks/useAccessToken';
import { useUserData } from '@/hooks/useUserData'; import { useUserData } from '@/hooks/useUserData';
import { import {
useGetOrganizationsQuery, useGetOrganizationsQuery,
@@ -17,6 +17,7 @@ import {
} from '@/utils/__generated__/graphql'; } from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { Mail } from 'lucide-react';
import { type ReactElement } from 'react'; import { type ReactElement } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -27,7 +28,7 @@ type Organization = Omit<
>; >;
const validationSchema = Yup.object({ const validationSchema = Yup.object({
organization: Yup.string().label('Organization'), organization: Yup.string().label('Organization').required(),
project: Yup.string().label('Project').required(), project: Yup.string().label('Project').required(),
services: Yup.array() services: Yup.array()
.of(Yup.object({ label: Yup.string(), value: Yup.string() })) .of(Yup.object({ label: Yup.string(), value: Yup.string() }))
@@ -36,7 +37,6 @@ const validationSchema = Yup.object({
priority: Yup.string().label('Priority').required(), priority: Yup.string().label('Priority').required(),
subject: Yup.string().label('Subject').required(), subject: Yup.string().label('Subject').required(),
description: Yup.string().label('Description').required(), description: Yup.string().label('Description').required(),
ccs: Yup.string().label('CCs').optional(),
}); });
export type CreateTicketFormValues = Yup.InferType<typeof validationSchema>; export type CreateTicketFormValues = Yup.InferType<typeof validationSchema>;
@@ -58,7 +58,6 @@ function TicketPage() {
priority: '', priority: '',
subject: '', subject: '',
description: '', description: '',
ccs: '',
}, },
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
@@ -71,6 +70,7 @@ function TicketPage() {
const selectedOrganization = watch('organization'); const selectedOrganization = watch('organization');
const user = useUserData(); const user = useUserData();
const token = useAccessToken();
const { data: organizationsData } = useGetOrganizationsQuery({ const { data: organizationsData } = useGetOrganizationsQuery({
variables: { variables: {
@@ -91,56 +91,32 @@ function TicketPage() {
}; };
const handleSubmit = async (formValues: CreateTicketFormValues) => { const handleSubmit = async (formValues: CreateTicketFormValues) => {
const { project, services, priority, subject, description, ccs } = const { project, services, priority, subject, description } = formValues;
formValues;
const auth = btoa(
`${process.env.NEXT_PUBLIC_ZENDESK_USER_EMAIL}/token:${process.env.NEXT_PUBLIC_ZENDESK_API_KEY}`,
);
const emails = ccs
?.replace(/ /g, '')
.split(',')
.map((email) => ({ user_email: email }));
await execPromiseWithErrorToast( await execPromiseWithErrorToast(
async () => { async () => {
await fetch( const response = await fetch('/api/support/create-ticket', {
`${process.env.NEXT_PUBLIC_ZENDESK_URL}/api/v2/requests.json`, method: 'POST',
{ headers: {
method: 'POST', 'Content-Type': 'application/json',
headers: { Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
Authorization: `Basic ${auth}`,
},
body: JSON.stringify({
request: {
subject,
comment: {
body: description,
},
priority,
requester: {
name: user?.displayName,
email: user?.email,
},
email_ccs: emails,
custom_fields: [
// these custom field IDs come from zendesk
{
id: 19502784542098,
value: project,
},
{
id: 19922709880978,
value: services.map((service) =>
service.value?.toLowerCase(),
),
},
],
},
}),
}, },
); body: JSON.stringify({
project,
services,
priority,
subject,
description,
userName: user?.displayName,
userEmail: user?.email,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to create ticket');
}
form.reset(); form.reset();
}, },
{ {
@@ -191,7 +167,7 @@ function TicketPage() {
> >
{organizations.map((organization) => ( {organizations.map((organization) => (
<Option <Option
key={organization.name} key={organization.id}
value={organization.id} value={organization.id}
label={organization.name} label={organization.name}
> >
@@ -261,6 +237,8 @@ function TicketPage() {
slotProps={{ slotProps={{
root: { className: 'grid grid-flow-col gap-1 mb-4' }, root: { className: 'grid grid-flow-col gap-1 mb-4' },
}} }}
error={!!errors.priority}
helperText={errors.priority?.message}
renderValue={(option) => ( renderValue={(option) => (
<span className="inline-grid grid-flow-col items-center gap-2"> <span className="inline-grid grid-flow-col items-center gap-2">
{option?.label} {option?.label}
@@ -310,7 +288,6 @@ function TicketPage() {
label="Subject" label="Subject"
placeholder="Summary of the problem you are experiencing" placeholder="Summary of the problem you are experiencing"
fullWidth fullWidth
autoFocus
inputProps={{ min: 2, max: 128 }} inputProps={{ min: 2, max: 128 }}
error={!!errors.subject} error={!!errors.subject}
helperText={errors.subject?.message} helperText={errors.subject?.message}
@@ -330,31 +307,16 @@ function TicketPage() {
helperText={errors.description?.message} helperText={errors.description?.message}
/> />
<Divider /> <Box className="ml-auto flex flex-col gap-4 lg:w-80">
<Text className="mt-4 font-bold">Notifications</Text>
<StyledInput
{...register('ccs')}
id="ccs"
label="CCs"
placeholder="Comma separated list of emails you want to share this ticket with."
fullWidth
inputProps={{ min: 2, max: 128 }}
error={!!errors.ccs}
helperText={errors.ccs?.message}
/>
<Box className="ml-auto flex w-80 flex-col gap-4">
<Text color="secondary" className="text-right text-sm"> <Text color="secondary" className="text-right text-sm">
We will contact you at <strong>{user?.email}</strong> We will contact you at <strong>{user?.email}</strong>
</Text> </Text>
<Button <Button
variant="outlined" variant="outlined"
className="hover:!bg-white hover:!bg-opacity-10 focus:ring-0" className="text-base hover:!bg-white hover:!bg-opacity-10 focus:ring-0"
size="large" size="large"
type="submit" type="submit"
startIcon={<EnvelopeIcon />} startIcon={<Mail className="size-4" />}
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting} loading={isSubmitting}
> >
@@ -373,7 +335,7 @@ function TicketPage() {
TicketPage.getLayout = function getLayout(page: ReactElement) { TicketPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AuthenticatedLayout title="Help & Support | Nhost"> <AuthenticatedLayout title="Help & Support | Nhost" withMainNav={false}>
{page} {page}
</AuthenticatedLayout> </AuthenticatedLayout>
); );

View File

@@ -1,19 +1,27 @@
import {
clearGitHubToken,
saveGitHubToken,
type GitHubProviderToken,
} from '@/features/orgs/projects/git/common/utils';
import { isNotEmptyValue } from '@/lib/utils';
import { useNhostClient } from '@/providers/nhost/'; import { useNhostClient } from '@/providers/nhost/';
import { useGetAuthUserProvidersLazyQuery } from '@/utils/__generated__/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { getToastStyleProps } from '@/utils/constants/settings';
import { type Session } from '@nhost/nhost-js/auth'; import { type Session } from '@nhost/nhost-js/auth';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { import {
type PropsWithChildren,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useState, useState,
type PropsWithChildren,
} from 'react'; } from 'react';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { AuthContext, type AuthContextType } from './AuthContext'; import { AuthContext, type AuthContextType } from './AuthContext';
function AuthProvider({ children }: PropsWithChildren) { function AuthProvider({ children }: PropsWithChildren) {
const nhost = useNhostClient(); const nhost = useNhostClient();
const [getAuthUserProviders] = useGetAuthUserProvidersLazyQuery();
const { const {
query, query,
isReady: isRouterReady, isReady: isRouterReady,
@@ -21,7 +29,15 @@ function AuthProvider({ children }: PropsWithChildren) {
pathname, pathname,
push, push,
} = useRouter(); } = useRouter();
const { refreshToken, error, errorDescription, ...remainingQuery } = query; const {
refreshToken,
error,
errorDescription,
signinProvider,
state,
provider_state: providerState,
...remainingQuery
} = query;
const [session, setSession] = useState<Session | null>(null); const [session, setSession] = useState<Session | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSigningOut, setIsSigningOut] = useState(false); const [isSigningOut, setIsSigningOut] = useState(false);
@@ -55,7 +71,6 @@ function AuthProvider({ children }: PropsWithChildren) {
return; return;
} }
setIsLoading(true); setIsLoading(true);
// reset state if we have just signed out
setIsSigningOut(false); setIsSigningOut(false);
if (refreshToken && typeof refreshToken === 'string') { if (refreshToken && typeof refreshToken === 'string') {
const sessionResponse = await nhost.auth.refreshToken({ const sessionResponse = await nhost.auth.refreshToken({
@@ -63,24 +78,84 @@ function AuthProvider({ children }: PropsWithChildren) {
}); });
setSession(sessionResponse.body); setSession(sessionResponse.body);
removeQueryParamsFromURL(); removeQueryParamsFromURL();
if (sessionResponse.body && signinProvider === 'github') {
try {
const providerTokensResponse =
await nhost.auth.getProviderTokens(signinProvider);
if (providerTokensResponse.body) {
const { data } = await getAuthUserProviders();
const githubProvider = data?.authUserProviders?.find(
(provider) => provider.providerId === 'github',
);
const newGitHubToken: GitHubProviderToken =
providerTokensResponse.body;
if (isNotEmptyValue(githubProvider?.id)) {
newGitHubToken.authUserProviderId = githubProvider!.id;
}
saveGitHubToken(newGitHubToken);
}
} catch (err) {
console.error('Failed to fetch provider tokens:', err);
}
}
} else { } else {
const currentSession = nhost.getUserSession(); const currentSession = nhost.getUserSession();
setSession(currentSession); setSession(currentSession);
} }
// handle OAuth redirect errors (e.g., error=unverified-user) if (
state &&
typeof state === 'string' &&
state.startsWith('signin-refresh:')
) {
const [, orgSlug, projectSubdomain] = state.split(':');
removeQueryParamsFromURL();
await push(
`/orgs/${orgSlug}/projects/${projectSubdomain}/settings/git?github-modal`,
);
}
if (typeof error === 'string') { if (typeof error === 'string') {
if (error === 'unverified-user') { switch (error) {
removeQueryParamsFromURL(); case 'unverified-user': {
await push('/email/verify'); removeQueryParamsFromURL();
} else { await push('/email/verify');
const description = break;
typeof errorDescription === 'string' }
? errorDescription
: 'An error occurred during the sign-in process. Please try again.'; /*
toast.error(description, getToastStyleProps()); * If the state isn't handled by Hasura auth, it returns `invalid-state`.
removeQueryParamsFromURL(); * However, we check the provider_state search param to see if it has this format:
await push('/signin'); * `install-github-app:<org-slug>:<project-subdomain>`.
* If it has this format, that means that we connected to GitHub in `/settings/git`,
* thus we need to show the connect GitHub modal again.
* Otherwise, we fall through to default error handling.
*/
case 'invalid-state': {
if (
isNotEmptyValue(providerState) &&
typeof providerState === 'string' &&
providerState.startsWith('install-github-app:')
) {
const [, orgSlug, projectSubdomain] = providerState.split(':');
removeQueryParamsFromURL();
await push(
`/orgs/${orgSlug}/projects/${projectSubdomain}/settings/git?github-modal`,
);
break;
}
// Fall through to default error handling if state search param is invalid
}
default: {
const description =
typeof errorDescription === 'string'
? errorDescription
: 'An error occurred during the sign-in process. Please try again.';
toast.error(description, getToastStyleProps());
removeQueryParamsFromURL();
await push('/signin');
}
} }
} }
@@ -103,6 +178,7 @@ function AuthProvider({ children }: PropsWithChildren) {
nhost.auth.signOut({ nhost.auth.signOut({
refreshToken: session!.refreshToken, refreshToken: session!.refreshToken,
}); });
clearGitHubToken();
await push('/signin'); await push('/signin');
}, },

View File

@@ -108,16 +108,16 @@ function Providers({ children }: PropsWithChildren<{}>) {
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<CacheProvider value={emotionCache}> <CacheProvider value={emotionCache}>
<NhostProvider nhost={nhost}> <NhostProvider nhost={nhost}>
<AuthProvider> <ApolloProvider client={mockClient}>
<ApolloProvider client={mockClient}> <AuthProvider>
<UIProvider> <UIProvider>
<Toaster position="bottom-center" /> <Toaster position="bottom-center" />
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<DialogProvider>{children}</DialogProvider> <DialogProvider>{children}</DialogProvider>
</ThemeProvider> </ThemeProvider>
</UIProvider> </UIProvider>
</ApolloProvider> </AuthProvider>
</AuthProvider> </ApolloProvider>
</NhostProvider> </NhostProvider>
</CacheProvider> </CacheProvider>
</QueryClientProvider> </QueryClientProvider>

View File

@@ -3118,7 +3118,9 @@ export type ConfigSystemConfigPostgres = {
database: Scalars['String']; database: Scalars['String'];
disk?: Maybe<ConfigSystemConfigPostgresDisk>; disk?: Maybe<ConfigSystemConfigPostgresDisk>;
enabled?: Maybe<Scalars['Boolean']>; enabled?: Maybe<Scalars['Boolean']>;
encryptColumnKey?: Maybe<Scalars['String']>;
majorVersion?: Maybe<Scalars['String']>; majorVersion?: Maybe<Scalars['String']>;
oldEncryptColumnKey?: Maybe<Scalars['String']>;
}; };
export type ConfigSystemConfigPostgresComparisonExp = { export type ConfigSystemConfigPostgresComparisonExp = {
@@ -3129,7 +3131,9 @@ export type ConfigSystemConfigPostgresComparisonExp = {
database?: InputMaybe<ConfigStringComparisonExp>; database?: InputMaybe<ConfigStringComparisonExp>;
disk?: InputMaybe<ConfigSystemConfigPostgresDiskComparisonExp>; disk?: InputMaybe<ConfigSystemConfigPostgresDiskComparisonExp>;
enabled?: InputMaybe<ConfigBooleanComparisonExp>; enabled?: InputMaybe<ConfigBooleanComparisonExp>;
encryptColumnKey?: InputMaybe<ConfigStringComparisonExp>;
majorVersion?: InputMaybe<ConfigStringComparisonExp>; majorVersion?: InputMaybe<ConfigStringComparisonExp>;
oldEncryptColumnKey?: InputMaybe<ConfigStringComparisonExp>;
}; };
export type ConfigSystemConfigPostgresConnectionString = { export type ConfigSystemConfigPostgresConnectionString = {
@@ -3193,7 +3197,9 @@ export type ConfigSystemConfigPostgresInsertInput = {
database: Scalars['String']; database: Scalars['String'];
disk?: InputMaybe<ConfigSystemConfigPostgresDiskInsertInput>; disk?: InputMaybe<ConfigSystemConfigPostgresDiskInsertInput>;
enabled?: InputMaybe<Scalars['Boolean']>; enabled?: InputMaybe<Scalars['Boolean']>;
encryptColumnKey?: InputMaybe<Scalars['String']>;
majorVersion?: InputMaybe<Scalars['String']>; majorVersion?: InputMaybe<Scalars['String']>;
oldEncryptColumnKey?: InputMaybe<Scalars['String']>;
}; };
export type ConfigSystemConfigPostgresUpdateInput = { export type ConfigSystemConfigPostgresUpdateInput = {
@@ -3201,7 +3207,9 @@ export type ConfigSystemConfigPostgresUpdateInput = {
database?: InputMaybe<Scalars['String']>; database?: InputMaybe<Scalars['String']>;
disk?: InputMaybe<ConfigSystemConfigPostgresDiskUpdateInput>; disk?: InputMaybe<ConfigSystemConfigPostgresDiskUpdateInput>;
enabled?: InputMaybe<Scalars['Boolean']>; enabled?: InputMaybe<Scalars['Boolean']>;
encryptColumnKey?: InputMaybe<Scalars['String']>;
majorVersion?: InputMaybe<Scalars['String']>; majorVersion?: InputMaybe<Scalars['String']>;
oldEncryptColumnKey?: InputMaybe<Scalars['String']>;
}; };
export type ConfigSystemConfigUpdateInput = { export type ConfigSystemConfigUpdateInput = {
@@ -4426,6 +4434,7 @@ export type Apps = {
billingDedicatedCompute?: Maybe<Billing_Dedicated_Compute>; billingDedicatedCompute?: Maybe<Billing_Dedicated_Compute>;
/** An object relationship */ /** An object relationship */
billingSubscriptions?: Maybe<Billing_Subscriptions>; billingSubscriptions?: Maybe<Billing_Subscriptions>;
/** main entrypoint to the configuration */
config?: Maybe<ConfigConfig>; config?: Maybe<ConfigConfig>;
createdAt: Scalars['timestamptz']; createdAt: Scalars['timestamptz'];
/** An object relationship */ /** An object relationship */
@@ -11094,6 +11103,7 @@ export type Deployments = {
commitSHA: Scalars['String']; commitSHA: Scalars['String'];
commitUserAvatarUrl?: Maybe<Scalars['String']>; commitUserAvatarUrl?: Maybe<Scalars['String']>;
commitUserName?: Maybe<Scalars['String']>; commitUserName?: Maybe<Scalars['String']>;
createdAt: Scalars['timestamptz'];
deploymentEndedAt?: Maybe<Scalars['timestamptz']>; deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
/** An array relationship */ /** An array relationship */
deploymentLogs: Array<DeploymentLogs>; deploymentLogs: Array<DeploymentLogs>;
@@ -11191,6 +11201,7 @@ export type Deployments_Bool_Exp = {
commitSHA?: InputMaybe<String_Comparison_Exp>; commitSHA?: InputMaybe<String_Comparison_Exp>;
commitUserAvatarUrl?: InputMaybe<String_Comparison_Exp>; commitUserAvatarUrl?: InputMaybe<String_Comparison_Exp>;
commitUserName?: InputMaybe<String_Comparison_Exp>; commitUserName?: InputMaybe<String_Comparison_Exp>;
createdAt?: InputMaybe<Timestamptz_Comparison_Exp>;
deploymentEndedAt?: InputMaybe<Timestamptz_Comparison_Exp>; deploymentEndedAt?: InputMaybe<Timestamptz_Comparison_Exp>;
deploymentLogs?: InputMaybe<DeploymentLogs_Bool_Exp>; deploymentLogs?: InputMaybe<DeploymentLogs_Bool_Exp>;
deploymentLogs_aggregate?: InputMaybe<DeploymentLogs_Aggregate_Bool_Exp>; deploymentLogs_aggregate?: InputMaybe<DeploymentLogs_Aggregate_Bool_Exp>;
@@ -11222,6 +11233,7 @@ export type Deployments_Insert_Input = {
commitSHA?: InputMaybe<Scalars['String']>; commitSHA?: InputMaybe<Scalars['String']>;
commitUserAvatarUrl?: InputMaybe<Scalars['String']>; commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
commitUserName?: InputMaybe<Scalars['String']>; commitUserName?: InputMaybe<Scalars['String']>;
createdAt?: InputMaybe<Scalars['timestamptz']>;
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>; deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
deploymentLogs?: InputMaybe<DeploymentLogs_Arr_Rel_Insert_Input>; deploymentLogs?: InputMaybe<DeploymentLogs_Arr_Rel_Insert_Input>;
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>; deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
@@ -11246,6 +11258,7 @@ export type Deployments_Max_Fields = {
commitSHA?: Maybe<Scalars['String']>; commitSHA?: Maybe<Scalars['String']>;
commitUserAvatarUrl?: Maybe<Scalars['String']>; commitUserAvatarUrl?: Maybe<Scalars['String']>;
commitUserName?: Maybe<Scalars['String']>; commitUserName?: Maybe<Scalars['String']>;
createdAt?: Maybe<Scalars['timestamptz']>;
deploymentEndedAt?: Maybe<Scalars['timestamptz']>; deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
deploymentStartedAt?: Maybe<Scalars['timestamptz']>; deploymentStartedAt?: Maybe<Scalars['timestamptz']>;
deploymentStatus?: Maybe<Scalars['String']>; deploymentStatus?: Maybe<Scalars['String']>;
@@ -11268,6 +11281,7 @@ export type Deployments_Max_Order_By = {
commitSHA?: InputMaybe<Order_By>; commitSHA?: InputMaybe<Order_By>;
commitUserAvatarUrl?: InputMaybe<Order_By>; commitUserAvatarUrl?: InputMaybe<Order_By>;
commitUserName?: InputMaybe<Order_By>; commitUserName?: InputMaybe<Order_By>;
createdAt?: InputMaybe<Order_By>;
deploymentEndedAt?: InputMaybe<Order_By>; deploymentEndedAt?: InputMaybe<Order_By>;
deploymentStartedAt?: InputMaybe<Order_By>; deploymentStartedAt?: InputMaybe<Order_By>;
deploymentStatus?: InputMaybe<Order_By>; deploymentStatus?: InputMaybe<Order_By>;
@@ -11291,6 +11305,7 @@ export type Deployments_Min_Fields = {
commitSHA?: Maybe<Scalars['String']>; commitSHA?: Maybe<Scalars['String']>;
commitUserAvatarUrl?: Maybe<Scalars['String']>; commitUserAvatarUrl?: Maybe<Scalars['String']>;
commitUserName?: Maybe<Scalars['String']>; commitUserName?: Maybe<Scalars['String']>;
createdAt?: Maybe<Scalars['timestamptz']>;
deploymentEndedAt?: Maybe<Scalars['timestamptz']>; deploymentEndedAt?: Maybe<Scalars['timestamptz']>;
deploymentStartedAt?: Maybe<Scalars['timestamptz']>; deploymentStartedAt?: Maybe<Scalars['timestamptz']>;
deploymentStatus?: Maybe<Scalars['String']>; deploymentStatus?: Maybe<Scalars['String']>;
@@ -11313,6 +11328,7 @@ export type Deployments_Min_Order_By = {
commitSHA?: InputMaybe<Order_By>; commitSHA?: InputMaybe<Order_By>;
commitUserAvatarUrl?: InputMaybe<Order_By>; commitUserAvatarUrl?: InputMaybe<Order_By>;
commitUserName?: InputMaybe<Order_By>; commitUserName?: InputMaybe<Order_By>;
createdAt?: InputMaybe<Order_By>;
deploymentEndedAt?: InputMaybe<Order_By>; deploymentEndedAt?: InputMaybe<Order_By>;
deploymentStartedAt?: InputMaybe<Order_By>; deploymentStartedAt?: InputMaybe<Order_By>;
deploymentStatus?: InputMaybe<Order_By>; deploymentStatus?: InputMaybe<Order_By>;
@@ -11359,6 +11375,7 @@ export type Deployments_Order_By = {
commitSHA?: InputMaybe<Order_By>; commitSHA?: InputMaybe<Order_By>;
commitUserAvatarUrl?: InputMaybe<Order_By>; commitUserAvatarUrl?: InputMaybe<Order_By>;
commitUserName?: InputMaybe<Order_By>; commitUserName?: InputMaybe<Order_By>;
createdAt?: InputMaybe<Order_By>;
deploymentEndedAt?: InputMaybe<Order_By>; deploymentEndedAt?: InputMaybe<Order_By>;
deploymentLogs_aggregate?: InputMaybe<DeploymentLogs_Aggregate_Order_By>; deploymentLogs_aggregate?: InputMaybe<DeploymentLogs_Aggregate_Order_By>;
deploymentStartedAt?: InputMaybe<Order_By>; deploymentStartedAt?: InputMaybe<Order_By>;
@@ -11393,6 +11410,8 @@ export enum Deployments_Select_Column {
/** column name */ /** column name */
CommitUserName = 'commitUserName', CommitUserName = 'commitUserName',
/** column name */ /** column name */
CreatedAt = 'createdAt',
/** column name */
DeploymentEndedAt = 'deploymentEndedAt', DeploymentEndedAt = 'deploymentEndedAt',
/** column name */ /** column name */
DeploymentStartedAt = 'deploymentStartedAt', DeploymentStartedAt = 'deploymentStartedAt',
@@ -11427,6 +11446,7 @@ export type Deployments_Set_Input = {
commitSHA?: InputMaybe<Scalars['String']>; commitSHA?: InputMaybe<Scalars['String']>;
commitUserAvatarUrl?: InputMaybe<Scalars['String']>; commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
commitUserName?: InputMaybe<Scalars['String']>; commitUserName?: InputMaybe<Scalars['String']>;
createdAt?: InputMaybe<Scalars['timestamptz']>;
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>; deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>; deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
deploymentStatus?: InputMaybe<Scalars['String']>; deploymentStatus?: InputMaybe<Scalars['String']>;
@@ -11457,6 +11477,7 @@ export type Deployments_Stream_Cursor_Value_Input = {
commitSHA?: InputMaybe<Scalars['String']>; commitSHA?: InputMaybe<Scalars['String']>;
commitUserAvatarUrl?: InputMaybe<Scalars['String']>; commitUserAvatarUrl?: InputMaybe<Scalars['String']>;
commitUserName?: InputMaybe<Scalars['String']>; commitUserName?: InputMaybe<Scalars['String']>;
createdAt?: InputMaybe<Scalars['timestamptz']>;
deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>; deploymentEndedAt?: InputMaybe<Scalars['timestamptz']>;
deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>; deploymentStartedAt?: InputMaybe<Scalars['timestamptz']>;
deploymentStatus?: InputMaybe<Scalars['String']>; deploymentStatus?: InputMaybe<Scalars['String']>;
@@ -11485,6 +11506,8 @@ export enum Deployments_Update_Column {
/** column name */ /** column name */
CommitUserName = 'commitUserName', CommitUserName = 'commitUserName',
/** column name */ /** column name */
CreatedAt = 'createdAt',
/** column name */
DeploymentEndedAt = 'deploymentEndedAt', DeploymentEndedAt = 'deploymentEndedAt',
/** column name */ /** column name */
DeploymentStartedAt = 'deploymentStartedAt', DeploymentStartedAt = 'deploymentStartedAt',
@@ -13067,6 +13090,7 @@ export type Mutation_Root = {
billingUpgradeFreeOrganization: Scalars['String']; billingUpgradeFreeOrganization: Scalars['String'];
billingUploadReports: Scalars['Boolean']; billingUploadReports: Scalars['Boolean'];
changeDatabaseVersion: Scalars['Boolean']; changeDatabaseVersion: Scalars['Boolean'];
connectGithubRepo: Scalars['Boolean'];
/** delete single row from the table: "announcements_read" */ /** delete single row from the table: "announcements_read" */
deleteAnnouncementRead?: Maybe<Announcements_Read>; deleteAnnouncementRead?: Maybe<Announcements_Read>;
/** delete data from the table: "announcements_read" */ /** delete data from the table: "announcements_read" */
@@ -13968,6 +13992,15 @@ export type Mutation_RootChangeDatabaseVersionArgs = {
}; };
/** mutation root */
export type Mutation_RootConnectGithubRepoArgs = {
appID: Scalars['uuid'];
baseFolder: Scalars['String'];
githubNodeID: Scalars['String'];
productionBranch: Scalars['String'];
};
/** mutation root */ /** mutation root */
export type Mutation_RootDeleteAnnouncementReadArgs = { export type Mutation_RootDeleteAnnouncementReadArgs = {
id: Scalars['uuid']; id: Scalars['uuid'];
@@ -27517,6 +27550,16 @@ export type ResetDatabasePasswordMutationVariables = Exact<{
export type ResetDatabasePasswordMutation = { __typename?: 'mutation_root', resetPostgresPassword: boolean }; export type ResetDatabasePasswordMutation = { __typename?: 'mutation_root', resetPostgresPassword: boolean };
export type ConnectGithubRepoMutationVariables = Exact<{
appID: Scalars['uuid'];
githubNodeID: Scalars['String'];
productionBranch: Scalars['String'];
baseFolder: Scalars['String'];
}>;
export type ConnectGithubRepoMutation = { __typename?: 'mutation_root', connectGithubRepo: boolean };
export type GetHasuraSettingsQueryVariables = Exact<{ export type GetHasuraSettingsQueryVariables = Exact<{
appId: Scalars['uuid']; appId: Scalars['uuid'];
}>; }>;
@@ -29446,6 +29489,45 @@ export function useResetDatabasePasswordMutation(baseOptions?: Apollo.MutationHo
export type ResetDatabasePasswordMutationHookResult = ReturnType<typeof useResetDatabasePasswordMutation>; export type ResetDatabasePasswordMutationHookResult = ReturnType<typeof useResetDatabasePasswordMutation>;
export type ResetDatabasePasswordMutationResult = Apollo.MutationResult<ResetDatabasePasswordMutation>; export type ResetDatabasePasswordMutationResult = Apollo.MutationResult<ResetDatabasePasswordMutation>;
export type ResetDatabasePasswordMutationOptions = Apollo.BaseMutationOptions<ResetDatabasePasswordMutation, ResetDatabasePasswordMutationVariables>; export type ResetDatabasePasswordMutationOptions = Apollo.BaseMutationOptions<ResetDatabasePasswordMutation, ResetDatabasePasswordMutationVariables>;
export const ConnectGithubRepoDocument = gql`
mutation ConnectGithubRepo($appID: uuid!, $githubNodeID: String!, $productionBranch: String!, $baseFolder: String!) {
connectGithubRepo(
appID: $appID
githubNodeID: $githubNodeID
productionBranch: $productionBranch
baseFolder: $baseFolder
)
}
`;
export type ConnectGithubRepoMutationFn = Apollo.MutationFunction<ConnectGithubRepoMutation, ConnectGithubRepoMutationVariables>;
/**
* __useConnectGithubRepoMutation__
*
* To run a mutation, you first call `useConnectGithubRepoMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useConnectGithubRepoMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [connectGithubRepoMutation, { data, loading, error }] = useConnectGithubRepoMutation({
* variables: {
* appID: // value for 'appID'
* githubNodeID: // value for 'githubNodeID'
* productionBranch: // value for 'productionBranch'
* baseFolder: // value for 'baseFolder'
* },
* });
*/
export function useConnectGithubRepoMutation(baseOptions?: Apollo.MutationHookOptions<ConnectGithubRepoMutation, ConnectGithubRepoMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ConnectGithubRepoMutation, ConnectGithubRepoMutationVariables>(ConnectGithubRepoDocument, options);
}
export type ConnectGithubRepoMutationHookResult = ReturnType<typeof useConnectGithubRepoMutation>;
export type ConnectGithubRepoMutationResult = Apollo.MutationResult<ConnectGithubRepoMutation>;
export type ConnectGithubRepoMutationOptions = Apollo.BaseMutationOptions<ConnectGithubRepoMutation, ConnectGithubRepoMutationVariables>;
export const GetHasuraSettingsDocument = gql` export const GetHasuraSettingsDocument = gql`
query GetHasuraSettings($appId: uuid!) { query GetHasuraSettings($appId: uuid!) {
config(appID: $appId, resolve: false) { config(appID: $appId, resolve: false) {

View File

@@ -4,7 +4,7 @@ import {
getGraphqlServiceUrl, getGraphqlServiceUrl,
getStorageServiceUrl, getStorageServiceUrl,
} from '@/utils/env'; } from '@/utils/env';
import { createClient } from '@nhost/nhost-js'; import { createClient, createNhostClient } from '@nhost/nhost-js';
import { type Session, type SessionStorageBackend } from '@nhost/nhost-js/session'; import { type Session, type SessionStorageBackend } from '@nhost/nhost-js/session';
const nhost = createClient({ const nhost = createClient({
@@ -14,6 +14,13 @@ const nhost = createClient({
storageUrl: getStorageServiceUrl(), storageUrl: getStorageServiceUrl(),
}); });
const nhostRoutesClient = createNhostClient({
authUrl: getAuthServiceUrl(),
graphqlUrl: getGraphqlServiceUrl(),
functionsUrl: getFunctionsServiceUrl(),
storageUrl: getStorageServiceUrl(),
});
export class DummySessionStorage implements SessionStorageBackend { export class DummySessionStorage implements SessionStorageBackend {
private session: Session | null = null; private session: Session | null = null;
@@ -41,4 +48,5 @@ export class DummySessionStorage implements SessionStorageBackend {
} }
} }
export { nhostRoutesClient };
export default nhost; export default nhost;

View File

@@ -122,29 +122,37 @@
"group": "Sign In Methods", "group": "Sign In Methods",
"pages": [ "pages": [
{ {
"group": "Social Providers", "group": "Providers",
"icon": "at", "icon": "at",
"pages": [ "pages": [
"products/auth/social/sign-in-apple", "products/auth/providers/overview",
"products/auth/social/sign-in-azuread", "products/auth/providers/tokens",
"products/auth/social/sign-in-discord", "products/auth/providers/connect",
"products/auth/social/sign-in-entraid", "products/auth/providers/idtokens",
"products/auth/social/sign-in-facebook", {
"products/auth/social/sign-in-github", "group": "Configuration",
"products/auth/social/sign-in-google", "icon": "gear",
"products/auth/social/sign-in-linkedin", "pages": [
"products/auth/social/sign-in-spotify", "products/auth/providers/sign-in-apple",
"products/auth/social/sign-in-twitch", "products/auth/providers/sign-in-azuread",
"products/auth/social/sign-in-workos" "products/auth/providers/sign-in-discord",
"products/auth/providers/sign-in-entraid",
"products/auth/providers/sign-in-facebook",
"products/auth/providers/sign-in-github",
"products/auth/providers/sign-in-google",
"products/auth/providers/sign-in-linkedin",
"products/auth/providers/sign-in-spotify",
"products/auth/providers/sign-in-twitch",
"products/auth/providers/sign-in-workos"
]
}
] ]
}, },
"/products/auth/social-connect",
"/products/auth/sign-in-email-password", "/products/auth/sign-in-email-password",
"/products/auth/sign-in-otp", "/products/auth/sign-in-otp",
"/products/auth/sign-in-magic-link", "/products/auth/sign-in-magic-link",
"/products/auth/sign-in-sms-otp", "/products/auth/sign-in-sms-otp",
"/products/auth/webauthn", "/products/auth/webauthn"
"/products/auth/idtokens"
] ]
}, },
{ {
@@ -152,7 +160,6 @@
"icon": "diagram-project", "icon": "diagram-project",
"pages": [ "pages": [
"/products/auth/workflows/email-password", "/products/auth/workflows/email-password",
"/products/auth/workflows/oauth-providers",
"/products/auth/workflows/passwordless-email", "/products/auth/workflows/passwordless-email",
"/products/auth/workflows/passwordless-sms", "/products/auth/workflows/passwordless-sms",
"/products/auth/workflows/webauthn", "/products/auth/workflows/webauthn",
@@ -352,6 +359,7 @@
"reference/auth/post-signin-pat", "reference/auth/post-signin-pat",
"reference/auth/get-signin-provider-{provider}", "reference/auth/get-signin-provider-{provider}",
"reference/auth/get-signin-provider-{provider}-callback", "reference/auth/get-signin-provider-{provider}-callback",
"reference/auth/get-signin-provider-{provider}-callback-tokens",
"reference/auth/post-signin-provider-{provider}-callback", "reference/auth/post-signin-provider-{provider}-callback",
"reference/auth/post-signin-webauthn", "reference/auth/post-signin-webauthn",
"reference/auth/post-signin-webauthn-verify", "reference/auth/post-signin-webauthn-verify",
@@ -360,6 +368,7 @@
"reference/auth/post-signup-webauthn", "reference/auth/post-signup-webauthn",
"reference/auth/post-signup-webauthn-verify", "reference/auth/post-signup-webauthn-verify",
"reference/auth/post-token", "reference/auth/post-token",
"reference/auth/post-token-provider-{provider}",
"reference/auth/post-token-verify", "reference/auth/post-token-verify",
"reference/auth/get-user", "reference/auth/get-user",
"reference/auth/post-user-deanonymize", "reference/auth/post-user-deanonymize",
@@ -368,7 +377,6 @@
"reference/auth/post-user-mfa", "reference/auth/post-user-mfa",
"reference/auth/post-user-password", "reference/auth/post-user-password",
"reference/auth/post-user-password-reset", "reference/auth/post-user-password-reset",
"reference/auth/get-user-provider-{provider}-tokens",
"reference/auth/post-user-webauthn-add", "reference/auth/post-user-webauthn-add",
"reference/auth/post-user-webauthn-verify", "reference/auth/post-user-webauthn-verify",
"reference/auth/get-verify", "reference/auth/get-verify",
@@ -705,6 +713,20 @@
} }
] ]
}, },
"redirects": [
{
"source": "/products/auth/social/:slug*",
"destination": "/products/auth/providers/:slug*"
},
{
"source": "/products/auth/social-connect",
"destination": "products/auth/providers/connect"
},
{
"source": "/products/auth/idtokens",
"destination": "products/auth/providers/idtokens"
}
],
"logo": { "logo": {
"light": "/images/logo/light.svg", "light": "/images/logo/light.svg",
"dark": "/images/logo/dark.svg" "dark": "/images/logo/dark.svg"

View File

@@ -62,6 +62,7 @@ function build_typedoc() {
function build_cli_docs() { function build_cli_docs() {
echo "⚒️⚒️⚒️ Building CLI documentation..." echo "⚒️⚒️⚒️ Building CLI documentation..."
cli docs > reference/cli/commands.mdx cli docs > reference/cli/commands.mdx
cat reference/cli/commands.mdx
} }
build_openapi build_openapi

View File

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 245 KiB

View File

@@ -120,21 +120,26 @@ sender = 'myapp@mydomain.com'
It is very important that the `host` is set exactly to `postmark`. It is very important that the `host` is set exactly to `postmark`.
</Warning> </Warning>
2. In postmark you need to create thre templates for each locale with the following alias: 2. In postmark you need to create three templates for each locale with the following alias:
- $locale.email-change - $locale.email-confirm-change
- $locale.email-verify - $locale.email-verify
- $locale.password-reset - $locale.password-reset
For instance: For instance:
- en.email-change - en.email-confirm-change
- en.email-verify - en.email-verify
- en.password-reset - en.password-reset
- se.email-change - se.email-confirm-change
- se.email-verify - se.email-verify
- se.password-reset - se.password-reset
Additionally, if you want to use the passwordless sign-in or OTP sign-in emails, you need to create the following templates as well:
- $locale.signin-passwordless
- $locale.signin-otp
After these two steps have been completed, the Auth service will leverage these templates instead of the ones described in the previous section. After these two steps have been completed, the Auth service will leverage these templates instead of the ones described in the previous section.
<Note> <Note>

View File

@@ -19,7 +19,7 @@ Nhost Auth is a ready-to-use authentication service seamlessly integrated with t
</Card> </Card>
<Card title="Security Keys (WebAuthn)" icon="square-5" href="/products/auth/webauthn"> <Card title="Security Keys (WebAuthn)" icon="square-5" href="/products/auth/webauthn">
</Card> </Card>
<Card title="ID Tokens" icon="square-6" href="/products/auth/idtokens"> <Card title="ID Tokens" icon="square-6" href="/products/auth/providers/idtokens">
</Card> </Card>
<Card title="Elevated Permissions" icon="square-7" href="/products/auth/elevated-permissions"> <Card title="Elevated Permissions" icon="square-7" href="/products/auth/elevated-permissions">
</Card> </Card>
@@ -28,24 +28,24 @@ Nhost Auth is a ready-to-use authentication service seamlessly integrated with t
### OAuth Providers ### OAuth Providers
<CardGroup cols={4}> <CardGroup cols={4}>
<Card title="Apple" icon="apple" href="/products/auth/social/sign-in-apple"> <Card title="Apple" icon="apple" href="/products/auth/providers/sign-in-apple">
</Card> </Card>
<Card title="Discord" icon="discord" href="/products/auth/social/sign-in-discord"> <Card title="Discord" icon="discord" href="/products/auth/providers/sign-in-discord">
</Card> </Card>
<Card title="Entra ID" icon="microsoft" href="/products/auth/social/sign-in-entraid"> <Card title="Entra ID" icon="microsoft" href="/products/auth/providers/sign-in-entraid">
</Card> </Card>
<Card title="Facebook" icon="facebook" href="/products/auth/social/sign-in-facebook"> <Card title="Facebook" icon="facebook" href="/products/auth/providers/sign-in-facebook">
</Card> </Card>
<Card title="GitHub" icon="github" href="/products/auth/social/sign-in-github"> <Card title="GitHub" icon="github" href="/products/auth/providers/sign-in-github">
</Card> </Card>
<Card title="Google" icon="google" href="/products/auth/social/sign-in-google"> <Card title="Google" icon="google" href="/products/auth/providers/sign-in-google">
</Card> </Card>
<Card title="Linkedin" icon="linkedin" href="/products/auth/social/sign-in-linkedin"> <Card title="Linkedin" icon="linkedin" href="/products/auth/providers/sign-in-linkedin">
</Card> </Card>
<Card title="Spotify" icon="spotify" href="/products/auth/social/sign-in-spotify"> <Card title="Spotify" icon="spotify" href="/products/auth/providers/sign-in-spotify">
</Card> </Card>
<Card title="Twitch" icon="twitch" href="/products/auth/social/sign-in-twitch"> <Card title="Twitch" icon="twitch" href="/products/auth/providers/sign-in-twitch">
</Card> </Card>
<Card title="WorkOS" icon="square-9" href="/products/auth/social/sign-in-workos"> <Card title="WorkOS" icon="square-9" href="/products/auth/providers/sign-in-workos">
</Card> </Card>
</CardGroup> </CardGroup>

View File

@@ -0,0 +1,41 @@
---
title: Connecting existing users
sidebarTitle: Connecting existing users
description: Connect a provider to existing user accounts
icon: link
---
With the provider connect feature, users can link configured providers with their account, regardless of the initial sign-up method. It enables users to link different providers to their accounts, even if the email addresses do not match (e.g., linking a GitHub profile to an account registered with a different email). This feature offers flexibility, allowing users to streamline their login process by connecting multiple authentication methods.
To add a provider authentication method to an existing user you need to call the url `https://${subdomain}.auth.${region}.nhost.run/v1/signin/provider/${provider}?connect=${jwt}`. This is very easy to achieve with our SDK:
``` js
nhost.auth.connectProvider({
provider: 'github'
})
```
<Note>
Keep in mind that as we need a `JWT` the user needs to be logged in.
</Note>
## Viewing and Deleting Provider Authentication Mechanisms
If you want to allow your users to view and/or delete provider authentication mechanisms, you can provide the necessary permissions to the table `auth.user_providers` (i.e. `select` and/or `delete`) and then use the appropriate GraphQL query. For example, the following permissions should allow users to list their own providers:
![provider connect permissions](/images/auth/provider-connect-permissions.png)
Using the following GraphQL query:
``` js
const { error, data } = await nhost.graphql.request(
gql`
query getAuthUserProviders {
authUserProviders {
id
providerId
}
}
`,
)
```

View File

@@ -13,7 +13,9 @@ ID tokens serve as a secure proof that a user has already been authenticated by
## Usage ## Usage
To use ID tokens, you need to configure supported identity providers (currently [apple](/products/auth/social/sign-in-apple) and [google](/products/auth/social/sign-in-google)) and make sure the `audience` is set correctly. <Note>
To use ID tokens, you need to configure supported identity providers (currently [apple](/products/auth/providers/sign-in-apple) and [google](/products/auth/providers/sign-in-google)) and make sure the `audience` is set correctly.
</Note>
### Sign in ### Sign in
@@ -41,7 +43,7 @@ Once everything is configured you can use an ID token to authenticate users with
### Link Provider to existing user ### Link Provider to existing user
Similarly to the [Social Connect](/products/auth/social-connect) feature, you can link an identity provider to an existing user: Similarly to the [Provider Connect](/products/auth/providers/connect) feature, you can link an identity provider to an existing user:
<Tabs> <Tabs>
<Tab title="javascript"> <Tab title="javascript">
@@ -69,10 +71,4 @@ Below you can find some examples on how to extract an ID Token from various iden
### React Native ### React Native
#### Apple Please, refer to our [React Native Tutorial](/getting-started/tutorials/reactnative/1-introduction) on how to implement Sign in with Apple and Google Sign-In in your React Native app.
For an example on how to authenticate using "Sign in with Apple" on iOS using React Native you can refer to our [sample component](https://github.com/nhost/nhost/blob/main/examples/react_native/src/components/SignInWithAppleButton.tsx).
#### Google
For an example on how to authenticate using "Sign in with Google" on Android using React Native you can refer to our [sample component](https://github.com/nhost/nhost/blob/main/examples/react_native/src/components/SignInWithGoogleButton.tsx).

View File

@@ -0,0 +1,61 @@
---
title: Overview
sidebarTitle: Overview
description: Learn how OAuth2 authentication works with Nhost
icon: at
---
OAuth2 providers allow users to sign in to your Nhost application using their existing accounts from popular services like Google, GitHub, Apple, and many others. This eliminates the need for users to create and remember new credentials while providing a secure authentication method.
## How OAuth2 Authentication Works
When a user authenticates with an OAuth2 provider through Nhost, the following workflow occurs:
```mermaid
sequenceDiagram
autonumber
actor U as User
participant A as Auth
participant P as Oauth Provider
participant F as Frontend
U->>+A: HTTP GET /signin/provider/{provider}
A->>+P: Provider's authentication
deactivate A
P->>-A: HTTP GET /signin/provider/{provider}/callback
activate A
opt No user found
A->>A: Create user
end
A->>A: Flag user email as verified
A->>+F: HTTP redirect with refresh token
deactivate A
F->>-U: HTTP OK response
opt
U->>+A: HTTP POST /token
A->>-U: HTTP OK response
Note left of A: Refresh token + access token
end
```
<Snippet file="workflows/oauth-providers.mdx" />
### The Authentication Flow
1. **Initiation**: The user starts the authentication process by making a request to `/signin/provider/{provider}` (e.g., `/signin/provider/google`)
2. **Provider Authentication**: Auth redirects the user to the OAuth2 provider's authentication page where they log in and grant permissions
3. **Callback**: After successful authentication, the provider redirects back to Auth at `/signin/provider/{provider}/callback` with an authorization code
4. **User Management**: Auth processes the callback:
- If this is a new user, a user account is automatically created
- The user's email is flagged as verified (since the OAuth2 provider has already verified it)
5. **Token Issuance**: The user is redirected back to your frontend application with a refresh token
## Benefits of OAuth2 Authentication
- **Improved User Experience**: Users can sign in with accounts they already have
- **Enhanced Security**: No need to manage passwords; authentication is handled by established providers
- **Verified Emails**: Email addresses are automatically verified through the OAuth2 provider
- **Reduced Registration Friction**: Faster onboarding with one-click sign-in

View File

@@ -1,7 +1,7 @@
--- ---
title: Sign In with WorkOS title: Sign In with WorkOS
description: Follow this guide to sign in users with WorkOS. description: Follow this guide to sign in users with WorkOS.
icon: workos icon: W
--- ---

View File

@@ -0,0 +1,137 @@
---
title: OAuth Provider Access Tokens
sidebarTitle: Tokens
description: Access and refresh OAuth provider tokens to interact with external APIs
icon: key
---
When users authenticate with OAuth providers through Nhost, you can access the provider's access and refresh tokens. This enables your application to make API calls directly to external services (like Google Drive, GitHub repositories, or Spotify playlists) on behalf of your users.
## How Provider Token Management Works
After a user completes OAuth authentication, you can retrieve and manage the provider's tokens to interact with their external accounts:
```mermaid
sequenceDiagram
autonumber
actor U as User
participant F as Frontend
participant A as Auth
participant P as OAuth Provider
Note over U,P: Initial OAuth Sign-In
U->>+F: Complete OAuth sign-in
F->>+A: GET /signin/provider/{provider}/callback/tokens
Note right of A: Bearer token required
A->>A: Retrieve stored provider session
A->>-F: Provider tokens (access, refresh, expiresIn)
F->>F: Store tokens securely
deactivate F
Note over U,P: Using Provider Access Token
U->>+F: Request action requiring provider API
F->>+P: API call with provider access token
P->>-F: API response
F->>-U: Result
Note over U,P: Refreshing Expired Token
F->>F: Detect token expiration
F->>+A: POST /token/provider/{provider}
Note right of A: Include refresh token
A->>+P: Request new access token
P->>-A: New access token (+ optional new refresh token)
A->>-F: Updated tokens
F->>F: Update stored tokens
```
### The Token Flow
1. **OAuth Completion**: After the user successfully authenticates with an OAuth provider, they are redirected back to your application
2. **Token Retrieval**: Your frontend immediately calls `/signin/provider/{provider}/callback/tokens` with the user's Nhost authentication token to retrieve the provider's tokens
3. **Secure Storage**: The provider's access token, refresh token, and expiration time are stored securely in your application
4. **Provider API Calls**: Your application uses the provider's access token to make API calls to the external service on behalf of the user
5. **Token Refresh**: When the access token expires, your application uses the refresh token to obtain a new access token without requiring the user to re-authenticate
## Retrieving Provider Tokens
After a successful OAuth sign-in, retrieve the provider's session to access their tokens.
```javascript
import { createClient } from '@nhost/nhost-js'
const nhost = createClient({
subdomain: 'myapp',
region: 'us-east-1'
})
// Immediately after OAuth callback completes
const resp = await nhost.auth.getProviderTokens('google')
// Store tokens securely
localStorage.setItem('google_access_token', resp.body.accessToken)
if (resp.body.refreshToken) {
localStorage.setItem('google_refresh_token', resp.body.refreshToken)
}
// Use provider token to call Google APIs
const userInfo = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
'Authorization': `Bearer ${resp.body.accessToken}`
}
})
```
**Important Notes:**
- This method must be called **immediately** after the OAuth callback completes
- The provider session is stored temporarily and is cleared during this call
- Subsequent calls will fail without re-authenticating with the provider
- Requires the user to be authenticated with Nhost
## Refreshing Provider Tokens
Provider access tokens typically expire after a short period (often 1 hour). Use the refresh token to obtain a new access token without user interaction.
```javascript
import { createClient } from '@nhost/nhost-js'
const nhost = createClient({
subdomain: 'myapp',
region: 'us-east-1'
})
// Retrieve stored refresh token
const storedRefreshToken = localStorage.getItem('google_refresh_token')
// Refresh the provider tokens
const resp = await nhost.auth.refreshProviderToken('google', {
refreshToken: storedRefreshToken
})
// Update stored tokens
localStorage.setItem('google_access_token', resp.body.accessToken)
if (resp.body.refreshToken) {
// Some providers rotate refresh tokens
localStorage.setItem('google_refresh_token', resp.body.refreshToken)
}
```
**Important Notes:**
- Some providers (like Google) rotate refresh tokens, returning a new one in the response
- Always update your stored refresh token with the new value if provided
- Token refresh can fail if the user revokes access or the refresh token expires
- Handle these cases by prompting the user to re-authenticate
## Best Practices
**Immediate Retrieval** - Call the tokens endpoint immediately after OAuth callback to prevent session loss
**Secure Storage** - Store provider tokens in secure storage appropriate for your platform (encrypted storage for mobile, httpOnly cookies or secure localStorage for web)
**Proactive Refresh** - Use the `expiresIn` value to refresh tokens before they expire, preventing API call failures
**Handle Rotation** - Always update your stored refresh token when a new one is returned, as some providers invalidate old refresh tokens
**Graceful Errors** - Handle token refresh failures by prompting users to re-authenticate, as they may have revoked access through the provider's settings
**Token Isolation** - Store tokens per provider and per user to support multiple OAuth connections

View File

@@ -1,43 +0,0 @@
---
title: Social Provider Connect
sidebarTitle: Social Provider Connect
description: Add social sign in mechanism to existing users
icon: link
---
With the social provider connect feature, users can link their social authentication method to their account, regardless of the initial sign-up method. It enables users to link different social authentication providers to their accounts, even if the email addresses do not match (e.g., linking a GitHub profile to an account registered with a different email). This feature offers flexibility, allowing users to streamline their login process by connecting multiple authentication methods.
To add a social authentication method to an existing user you need to call the url `https://${subdomain}.auth.${region}.nhost.run/v1/signin/provider/${provider}?connect=${jwt}`. This is very easy to achieve with our SDK:
``` js
nhost.auth.connectProvider({
provider: 'github'
})
```
In addition, hooks for react, vue and other frameworks may be provided. Check our [reference](/reference/overview#client-libraries) documentation for more details.
<Note>
Keep in mind that as we need a `JWT` the user needs to be logged in.
</Note>
## Viewing and Deleting Social Provider Authentication Mechanisms
If you want to allow your users to view and/or delete social provider authentication mechanisms, you can provide the necessary permissions to the table `auth.user_providers` (i.e. `select` and/or `delete`) and then use the appropriate GraphQL query. For example, the following permissions should allow users to list their own social providers:
![social connect permissions](/images/auth/social-connect-permissions.png)
Using the following GraphQL query:
``` js
const { error, data } = await nhost.graphql.request(
gql`
query getAuthUserProviders {
authUserProviders {
id
providerId
}
}
`,
)
```

Some files were not shown because too many files have changed in this diff Show More