Compare commits

...

20 Commits

Author SHA1 Message Date
David Barroso
e8c9e1e239 Merge branch 'main' into timing 2025-10-30 16:42:08 +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
0cc0f74404 asd 2025-10-30 09:19:40 +01:00
David Barroso
dd04d64a6b chore(cli): increase timeouts for testing purposes 2025-10-30 09:16:50 +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
118 changed files with 2133 additions and 500 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**
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.**
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 ---
> **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
The PR title must follow the following pattern:

View File

@@ -1,8 +1,7 @@
---
name: "auth: check and build"
on:
# pull_request_target:
pull_request:
pull_request_target:
paths:
- '.github/workflows/auth_checks.yaml'
- '.github/workflows/wf_check.yaml'
@@ -49,7 +48,7 @@ jobs:
with:
NAME: 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:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -64,7 +63,7 @@ jobs:
with:
NAME: 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
DOCKER: true
secrets:

View File

@@ -1,8 +1,7 @@
---
name: "cli: check and build"
on:
# pull_request_target:
pull_request:
pull_request_target:
paths:
- '.github/workflows/cli_checks.yaml'
- '.github/workflows/wf_check.yaml'
@@ -50,7 +49,7 @@ jobs:
with:
NAME: cli
PATH: cli
GIT_REF: ${{ github.sha }}
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -65,7 +64,7 @@ jobs:
with:
NAME: 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
DOCKER: true
secrets:
@@ -81,7 +80,7 @@ jobs:
with:
NAME: cli
PATH: cli
GIT_REF: ${{ github.sha }}
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}

View File

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

View File

@@ -1,8 +1,7 @@
---
name: "codegen: check and build"
on:
# pull_request_target:
pull_request:
pull_request_target:
paths:
- '.github/workflows/wf_check.yaml'
- '.github/workflows/codegen_checks.yaml'
@@ -48,7 +47,7 @@ jobs:
with:
NAME: 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:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -62,7 +61,7 @@ jobs:
with:
NAME: 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
DOCKER: false
secrets:

View File

@@ -1,7 +1,7 @@
---
name: "dashboard: check and build"
on:
pull_request:
pull_request_target:
paths:
- '.github/workflows/wf_build_artifacts.yaml'
- '.github/workflows/wf_check.yaml'
@@ -54,7 +54,7 @@ jobs:
- check-permissions
with:
NAME: dashboard
GIT_REF: ${{ github.sha }}
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
ENVIRONMENT: preview
secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -73,7 +73,7 @@ jobs:
with:
NAME: 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
DOCKER: true
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'
@@ -91,7 +91,7 @@ jobs:
with:
NAME: dashboard
PATH: dashboard
GIT_REF: ${{ github.sha }}
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
@@ -107,7 +107,7 @@ jobs:
with:
NAME: 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_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
NHOST_TEST_ORGANIZATION_NAME: ${{ vars.NHOST_TEST_ORGANIZATION_NAME }}

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
---
name: "examples/demos: check and build"
on:
# pull_request_target:
pull_request:
pull_request_target:
paths:
- '.github/workflows/wf_check.yaml'
- '.github/workflows/examples_demos_checks.yaml'
@@ -64,7 +63,7 @@ jobs:
with:
NAME: 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:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -78,7 +77,7 @@ jobs:
with:
NAME: 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
DOCKER: false
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'

View File

@@ -1,8 +1,7 @@
---
name: "examples/guides: check and build"
on:
# pull_request_target:
pull_request:
pull_request_target:
paths:
- '.github/workflows/wf_check.yaml'
- '.github/workflows/examples_guides_checks.yaml'
@@ -64,7 +63,7 @@ jobs:
with:
NAME: 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:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -78,7 +77,7 @@ jobs:
with:
NAME: 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
DOCKER: false
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'

View File

@@ -1,8 +1,7 @@
---
name: "examples/tutorials: check and build"
on:
# pull_request_target:
pull_request:
pull_request_target:
paths:
- '.github/workflows/wf_check.yaml'
- '.github/workflows/examples_tutorials_checks.yaml'
@@ -64,7 +63,7 @@ jobs:
with:
NAME: 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:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -78,7 +77,7 @@ jobs:
with:
NAME: 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
DOCKER: false
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'

View File

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

View File

@@ -1,8 +1,7 @@
---
name: "nhost-js: check and build"
on:
# pull_request_target:
pull_request:
pull_request_target:
paths:
- '.github/workflows/wf_check.yaml'
- '.github/workflows/nhost-js_checks.yaml'
@@ -65,7 +64,7 @@ jobs:
with:
NAME: 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:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -79,7 +78,7 @@ jobs:
with:
NAME: 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
DOCKER: false
secrets:

View File

@@ -1,8 +1,7 @@
---
name: "nixops: check and build"
on:
# pull_request_target:
pull_request:
pull_request_target:
paths:
- '.github/workflows/wf_check.yaml'
- '.github/workflows/nixops_checks.yaml'
@@ -40,7 +39,7 @@ jobs:
with:
NAME: nixops
PATH: nixops
GIT_REF: ${{ github.sha }}
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -54,7 +53,7 @@ jobs:
with:
NAME: 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
DOCKER: true
secrets:

View File

@@ -1,8 +1,7 @@
---
name: "storage: check and build"
on:
# pull_request_target:
pull_request:
pull_request_target:
paths:
- '.github/workflows/storage_checks.yaml'
- '.github/workflows/wf_check.yaml'
@@ -49,7 +48,7 @@ jobs:
with:
NAME: 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:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
@@ -64,7 +63,7 @@ jobs:
with:
NAME: 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
DOCKER: true
secrets:

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ jobs:
echo "VERSION=$(make get-version VER=${{ inputs.VERSION }})" >> $GITHUB_OUTPUT
- name: "Get artifacts"
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
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.
- 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 should be used to report problems with Nhost, request a new feature, or discuss potential changes before a PR is created.

View File

@@ -2,6 +2,19 @@
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

View File

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

View File

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

View File

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

View File

@@ -38,6 +38,8 @@ func graphql( //nolint:funlen
env[v.Name] = v.Value
}
env["HASURA_GRAPHQL_MIGRATIONS_SERVER_TIMEOUT"] = "600"
return &Service{
Image: "nhost/graphql-engine:" + *cfg.GetHasura().GetVersion(),
DependsOn: map[string]DependsOn{
@@ -54,7 +56,7 @@ func graphql( //nolint:funlen
"CMD-SHELL",
"curl http://localhost:8080/healthz > /dev/null 2>&1",
},
Timeout: "60s",
Timeout: "600s",
Interval: "5s",
StartPeriod: "60s",
},
@@ -135,6 +137,8 @@ func console( //nolint:funlen
env[v.Name] = v.Value
}
env["HASURA_GRAPHQL_MIGRATIONS_SERVER_TIMEOUT"] = "600"
return &Service{
Image: fmt.Sprintf(
"nhost/graphql-engine:%s.cli-migrations-v3",
@@ -165,7 +169,7 @@ func console( //nolint:funlen
"CMD-SHELL",
"timeout 1s bash -c ':> /dev/tcp/127.0.0.1/9695' || exit 1",
},
Timeout: "60s",
Timeout: "600s",
Interval: "5s",
StartPeriod: "60s",
},

View File

@@ -39,6 +39,7 @@ func expectedGraphql() *Service {
"HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_BATCH_SIZE": "100",
"HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL": "1000",
"HASURA_GRAPHQL_LOG_LEVEL": "info",
"HASURA_GRAPHQL_MIGRATIONS_SERVER_TIMEOUT": "600",
"HASURA_GRAPHQL_PG_CONNECTIONS": "50",
"HASURA_GRAPHQL_PG_TIMEOUT": "180",
"HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES": "false",
@@ -77,7 +78,7 @@ func expectedGraphql() *Service {
"CMD-SHELL",
"curl http://localhost:8080/healthz > /dev/null 2>&1",
},
Timeout: "60s",
Timeout: "600s",
Interval: "5s",
StartPeriod: "60s",
},
@@ -178,6 +179,7 @@ func expectedConsole() *Service {
"HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_BATCH_SIZE": "100",
"HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL": "1000",
"HASURA_GRAPHQL_LOG_LEVEL": "info",
"HASURA_GRAPHQL_MIGRATIONS_SERVER_TIMEOUT": "600",
"HASURA_GRAPHQL_PG_CONNECTIONS": "50",
"HASURA_GRAPHQL_PG_TIMEOUT": "180",
"HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES": "false",
@@ -216,7 +218,7 @@ func expectedConsole() *Service {
"CMD-SHELL",
"timeout 1s bash -c ':> /dev/tcp/127.0.0.1/9695' || exit 1",
},
Timeout: "60s",
Timeout: "600s",
Interval: "5s",
StartPeriod: "60s",
},

View File

@@ -1,6 +1,6 @@
// 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
import (

View File

@@ -1,6 +1,6 @@
// 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
import (

View File

@@ -2823,6 +2823,7 @@ type Deployments struct {
CommitSha string `json:"commitSHA"`
CommitUserAvatarURL *string `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *string `json:"commitUserName,omitempty"`
CreatedAt time.Time `json:"createdAt"`
DeploymentEndedAt *time.Time `json:"deploymentEndedAt,omitempty"`
// An array relationship
DeploymentLogs []*DeploymentLogs `json:"deploymentLogs"`
@@ -2865,6 +2866,7 @@ type DeploymentsBoolExp struct {
CommitSha *StringComparisonExp `json:"commitSHA,omitempty"`
CommitUserAvatarURL *StringComparisonExp `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *StringComparisonExp `json:"commitUserName,omitempty"`
CreatedAt *TimestamptzComparisonExp `json:"createdAt,omitempty"`
DeploymentEndedAt *TimestamptzComparisonExp `json:"deploymentEndedAt,omitempty"`
DeploymentLogs *DeploymentLogsBoolExp `json:"deploymentLogs,omitempty"`
DeploymentStartedAt *TimestamptzComparisonExp `json:"deploymentStartedAt,omitempty"`
@@ -2899,6 +2901,7 @@ type DeploymentsMaxOrderBy struct {
CommitSha *OrderBy `json:"commitSHA,omitempty"`
CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *OrderBy `json:"commitUserName,omitempty"`
CreatedAt *OrderBy `json:"createdAt,omitempty"`
DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"`
DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"`
DeploymentStatus *OrderBy `json:"deploymentStatus,omitempty"`
@@ -2921,6 +2924,7 @@ type DeploymentsMinOrderBy struct {
CommitSha *OrderBy `json:"commitSHA,omitempty"`
CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *OrderBy `json:"commitUserName,omitempty"`
CreatedAt *OrderBy `json:"createdAt,omitempty"`
DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"`
DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"`
DeploymentStatus *OrderBy `json:"deploymentStatus,omitempty"`
@@ -2959,6 +2963,7 @@ type DeploymentsOrderBy struct {
CommitSha *OrderBy `json:"commitSHA,omitempty"`
CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *OrderBy `json:"commitUserName,omitempty"`
CreatedAt *OrderBy `json:"createdAt,omitempty"`
DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"`
DeploymentLogsAggregate *DeploymentLogsAggregateOrderBy `json:"deploymentLogs_aggregate,omitempty"`
DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"`
@@ -2990,6 +2995,7 @@ type DeploymentsStreamCursorValueInput struct {
CommitSha *string `json:"commitSHA,omitempty"`
CommitUserAvatarURL *string `json:"commitUserAvatarUrl,omitempty"`
CommitUserName *string `json:"commitUserName,omitempty"`
CreatedAt *time.Time `json:"createdAt,omitempty"`
DeploymentEndedAt *time.Time `json:"deploymentEndedAt,omitempty"`
DeploymentStartedAt *time.Time `json:"deploymentStartedAt,omitempty"`
DeploymentStatus *string `json:"deploymentStatus,omitempty"`
@@ -6983,6 +6989,8 @@ const (
// column name
DeploymentsSelectColumnCommitUserName DeploymentsSelectColumn = "commitUserName"
// column name
DeploymentsSelectColumnCreatedAt DeploymentsSelectColumn = "createdAt"
// column name
DeploymentsSelectColumnDeploymentEndedAt DeploymentsSelectColumn = "deploymentEndedAt"
// column name
DeploymentsSelectColumnDeploymentStartedAt DeploymentsSelectColumn = "deploymentStartedAt"
@@ -7016,6 +7024,7 @@ var AllDeploymentsSelectColumn = []DeploymentsSelectColumn{
DeploymentsSelectColumnCommitSha,
DeploymentsSelectColumnCommitUserAvatarURL,
DeploymentsSelectColumnCommitUserName,
DeploymentsSelectColumnCreatedAt,
DeploymentsSelectColumnDeploymentEndedAt,
DeploymentsSelectColumnDeploymentStartedAt,
DeploymentsSelectColumnDeploymentStatus,
@@ -7033,7 +7042,7 @@ var AllDeploymentsSelectColumn = []DeploymentsSelectColumn{
func (e DeploymentsSelectColumn) IsValid() bool {
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 false

View File

@@ -3,12 +3,13 @@ NEXT_PUBLIC_ENV=dev
NEXT_PUBLIC_NHOST_PLATFORM=false
# 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_GRAPHQL_URL=https://local.graphql.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_MIGRATIONS_API_URL=https://local.hasura.local.nhost.run/v1/migrations
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/apis/migrate
NEXT_PUBLIC_NHOST_HASURA_API_URL=https://local.hasura.local.nhost.run
# Environment Variables when running the Nhost Dashboard against the Nhost Backend

View File

@@ -2,6 +2,17 @@
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

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_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
| Name | Description |

View File

@@ -4,21 +4,31 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
});
const { version } = require('./package.json');
const cspHeader = `
default-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run;
script-src 'self' 'unsafe-eval' cdn.segment.com js.stripe.com challenges.cloudflare.com googletagmanager.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;
style-src 'self' 'unsafe-inline';
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;
`;
function getCspHeader() {
switch (process.env.CSP_MODE) {
case 'disabled':
return null;
case 'custom':
return process.env.CSP_HEADER || null;
case 'nhost':
default:
return [
"default-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run",
"script-src 'self' 'unsafe-eval' cdn.segment.com js.stripe.com challenges.cloudflare.com googletagmanager.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",
"style-src 'self' 'unsafe-inline'",
"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({
reactStrictMode: false,
@@ -34,13 +44,19 @@ module.exports = withBundleAnalyzer({
dirs: ['src'],
},
async headers() {
const cspHeader = getCspHeader();
if (!cspHeader) {
return []; // No CSP headers
}
return [
{
source: '/(.*)',
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader.replace(/\s+/g, ' ').trim(),
value: cspHeader,
},
{
key: 'X-Frame-Options',

View File

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

View File

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

View File

@@ -176,7 +176,7 @@ export default function AppleProviderSettings() {
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"
icon={
theme.palette.mode === 'dark'

View File

@@ -141,7 +141,7 @@ export default function DiscordProviderSettings() {
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"
icon="/assets/brands/discord.svg"
switchId="enabled"

View File

@@ -142,7 +142,7 @@ export default function FacebookProviderSettings() {
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"
icon="/assets/brands/facebook.svg"
switchId="enabled"

View File

@@ -144,7 +144,7 @@ export default function GitHubProviderSettings() {
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"
icon={
theme.palette.mode === 'dark'

View File

@@ -162,7 +162,7 @@ export default function GoogleProviderSettings() {
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"
icon="/assets/brands/google.svg"
switchId="enabled"

View File

@@ -142,7 +142,7 @@ export default function LinkedInProviderSettings() {
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"
icon="/assets/brands/linkedin.svg"
switchId="enabled"

View File

@@ -142,7 +142,7 @@ export default function SpotifyProviderSettings() {
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"
icon="/assets/brands/spotify.svg"
switchId="enabled"

View File

@@ -144,7 +144,7 @@ export default function TwitchProviderSettings() {
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"
icon={
theme.palette.mode === 'dark'

View File

@@ -177,7 +177,7 @@ export default function WorkOsProviderSettings() {
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"
icon="/assets/brands/workos.svg"
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 { useProject } from '@/features/orgs/projects/hooks/useProject';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getHasuraAdminSecret } from '@/utils/env';
import { getHasuraAdminSecret, getHasuraMigrationsApiUrl } from '@/utils/env';
import { parseIdentifiersFromSQL } from '@/utils/sql';
import { useRouter } from 'next/router';
import { useState } from 'react';
@@ -53,7 +53,10 @@ export default function useRunSQL(
isCascade: boolean,
) => {
try {
const migrationApiResponse = await fetch(`${appUrl}/apis/migrate`, {
const url = isPlatform
? `${appUrl}/apis/migrate`
: getHasuraMigrationsApiUrl();
const migrationApiResponse = await fetch(url, {
method: 'POST',
headers: { 'x-hasura-admin-secret': adminSecret },
body: JSON.stringify({

View File

@@ -12,7 +12,9 @@ function SupportPage() {
return (
<Box className="h-full overflow-auto pb-4">
<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>
<div className="flex flex-col items-center justify-center">

View File

@@ -5,7 +5,6 @@ import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Divider } from '@/components/ui/v2/Divider';
import { EnvelopeIcon } from '@/components/ui/v2/icons/EnvelopeIcon';
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Option } from '@/components/ui/v2/Option';
import { Text } from '@/components/ui/v2/Text';
@@ -18,6 +17,7 @@ import {
} from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { styled } from '@mui/material';
import { Mail } from 'lucide-react';
import { type ReactElement } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -28,7 +28,7 @@ type Organization = Omit<
>;
const validationSchema = Yup.object({
organization: Yup.string().label('Organization'),
organization: Yup.string().label('Organization').required(),
project: Yup.string().label('Project').required(),
services: Yup.array()
.of(Yup.object({ label: Yup.string(), value: Yup.string() }))
@@ -167,7 +167,7 @@ function TicketPage() {
>
{organizations.map((organization) => (
<Option
key={organization.name}
key={organization.id}
value={organization.id}
label={organization.name}
>
@@ -237,6 +237,8 @@ function TicketPage() {
slotProps={{
root: { className: 'grid grid-flow-col gap-1 mb-4' },
}}
error={!!errors.priority}
helperText={errors.priority?.message}
renderValue={(option) => (
<span className="inline-grid grid-flow-col items-center gap-2">
{option?.label}
@@ -286,7 +288,6 @@ function TicketPage() {
label="Subject"
placeholder="Summary of the problem you are experiencing"
fullWidth
autoFocus
inputProps={{ min: 2, max: 128 }}
error={!!errors.subject}
helperText={errors.subject?.message}
@@ -306,16 +307,16 @@ function TicketPage() {
helperText={errors.description?.message}
/>
<Box className="ml-auto flex w-80 flex-col gap-4">
<Box className="ml-auto flex flex-col gap-4 lg:w-80">
<Text color="secondary" className="text-right text-sm">
We will contact you at <strong>{user?.email}</strong>
</Text>
<Button
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"
type="submit"
startIcon={<EnvelopeIcon />}
startIcon={<Mail className="size-4" />}
disabled={isSubmitting}
loading={isSubmitting}
>
@@ -334,7 +335,7 @@ function TicketPage() {
TicketPage.getLayout = function getLayout(page: ReactElement) {
return (
<AuthenticatedLayout title="Help & Support | Nhost">
<AuthenticatedLayout title="Help & Support | Nhost" withMainNav={false}>
{page}
</AuthenticatedLayout>
);

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 245 KiB

View File

@@ -19,7 +19,7 @@ Nhost Auth is a ready-to-use authentication service seamlessly integrated with t
</Card>
<Card title="Security Keys (WebAuthn)" icon="square-5" href="/products/auth/webauthn">
</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 title="Elevated Permissions" icon="square-7" href="/products/auth/elevated-permissions">
</Card>
@@ -28,24 +28,24 @@ Nhost Auth is a ready-to-use authentication service seamlessly integrated with t
### OAuth Providers
<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 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 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 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 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 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 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 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 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 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>
</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
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
@@ -41,7 +43,7 @@ Once everything is configured you can use an ID token to authenticate users with
### 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>
<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
#### Apple
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).
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.

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
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
}
}
`,
)
```

View File

@@ -1,25 +0,0 @@
```mermaid
sequenceDiagram
autonumber
actor U as User
participant A as Hasura 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
```

View File

@@ -592,6 +592,12 @@ paths:
If set, this means that the user is already authenticated and wants to link their account. This needs to be a valid JWT access token.
schema:
type: string
- name: state
in: query
required: false
description: Opaque state value to be returned by the provider
schema:
type: string
responses:
"302":
@@ -738,6 +744,31 @@ paths:
$ref: "#/components/schemas/ErrorResponse"
description: "An error occurred while processing the request"
/signin/provider/{provider}/callback/tokens:
get:
summary: Retrieve OAuth2 provider tokens from callback
description: After successful OAuth2 authentication, retrieve the provider session containing access token, refresh token, and expiration information for the specified provider. To ensure the data isn't stale this endpoint must be called immediately after the OAuth callback to obtain the tokens. The session is cleared from the database during this call, so subsequent calls will fail without going through the sign-in flow again. It is the user's responsibility to store the session safely (e.g., in browser local storage).
operationId: getProviderTokens
tags:
- authentication
security:
- BearerAuth: []
parameters:
- $ref: "#/components/parameters/SignInProvider"
responses:
"200":
description: Successfully retrieved provider session
content:
application/json:
schema:
$ref: "#/components/schemas/ProviderSession"
default:
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: "An error occurred while processing the request"
/signin/webauthn:
post:
summary: Sign in with Webauthn
@@ -954,6 +985,38 @@ paths:
$ref: "#/components/schemas/ErrorResponse"
description: "An error occurred while processing the request"
/token/provider/{provider}:
post:
summary: Refresh OAuth2 provider tokens
description: Refresh the OAuth2 provider access token using a valid refresh token. Returns a new provider session with updated access token, refresh token (if rotated by provider), and expiration information. This endpoint allows maintaining long-lived access to provider APIs without requiring the user to re-authenticate.
operationId: refreshProviderToken
tags:
- authentication
security:
- BearerAuth: []
parameters:
- $ref: "#/components/parameters/SignInProvider"
requestBody:
description: Provider refresh token to exchange for a new access token
content:
application/json:
schema:
$ref: "#/components/schemas/RefreshProviderTokenRequest"
required: true
responses:
"200":
description: Successfully refreshed provider tokens
content:
application/json:
schema:
$ref: "#/components/schemas/ProviderSession"
default:
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: "An error occurred while processing the request"
/token/verify:
post:
summary: Verify JWT token
@@ -1814,6 +1877,46 @@ components:
required:
- challenge
ProviderSession:
type: object
description: "OAuth2 provider session containing access and refresh tokens"
additionalProperties: false
properties:
accessToken:
type: string
description: "OAuth2 provider access token for API calls"
example: "ya29.a0AfH6SMBx..."
expiresIn:
type: integer
description: "Number of seconds until the access token expires"
example: 3599
expiresAt:
type: string
format: date-time
description: "Timestamp when the access token expires"
example: "2024-12-31T23:59:59Z"
refreshToken:
type: string
nullable: true
description: "OAuth2 provider refresh token for obtaining new access tokens (if provided by the provider)"
example: "1//0gK8..."
required:
- accessToken
- expiresIn
- expiresAt
RefreshProviderTokenRequest:
type: object
description: "Request to refresh OAuth2 provider tokens"
additionalProperties: false
properties:
refreshToken:
type: string
description: "OAuth2 provider refresh token obtained from previous authentication"
example: "1//0gK8..."
required:
- refreshToken
RefreshTokenRequest:
type: object
description: "Request to refresh an access token"
@@ -2512,7 +2615,6 @@ components:
- facebook
- windowslive
- twitter
deprecated: true
TicketQuery:
in: query

View File

@@ -0,0 +1,5 @@
---
title: "getProviderTokens"
openapi: get /signin/provider/{provider}/callback/tokens
sidebarTitle: /signin/provider/{provider}/callback/tokens
---

View File

@@ -0,0 +1,5 @@
---
title: "refreshProviderToken"
openapi: post /token/provider/{provider}
sidebarTitle: /token/provider/{provider}
---

View File

@@ -254,7 +254,7 @@ Start local development environment
**--ca-certificates**="": Mounts and everrides path to CA certificates in the containers
**--dashboard-version**="": Dashboard version to use (default: nhost/dashboard:2.38.4)
**--dashboard-version**="": Dashboard version to use (default: nhost/dashboard:2.40.0)
**--disable-tls**: Disable TLS
@@ -284,7 +284,7 @@ Start local development environment connected to an Nhost Cloud project (BETA)
**--ca-certificates**="": Mounts and everrides path to CA certificates in the containers
**--dashboard-version**="": Dashboard version to use (default: nhost/dashboard:2.38.4)
**--dashboard-version**="": Dashboard version to use (default: nhost/dashboard:2.40.0)
**--disable-tls**: Disable TLS

View File

@@ -468,6 +468,30 @@ This method may return different T based on the response code:
`Promise`&lt;[`FetchResponse`](./fetch#fetchresponse)&lt;[`JWKSet`](#jwkset)&gt;&gt;
#### getProviderTokens()
```ts
getProviderTokens(provider: SignInProvider, options?: RequestInit): Promise<FetchResponse<ProviderSession>>;
```
Summary: Retrieve OAuth2 provider tokens from callback
After successful OAuth2 authentication, retrieve the provider session containing access token, refresh token, and expiration information for the specified provider. To ensure the data isn't stale this endpoint must be called immediately after the OAuth callback to obtain the tokens. The session is cleared from the database during this call, so subsequent calls will fail without going through the sign-in flow again. It is the user's responsibility to store the session safely (e.g., in browser local storage).
This method may return different T based on the response code:
- 200: ProviderSession
##### Parameters
| Parameter | Type |
| ---------- | ----------------------------------- |
| `provider` | [`SignInProvider`](#signinprovider) |
| `options?` | `RequestInit` |
##### Returns
`Promise`&lt;[`FetchResponse`](./fetch#fetchresponse)&lt;[`ProviderSession`](#providersession)&gt;&gt;
#### getUser()
```ts
@@ -602,6 +626,34 @@ Add a middleware function to the fetch chain
`void`
#### refreshProviderToken()
```ts
refreshProviderToken(
provider: SignInProvider,
body: RefreshProviderTokenRequest,
options?: RequestInit): Promise<FetchResponse<ProviderSession>>;
```
Summary: Refresh OAuth2 provider tokens
Refresh the OAuth2 provider access token using a valid refresh token. Returns a new provider session with updated access token, refresh token (if rotated by provider), and expiration information. This endpoint allows maintaining long-lived access to provider APIs without requiring the user to re-authenticate.
This method may return different T based on the response code:
- 200: ProviderSession
##### Parameters
| Parameter | Type |
| ---------- | ------------------------------------------------------------- |
| `provider` | [`SignInProvider`](#signinprovider) |
| `body` | [`RefreshProviderTokenRequest`](#refreshprovidertokenrequest) |
| `options?` | `RequestInit` |
##### Returns
`Promise`&lt;[`FetchResponse`](./fetch#fetchresponse)&lt;[`ProviderSession`](#providersession)&gt;&gt;
#### refreshToken()
```ts
@@ -1605,6 +1657,54 @@ Format - uri
---
## ProviderSession
OAuth2 provider session containing access and refresh tokens
### Properties
#### accessToken
```ts
accessToken: string
```
(`string`) - OAuth2 provider access token for API calls
- Example - `"ya29.a0AfH6SMBx..."`
#### expiresAt
```ts
expiresAt: string
```
(`string`) - Timestamp when the access token expires
- Example - `"2024-12-31T23:59:59Z"`
- Format - date-time
#### expiresIn
```ts
expiresIn: number
```
(`number`) - Number of seconds until the access token expires
- Example - `3599`
#### refreshToken?
```ts
optional refreshToken: string;
```
OAuth2 provider refresh token for obtaining new access tokens (if provided by the provider)
Example - `"1//0gK8..."`
---
## PublicKeyCredentialCreationOptions
### Properties
@@ -1795,6 +1895,24 @@ A requirement for user verification for the operation
---
## RefreshProviderTokenRequest
Request to refresh OAuth2 provider tokens
### Properties
#### refreshToken
```ts
refreshToken: string
```
(`string`) - OAuth2 provider refresh token obtained from previous authentication
- Example - `"1//0gK8..."`
---
## RefreshTokenRequest
Request to refresh an access token
@@ -2295,6 +2413,14 @@ optional redirectTo: string;
URI to redirect to
#### state?
```ts
optional state: string;
```
Opaque state value to be returned by the provider
---
## SignInWebauthnRequest

View File

@@ -454,7 +454,7 @@ Example - `"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
##### Inherited from
[`Session`](./auth#session).[`accessToken`](./auth#accesstoken)
[`Session`](./auth#session).[`accessToken`](./auth#accesstoken-1)
#### accessTokenExpiresIn
@@ -490,7 +490,7 @@ Pattern - \b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b
##### Inherited from
[`Session`](./auth#session).[`refreshToken`](./auth#refreshtoken-3)
[`Session`](./auth#session).[`refreshToken`](./auth#refreshtoken-5)
#### refreshTokenId

18
flake.lock generated
View File

@@ -20,11 +20,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1731533336,
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
"lastModified": 1757882181,
"narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
"rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
"type": "github"
},
"original": {
@@ -40,11 +40,11 @@
]
},
"locked": {
"lastModified": 1752002763,
"narHash": "sha256-JYAkdZvpdSx9GUoHPArctYMypSONob4DYKRkOubUWtY=",
"lastModified": 1761716996,
"narHash": "sha256-vdOuy2pid2/DasUgb08lDOswdPJkN5qjXfBYItVy/R4=",
"owner": "nlewo",
"repo": "nix2container",
"rev": "4f2437f6a1844b843b380d483087ae6d461240ee",
"rev": "e5496ab66e9de9e3f67dc06f692dfbc471b6316e",
"type": "github"
},
"original": {
@@ -55,11 +55,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1753399495,
"narHash": "sha256-7XG/QBqhrYOyA2houjRTL2NMa7IKZZ/somBqr+Q/6Wo=",
"lastModified": 1761656231,
"narHash": "sha256-EiED5k6gXTWoAIS8yQqi5mAX6ojnzpHwAQTS3ykeYMg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0d00f23f023b7215b3f1035adb5247c8ec180dbc",
"rev": "e99366c665bdd53b7b500ccdc5226675cfc51f45",
"type": "github"
},
"original": {

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/nhost/nhost
go 1.24.2
go 1.25.3
require (
github.com/99designs/gqlgen v0.17.80

8
go.sum
View File

@@ -338,14 +338,6 @@ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nhost/be v0.0.0-20250929153845-6db3e5249d33 h1:BNFN3Mw4zY6LEmVc7RXkHSVvHtovDSm7Oesb7IUt27o=
github.com/nhost/be v0.0.0-20250929153845-6db3e5249d33/go.mod h1:feVvqP3dft8hWbp9zNZExdGKbFEYv8aLYohfyAeINNQ=
github.com/nhost/be v0.0.0-20251015125528-facecfb60cea h1:QQMCKMdkOkVN5PlZB/iiBSdQL6u84JIVppWf7hC5OoU=
github.com/nhost/be v0.0.0-20251015125528-facecfb60cea/go.mod h1:feVvqP3dft8hWbp9zNZExdGKbFEYv8aLYohfyAeINNQ=
github.com/nhost/be v0.0.0-20251020104454-acc8934d2c11 h1:5uPnBt2gjVkRpjUEJ8uS/q+FpvpTQJ+CN6zLCBYcfPA=
github.com/nhost/be v0.0.0-20251020104454-acc8934d2c11/go.mod h1:feVvqP3dft8hWbp9zNZExdGKbFEYv8aLYohfyAeINNQ=
github.com/nhost/be v0.0.0-20251020115144-85e0542ecec0 h1:MRWnaC1Aoir6JPr4v4C2TAVG5KBIgsOdWiDeII5HQYg=
github.com/nhost/be v0.0.0-20251020115144-85e0542ecec0/go.mod h1:feVvqP3dft8hWbp9zNZExdGKbFEYv8aLYohfyAeINNQ=
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48 h1:+Oh4Rbr1psWlBaQTakoBYFNB8jBioiXuimNMaNPLTHk=
github.com/nhost/be v0.0.0-20251021065906-8abc7d8dfa48/go.mod h1:feVvqP3dft8hWbp9zNZExdGKbFEYv8aLYohfyAeINNQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=

View File

@@ -4,8 +4,13 @@ final: prev:
doCheck = false;
});
linux-pam = prev.linux-pam.overrideAttrs (oldAttrs: {
outputs = [ "out" "scripts" ];
});
nhost-cli = final.callPackage ./nhost-cli.nix { inherit final; };
}
// import ./go.nix final prev
// import ./js.nix final prev
// import ./postgres.nix final prev

View File

@@ -1,11 +1,11 @@
final: prev: rec {
go = prev.go_1_24.overrideAttrs
go = prev.go_1_25.overrideAttrs
(finalAttrs: previousAttrs: rec {
version = "1.24.6";
version = "1.25.3";
src = final.fetchurl {
url = "https://go.dev/dl/go${version}.src.tar.gz";
sha256 = "sha256-4ctVgqq1iGaLwEwH3hhogHD2uMmyqvNh+CHhm9R8/b0=";
sha256 = "sha256-qBpLpZPQAV4QxR4mfeP/B8eskU38oDfZUX0ClRcJd5U=";
};
});
@@ -30,16 +30,16 @@ final: prev: rec {
];
});
golines = final.buildGoModule rec {
golines = final.buildGoModule {
pname = "golines";
version = "0.13.0-beta";
version = "0.14.0-beta";
src = final.fetchFromGitHub {
owner = "segmentio";
repo = "golines";
rev = "fc305205784a70b4cfc17397654f4c94e3153ce4";
sha256 = "sha256-ZdCR4ZC1+Llyt/rcX0RGisM98u6rq9/ECUuHEMV+Kkc=";
rev = "8f32f0f7e89c30f572c7f2cd3b2a48016b9d8bbf";
sha256 = "sha256-Y4q3xpGw8bAi87zJ48+LVbdgOc7HB1lRdYhlsF1YcVA=";
};
vendorHash = "sha256-mmdaHm3YL/2eB/r3Sskd9liljKAe3/c8T0z5KIUHeK0=";
vendorHash = "sha256-94IXh9iBAE0jJXovaElY8oFdXE6hxYg0Ww0ZEHLnEwc=";
meta = with final.lib; {
description = "A golang formatter that fixes long lines";
homepage = "https://github.com/segmentio/golines";
@@ -92,7 +92,7 @@ final: prev: rec {
};
};
oapi-codegen = prev.oapi-codegen.overrideAttrs (oldAttrs: rec {
oapi-codegen = prev.oapi-codegen.overrideAttrs (oldAttrs: {
version = "2.6.0-beta0";
src = final.fetchFromGitHub {
owner = "dbarrosop";

View File

@@ -1,22 +1,22 @@
{ final }:
let
version = "1.33.0";
version = "1.34.4";
dist = {
aarch64-darwin = {
url = "https://github.com/nhost/nhost/releases/download/cli%40${version}/cli-${version}-darwin-arm64.tar.gz";
sha256 = "0d4l4pmcz79147xyc1ag6zahl5jbmwl6a86cccnx13axbf0gxh2b";
sha256 = "1pqyk9ny9gznh0q3v9lk4pd9aai0ysnzwx4cr1dh98rkf0504gic";
};
x86_64-darwin = {
url = "https://github.com/nhost/nhost/releases/download/cli%40${version}/cli-${version}-darwin-amd64.tar.gz";
sha256 = "16n1j1ml7p9m00mhs0wzxfj27x951xx70q6hp6j6m9s3m0y7wbgz";
sha256 = "19l61bli8nxljnfmlizdbs95ddmzyl0f0h8m35lw708cfyvj83n9";
};
aarch64-linux = {
url = "https://github.com/nhost/nhost/releases/download/cli%40${version}/cli-${version}-linux-arm64.tar.gz";
sha256 = "1z0vi2yb932yk4y7v1xwwbxx4h582mk5pd0j2fv7nvw23rgxmcd7";
sha256 = "0gicakx28aj3bpcgyqaq2y22kqkr90vvk25lnpinb0y04kggnla6";
};
x86_64-linux = {
url = "https://github.com/nhost/nhost/releases/download/cli%40${version}/cli-${version}-linux-amd64.tar.gz";
sha256 = "1q3pg5kdwdphdfpwzpnn41hdzdxy2l5l0vw23xwjqjand68cpyip";
sha256 = "1jlcqizlwa3z2lld1c1wdcm0vxc2vij0l6zdddrh2s0adbprsarr";
};
};

View File

@@ -133,7 +133,7 @@ in
name = "root";
paths = with pkgs; [
coreutils
nix
nixVersions.nix_2_28
bash
gnugrep
gnumake

View File

@@ -610,6 +610,53 @@ export interface PublicKeyCredentialRequestOptions {
extensions?: Record<string, unknown>;
}
/**
* OAuth2 provider session containing access and refresh tokens
@property accessToken (`string`) - OAuth2 provider access token for API calls
* Example - `"ya29.a0AfH6SMBx..."`
@property expiresIn (`number`) - Number of seconds until the access token expires
* Example - `3599`
@property expiresAt (`string`) - Timestamp when the access token expires
* Example - `"2024-12-31T23:59:59Z"`
* Format - date-time
@property refreshToken? (`string`) - OAuth2 provider refresh token for obtaining new access tokens (if provided by the provider)
* Example - `"1//0gK8..."`*/
export interface ProviderSession {
/**
* OAuth2 provider access token for API calls
* Example - `"ya29.a0AfH6SMBx..."`
*/
accessToken: string;
/**
* Number of seconds until the access token expires
* Example - `3599`
*/
expiresIn: number;
/**
* Timestamp when the access token expires
* Example - `"2024-12-31T23:59:59Z"`
* Format - date-time
*/
expiresAt: string;
/**
* OAuth2 provider refresh token for obtaining new access tokens (if provided by the provider)
* Example - `"1//0gK8..."`
*/
refreshToken?: string;
}
/**
* Request to refresh OAuth2 provider tokens
@property refreshToken (`string`) - OAuth2 provider refresh token obtained from previous authentication
* Example - `"1//0gK8..."`*/
export interface RefreshProviderTokenRequest {
/**
* OAuth2 provider refresh token obtained from previous authentication
* Example - `"1//0gK8..."`
*/
refreshToken: string;
}
/**
* Request to refresh an access token
@property refreshToken (`string`) - Refresh token used to generate a new access token
@@ -1537,6 +1584,8 @@ export interface GetVersionResponse200 {
@property redirectTo? (string) - URI to redirect to
@property connect? (string) - If set, this means that the user is already authenticated and wants to link their account. This needs to be a valid JWT access token.
@property state? (string) - Opaque state value to be returned by the provider
*/
export interface SignInProviderParams {
/**
@@ -1574,6 +1623,11 @@ export interface SignInProviderParams {
*/
connect?: string;
/**
* Opaque state value to be returned by the provider
*/
state?: string;
}
/**
* Parameters for the verifyTicket method.
@@ -1831,6 +1885,18 @@ export interface Client {
options?: RequestInit,
): string;
/**
Summary: Retrieve OAuth2 provider tokens from callback
After successful OAuth2 authentication, retrieve the provider session containing access token, refresh token, and expiration information for the specified provider. To ensure the data isn't stale this endpoint must be called immediately after the OAuth callback to obtain the tokens. The session is cleared from the database during this call, so subsequent calls will fail without going through the sign-in flow again. It is the user's responsibility to store the session safely (e.g., in browser local storage).
This method may return different T based on the response code:
- 200: ProviderSession
*/
getProviderTokens(
provider: SignInProvider,
options?: RequestInit,
): Promise<FetchResponse<ProviderSession>>;
/**
Summary: Sign in with Webauthn
Initiate a Webauthn sign-in process by sending a challenge to the user's device. The user must have previously registered a Webauthn credential.
@@ -1915,6 +1981,19 @@ export interface Client {
options?: RequestInit,
): Promise<FetchResponse<Session>>;
/**
Summary: Refresh OAuth2 provider tokens
Refresh the OAuth2 provider access token using a valid refresh token. Returns a new provider session with updated access token, refresh token (if rotated by provider), and expiration information. This endpoint allows maintaining long-lived access to provider APIs without requiring the user to re-authenticate.
This method may return different T based on the response code:
- 200: ProviderSession
*/
refreshProviderToken(
provider: SignInProvider,
body: RefreshProviderTokenRequest,
options?: RequestInit,
): Promise<FetchResponse<ProviderSession>>;
/**
Summary: Verify JWT token
Verify the validity of a JWT access token. If no request body is provided, the Authorization header will be used for verification.
@@ -2682,6 +2761,39 @@ export const createAPIClient = (
return url;
};
const getProviderTokens = async (
provider: SignInProvider,
options?: RequestInit,
): Promise<FetchResponse<ProviderSession>> => {
const url = `${baseURL}/signin/provider/${provider}/callback/tokens`;
const res = await fetch(url, {
...options,
method: "GET",
headers: {
...options?.headers,
},
});
if (res.status >= 300) {
const responseBody = [412].includes(res.status) ? null : await res.text();
const payload: unknown = responseBody ? JSON.parse(responseBody) : {};
throw new FetchError(payload, res.status, res.headers);
}
const responseBody = [204, 205, 304].includes(res.status)
? null
: await res.text();
const payload: ProviderSession = responseBody
? JSON.parse(responseBody)
: {};
return {
body: payload,
status: res.status,
headers: res.headers,
} as FetchResponse<ProviderSession>;
};
const signInWebauthn = async (
body?: SignInWebauthnRequest,
options?: RequestInit,
@@ -2923,6 +3035,42 @@ export const createAPIClient = (
} as FetchResponse<Session>;
};
const refreshProviderToken = async (
provider: SignInProvider,
body: RefreshProviderTokenRequest,
options?: RequestInit,
): Promise<FetchResponse<ProviderSession>> => {
const url = `${baseURL}/token/provider/${provider}`;
const res = await fetch(url, {
...options,
method: "POST",
headers: {
"Content-Type": "application/json",
...options?.headers,
},
body: JSON.stringify(body),
});
if (res.status >= 300) {
const responseBody = [412].includes(res.status) ? null : await res.text();
const payload: unknown = responseBody ? JSON.parse(responseBody) : {};
throw new FetchError(payload, res.status, res.headers);
}
const responseBody = [204, 205, 304].includes(res.status)
? null
: await res.text();
const payload: ProviderSession = responseBody
? JSON.parse(responseBody)
: {};
return {
body: payload,
status: res.status,
headers: res.headers,
} as FetchResponse<ProviderSession>;
};
const verifyToken = async (
body?: VerifyTokenRequest,
options?: RequestInit,
@@ -3325,6 +3473,7 @@ export const createAPIClient = (
verifySignInPasswordlessSms,
signInPAT,
signInProviderURL,
getProviderTokens,
signInWebauthn,
verifySignInWebauthn,
signOut,
@@ -3332,6 +3481,7 @@ export const createAPIClient = (
signUpWebauthn,
verifySignUpWebauthn,
refreshToken,
refreshProviderToken,
verifyToken,
getUser,
deanonymizeUser,

View File

@@ -592,6 +592,12 @@ paths:
If set, this means that the user is already authenticated and wants to link their account. This needs to be a valid JWT access token.
schema:
type: string
- name: state
in: query
required: false
description: Opaque state value to be returned by the provider
schema:
type: string
responses:
"302":
@@ -738,6 +744,31 @@ paths:
$ref: "#/components/schemas/ErrorResponse"
description: "An error occurred while processing the request"
/signin/provider/{provider}/callback/tokens:
get:
summary: Retrieve OAuth2 provider tokens from callback
description: After successful OAuth2 authentication, retrieve the provider session containing access token, refresh token, and expiration information for the specified provider. To ensure the data isn't stale this endpoint must be called immediately after the OAuth callback to obtain the tokens. The session is cleared from the database during this call, so subsequent calls will fail without going through the sign-in flow again. It is the user's responsibility to store the session safely (e.g., in browser local storage).
operationId: getProviderTokens
tags:
- authentication
security:
- BearerAuth: []
parameters:
- $ref: "#/components/parameters/SignInProvider"
responses:
"200":
description: Successfully retrieved provider session
content:
application/json:
schema:
$ref: "#/components/schemas/ProviderSession"
default:
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: "An error occurred while processing the request"
/signin/webauthn:
post:
summary: Sign in with Webauthn
@@ -954,6 +985,38 @@ paths:
$ref: "#/components/schemas/ErrorResponse"
description: "An error occurred while processing the request"
/token/provider/{provider}:
post:
summary: Refresh OAuth2 provider tokens
description: Refresh the OAuth2 provider access token using a valid refresh token. Returns a new provider session with updated access token, refresh token (if rotated by provider), and expiration information. This endpoint allows maintaining long-lived access to provider APIs without requiring the user to re-authenticate.
operationId: refreshProviderToken
tags:
- authentication
security:
- BearerAuth: []
parameters:
- $ref: "#/components/parameters/SignInProvider"
requestBody:
description: Provider refresh token to exchange for a new access token
content:
application/json:
schema:
$ref: "#/components/schemas/RefreshProviderTokenRequest"
required: true
responses:
"200":
description: Successfully refreshed provider tokens
content:
application/json:
schema:
$ref: "#/components/schemas/ProviderSession"
default:
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: "An error occurred while processing the request"
/token/verify:
post:
summary: Verify JWT token
@@ -1814,6 +1877,46 @@ components:
required:
- challenge
ProviderSession:
type: object
description: "OAuth2 provider session containing access and refresh tokens"
additionalProperties: false
properties:
accessToken:
type: string
description: "OAuth2 provider access token for API calls"
example: "ya29.a0AfH6SMBx..."
expiresIn:
type: integer
description: "Number of seconds until the access token expires"
example: 3599
expiresAt:
type: string
format: date-time
description: "Timestamp when the access token expires"
example: "2024-12-31T23:59:59Z"
refreshToken:
type: string
nullable: true
description: "OAuth2 provider refresh token for obtaining new access tokens (if provided by the provider)"
example: "1//0gK8..."
required:
- accessToken
- expiresIn
- expiresAt
RefreshProviderTokenRequest:
type: object
description: "Request to refresh OAuth2 provider tokens"
additionalProperties: false
properties:
refreshToken:
type: string
description: "OAuth2 provider refresh token obtained from previous authentication"
example: "1//0gK8..."
required:
- refreshToken
RefreshTokenRequest:
type: object
description: "Request to refresh an access token"
@@ -2512,7 +2615,6 @@ components:
- facebook
- windowslive
- twitter
deprecated: true
TicketQuery:
in: query

View File

@@ -1,6 +1,6 @@
// Package api 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 api
import (
@@ -88,6 +88,9 @@ type ServerInterface interface {
// OAuth2 provider callback endpoint (form_post)
// (POST /signin/provider/{provider}/callback)
SignInProviderCallbackPost(c *gin.Context, provider SignInProviderCallbackPostParamsProvider)
// Retrieve OAuth2 provider tokens from callback
// (GET /signin/provider/{provider}/callback/tokens)
GetProviderTokens(c *gin.Context, provider GetProviderTokensParamsProvider)
// Sign in with Webauthn
// (POST /signin/webauthn)
SignInWebauthn(c *gin.Context)
@@ -109,6 +112,9 @@ type ServerInterface interface {
// Refresh access token
// (POST /token)
RefreshToken(c *gin.Context)
// Refresh OAuth2 provider tokens
// (POST /token/provider/{provider})
RefreshProviderToken(c *gin.Context, provider RefreshProviderTokenParamsProvider)
// Verify JWT token
// (POST /token/verify)
VerifyToken(c *gin.Context)
@@ -480,6 +486,14 @@ func (siw *ServerInterfaceWrapper) SignInProvider(c *gin.Context) {
return
}
// ------------- Optional query parameter "state" -------------
err = runtime.BindQueryParameter("form", true, false, "state", c.Request.URL.Query(), &params.State)
if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter state: %w", err), http.StatusBadRequest)
return
}
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
@@ -612,6 +626,32 @@ func (siw *ServerInterfaceWrapper) SignInProviderCallbackPost(c *gin.Context) {
siw.Handler.SignInProviderCallbackPost(c, provider)
}
// GetProviderTokens operation middleware
func (siw *ServerInterfaceWrapper) GetProviderTokens(c *gin.Context) {
var err error
// ------------- Path parameter "provider" -------------
var provider GetProviderTokensParamsProvider
err = runtime.BindStyledParameterWithOptions("simple", "provider", c.Param("provider"), &provider, runtime.BindStyledParameterOptions{Explode: false, Required: true})
if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter provider: %w", err), http.StatusBadRequest)
return
}
c.Set(BearerAuthScopes, []string{})
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
return
}
}
siw.Handler.GetProviderTokens(c, provider)
}
// SignInWebauthn operation middleware
func (siw *ServerInterfaceWrapper) SignInWebauthn(c *gin.Context) {
@@ -705,6 +745,32 @@ func (siw *ServerInterfaceWrapper) RefreshToken(c *gin.Context) {
siw.Handler.RefreshToken(c)
}
// RefreshProviderToken operation middleware
func (siw *ServerInterfaceWrapper) RefreshProviderToken(c *gin.Context) {
var err error
// ------------- Path parameter "provider" -------------
var provider RefreshProviderTokenParamsProvider
err = runtime.BindStyledParameterWithOptions("simple", "provider", c.Param("provider"), &provider, runtime.BindStyledParameterOptions{Explode: false, Required: true})
if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter provider: %w", err), http.StatusBadRequest)
return
}
c.Set(BearerAuthScopes, []string{})
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
return
}
}
siw.Handler.RefreshProviderToken(c, provider)
}
// VerifyToken operation middleware
func (siw *ServerInterfaceWrapper) VerifyToken(c *gin.Context) {
@@ -968,6 +1034,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options
router.GET(options.BaseURL+"/signin/provider/:provider", wrapper.SignInProvider)
router.GET(options.BaseURL+"/signin/provider/:provider/callback", wrapper.SignInProviderCallbackGet)
router.POST(options.BaseURL+"/signin/provider/:provider/callback", wrapper.SignInProviderCallbackPost)
router.GET(options.BaseURL+"/signin/provider/:provider/callback/tokens", wrapper.GetProviderTokens)
router.POST(options.BaseURL+"/signin/webauthn", wrapper.SignInWebauthn)
router.POST(options.BaseURL+"/signin/webauthn/verify", wrapper.VerifySignInWebauthn)
router.POST(options.BaseURL+"/signout", wrapper.SignOut)
@@ -975,6 +1042,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options
router.POST(options.BaseURL+"/signup/webauthn", wrapper.SignUpWebauthn)
router.POST(options.BaseURL+"/signup/webauthn/verify", wrapper.VerifySignUpWebauthn)
router.POST(options.BaseURL+"/token", wrapper.RefreshToken)
router.POST(options.BaseURL+"/token/provider/:provider", wrapper.RefreshProviderToken)
router.POST(options.BaseURL+"/token/verify", wrapper.VerifyToken)
router.GET(options.BaseURL+"/user", wrapper.GetUser)
router.POST(options.BaseURL+"/user/deanonymize", wrapper.DeanonymizeUser)
@@ -1610,6 +1678,35 @@ func (response SignInProviderCallbackPostdefaultJSONResponse) VisitSignInProvide
return json.NewEncoder(w).Encode(response.Body)
}
type GetProviderTokensRequestObject struct {
Provider GetProviderTokensParamsProvider `json:"provider"`
}
type GetProviderTokensResponseObject interface {
VisitGetProviderTokensResponse(w http.ResponseWriter) error
}
type GetProviderTokens200JSONResponse ProviderSession
func (response GetProviderTokens200JSONResponse) VisitGetProviderTokensResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type GetProviderTokensdefaultJSONResponse struct {
Body ErrorResponse
StatusCode int
}
func (response GetProviderTokensdefaultJSONResponse) VisitGetProviderTokensResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type SignInWebauthnRequestObject struct {
Body *SignInWebauthnJSONRequestBody
}
@@ -1813,6 +1910,36 @@ func (response RefreshTokendefaultJSONResponse) VisitRefreshTokenResponse(w http
return json.NewEncoder(w).Encode(response.Body)
}
type RefreshProviderTokenRequestObject struct {
Provider RefreshProviderTokenParamsProvider `json:"provider"`
Body *RefreshProviderTokenJSONRequestBody
}
type RefreshProviderTokenResponseObject interface {
VisitRefreshProviderTokenResponse(w http.ResponseWriter) error
}
type RefreshProviderToken200JSONResponse ProviderSession
func (response RefreshProviderToken200JSONResponse) VisitRefreshProviderTokenResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type RefreshProviderTokendefaultJSONResponse struct {
Body ErrorResponse
StatusCode int
}
func (response RefreshProviderTokendefaultJSONResponse) VisitRefreshProviderTokenResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type VerifyTokenRequestObject struct {
Body *VerifyTokenJSONRequestBody
}
@@ -2231,6 +2358,9 @@ type StrictServerInterface interface {
// OAuth2 provider callback endpoint (form_post)
// (POST /signin/provider/{provider}/callback)
SignInProviderCallbackPost(ctx context.Context, request SignInProviderCallbackPostRequestObject) (SignInProviderCallbackPostResponseObject, error)
// Retrieve OAuth2 provider tokens from callback
// (GET /signin/provider/{provider}/callback/tokens)
GetProviderTokens(ctx context.Context, request GetProviderTokensRequestObject) (GetProviderTokensResponseObject, error)
// Sign in with Webauthn
// (POST /signin/webauthn)
SignInWebauthn(ctx context.Context, request SignInWebauthnRequestObject) (SignInWebauthnResponseObject, error)
@@ -2252,6 +2382,9 @@ type StrictServerInterface interface {
// Refresh access token
// (POST /token)
RefreshToken(ctx context.Context, request RefreshTokenRequestObject) (RefreshTokenResponseObject, error)
// Refresh OAuth2 provider tokens
// (POST /token/provider/{provider})
RefreshProviderToken(ctx context.Context, request RefreshProviderTokenRequestObject) (RefreshProviderTokenResponseObject, error)
// Verify JWT token
// (POST /token/verify)
VerifyToken(ctx context.Context, request VerifyTokenRequestObject) (VerifyTokenResponseObject, error)
@@ -2952,6 +3085,33 @@ func (sh *strictHandler) SignInProviderCallbackPost(ctx *gin.Context, provider S
}
}
// GetProviderTokens operation middleware
func (sh *strictHandler) GetProviderTokens(ctx *gin.Context, provider GetProviderTokensParamsProvider) {
var request GetProviderTokensRequestObject
request.Provider = provider
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
return sh.ssi.GetProviderTokens(ctx, request.(GetProviderTokensRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "GetProviderTokens")
}
response, err := handler(ctx, request)
if err != nil {
ctx.Error(err)
ctx.Status(http.StatusInternalServerError)
} else if validResponse, ok := response.(GetProviderTokensResponseObject); ok {
if err := validResponse.VisitGetProviderTokensResponse(ctx.Writer); err != nil {
ctx.Error(err)
}
} else if response != nil {
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
}
}
// SignInWebauthn operation middleware
func (sh *strictHandler) SignInWebauthn(ctx *gin.Context) {
var request SignInWebauthnRequestObject
@@ -3185,6 +3345,41 @@ func (sh *strictHandler) RefreshToken(ctx *gin.Context) {
}
}
// RefreshProviderToken operation middleware
func (sh *strictHandler) RefreshProviderToken(ctx *gin.Context, provider RefreshProviderTokenParamsProvider) {
var request RefreshProviderTokenRequestObject
request.Provider = provider
var body RefreshProviderTokenJSONRequestBody
if err := ctx.ShouldBindJSON(&body); err != nil {
ctx.Status(http.StatusBadRequest)
ctx.Error(err)
return
}
request.Body = &body
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
return sh.ssi.RefreshProviderToken(ctx, request.(RefreshProviderTokenRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "RefreshProviderToken")
}
response, err := handler(ctx, request)
if err != nil {
ctx.Error(err)
ctx.Status(http.StatusInternalServerError)
} else if validResponse, ok := response.(RefreshProviderTokenResponseObject); ok {
if err := validResponse.VisitRefreshProviderTokenResponse(ctx.Writer); err != nil {
ctx.Error(err)
}
} else if response != nil {
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
}
}
// VerifyToken operation middleware
func (sh *strictHandler) VerifyToken(ctx *gin.Context) {
var request VerifyTokenRequestObject
@@ -3556,171 +3751,181 @@ func (sh *strictHandler) GetVersion(ctx *gin.Context) {
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"fwAAAP//NPNLB2r4AAA=",
"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",
}
// GetSwagger returns the content of the embedded swagger specification file

View File

@@ -1,6 +1,6 @@
// Package api 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 api
import (
@@ -206,22 +206,62 @@ const (
// Defines values for SignInProviderCallbackPostParamsProvider.
const (
Apple SignInProviderCallbackPostParamsProvider = "apple"
Azuread SignInProviderCallbackPostParamsProvider = "azuread"
Bitbucket SignInProviderCallbackPostParamsProvider = "bitbucket"
Discord SignInProviderCallbackPostParamsProvider = "discord"
Entraid SignInProviderCallbackPostParamsProvider = "entraid"
Facebook SignInProviderCallbackPostParamsProvider = "facebook"
Github SignInProviderCallbackPostParamsProvider = "github"
Gitlab SignInProviderCallbackPostParamsProvider = "gitlab"
Google SignInProviderCallbackPostParamsProvider = "google"
Linkedin SignInProviderCallbackPostParamsProvider = "linkedin"
Spotify SignInProviderCallbackPostParamsProvider = "spotify"
Strava SignInProviderCallbackPostParamsProvider = "strava"
Twitch SignInProviderCallbackPostParamsProvider = "twitch"
Twitter SignInProviderCallbackPostParamsProvider = "twitter"
Windowslive SignInProviderCallbackPostParamsProvider = "windowslive"
Workos SignInProviderCallbackPostParamsProvider = "workos"
SignInProviderCallbackPostParamsProviderApple SignInProviderCallbackPostParamsProvider = "apple"
SignInProviderCallbackPostParamsProviderAzuread SignInProviderCallbackPostParamsProvider = "azuread"
SignInProviderCallbackPostParamsProviderBitbucket SignInProviderCallbackPostParamsProvider = "bitbucket"
SignInProviderCallbackPostParamsProviderDiscord SignInProviderCallbackPostParamsProvider = "discord"
SignInProviderCallbackPostParamsProviderEntraid SignInProviderCallbackPostParamsProvider = "entraid"
SignInProviderCallbackPostParamsProviderFacebook SignInProviderCallbackPostParamsProvider = "facebook"
SignInProviderCallbackPostParamsProviderGithub SignInProviderCallbackPostParamsProvider = "github"
SignInProviderCallbackPostParamsProviderGitlab SignInProviderCallbackPostParamsProvider = "gitlab"
SignInProviderCallbackPostParamsProviderGoogle SignInProviderCallbackPostParamsProvider = "google"
SignInProviderCallbackPostParamsProviderLinkedin SignInProviderCallbackPostParamsProvider = "linkedin"
SignInProviderCallbackPostParamsProviderSpotify SignInProviderCallbackPostParamsProvider = "spotify"
SignInProviderCallbackPostParamsProviderStrava SignInProviderCallbackPostParamsProvider = "strava"
SignInProviderCallbackPostParamsProviderTwitch SignInProviderCallbackPostParamsProvider = "twitch"
SignInProviderCallbackPostParamsProviderTwitter SignInProviderCallbackPostParamsProvider = "twitter"
SignInProviderCallbackPostParamsProviderWindowslive SignInProviderCallbackPostParamsProvider = "windowslive"
SignInProviderCallbackPostParamsProviderWorkos SignInProviderCallbackPostParamsProvider = "workos"
)
// Defines values for GetProviderTokensParamsProvider.
const (
GetProviderTokensParamsProviderApple GetProviderTokensParamsProvider = "apple"
GetProviderTokensParamsProviderAzuread GetProviderTokensParamsProvider = "azuread"
GetProviderTokensParamsProviderBitbucket GetProviderTokensParamsProvider = "bitbucket"
GetProviderTokensParamsProviderDiscord GetProviderTokensParamsProvider = "discord"
GetProviderTokensParamsProviderEntraid GetProviderTokensParamsProvider = "entraid"
GetProviderTokensParamsProviderFacebook GetProviderTokensParamsProvider = "facebook"
GetProviderTokensParamsProviderGithub GetProviderTokensParamsProvider = "github"
GetProviderTokensParamsProviderGitlab GetProviderTokensParamsProvider = "gitlab"
GetProviderTokensParamsProviderGoogle GetProviderTokensParamsProvider = "google"
GetProviderTokensParamsProviderLinkedin GetProviderTokensParamsProvider = "linkedin"
GetProviderTokensParamsProviderSpotify GetProviderTokensParamsProvider = "spotify"
GetProviderTokensParamsProviderStrava GetProviderTokensParamsProvider = "strava"
GetProviderTokensParamsProviderTwitch GetProviderTokensParamsProvider = "twitch"
GetProviderTokensParamsProviderTwitter GetProviderTokensParamsProvider = "twitter"
GetProviderTokensParamsProviderWindowslive GetProviderTokensParamsProvider = "windowslive"
GetProviderTokensParamsProviderWorkos GetProviderTokensParamsProvider = "workos"
)
// Defines values for RefreshProviderTokenParamsProvider.
const (
Apple RefreshProviderTokenParamsProvider = "apple"
Azuread RefreshProviderTokenParamsProvider = "azuread"
Bitbucket RefreshProviderTokenParamsProvider = "bitbucket"
Discord RefreshProviderTokenParamsProvider = "discord"
Entraid RefreshProviderTokenParamsProvider = "entraid"
Facebook RefreshProviderTokenParamsProvider = "facebook"
Github RefreshProviderTokenParamsProvider = "github"
Gitlab RefreshProviderTokenParamsProvider = "gitlab"
Google RefreshProviderTokenParamsProvider = "google"
Linkedin RefreshProviderTokenParamsProvider = "linkedin"
Spotify RefreshProviderTokenParamsProvider = "spotify"
Strava RefreshProviderTokenParamsProvider = "strava"
Twitch RefreshProviderTokenParamsProvider = "twitch"
Twitter RefreshProviderTokenParamsProvider = "twitter"
Windowslive RefreshProviderTokenParamsProvider = "windowslive"
Workos RefreshProviderTokenParamsProvider = "workos"
)
// Defines values for VerifyTicketParamsType.
@@ -420,6 +460,21 @@ type OptionsRedirectTo struct {
RedirectTo *string `json:"redirectTo,omitempty"`
}
// ProviderSession OAuth2 provider session containing access and refresh tokens
type ProviderSession struct {
// AccessToken OAuth2 provider access token for API calls
AccessToken string `json:"accessToken"`
// ExpiresAt Timestamp when the access token expires
ExpiresAt time.Time `json:"expiresAt"`
// ExpiresIn Number of seconds until the access token expires
ExpiresIn int `json:"expiresIn"`
// RefreshToken OAuth2 provider refresh token for obtaining new access tokens (if provided by the provider)
RefreshToken *string `json:"refreshToken"`
}
// PublicKeyCredentialCreationOptions defines model for PublicKeyCredentialCreationOptions.
type PublicKeyCredentialCreationOptions = protocol.PublicKeyCredentialCreationOptions
@@ -441,6 +496,12 @@ type PublicKeyCredentialHints string
// PublicKeyCredentialRequestOptions defines model for PublicKeyCredentialRequestOptions.
type PublicKeyCredentialRequestOptions = protocol.PublicKeyCredentialRequestOptions
// RefreshProviderTokenRequest Request to refresh OAuth2 provider tokens
type RefreshProviderTokenRequest struct {
// RefreshToken OAuth2 provider refresh token obtained from previous authentication
RefreshToken string `json:"refreshToken"`
}
// RefreshTokenRequest Request to refresh an access token
type RefreshTokenRequest struct {
// RefreshToken Refresh token used to generate a new access token
@@ -844,6 +905,9 @@ type SignInProviderParams struct {
// Connect If set, this means that the user is already authenticated and wants to link their account. This needs to be a valid JWT access token.
Connect *string `form:"connect,omitempty" json:"connect,omitempty"`
// State Opaque state value to be returned by the provider
State *string `form:"state,omitempty" json:"state,omitempty"`
}
// SignInProviderParamsProvider defines parameters for SignInProvider.
@@ -907,6 +971,12 @@ type SignInProviderCallbackPostFormdataBody struct {
// SignInProviderCallbackPostParamsProvider defines parameters for SignInProviderCallbackPost.
type SignInProviderCallbackPostParamsProvider string
// GetProviderTokensParamsProvider defines parameters for GetProviderTokens.
type GetProviderTokensParamsProvider string
// RefreshProviderTokenParamsProvider defines parameters for RefreshProviderToken.
type RefreshProviderTokenParamsProvider string
// VerifyTicketParams defines parameters for VerifyTicket.
type VerifyTicketParams struct {
// Ticket Ticket
@@ -985,6 +1055,9 @@ type VerifySignUpWebauthnJSONRequestBody = SignUpWebauthnVerifyRequest
// RefreshTokenJSONRequestBody defines body for RefreshToken for application/json ContentType.
type RefreshTokenJSONRequestBody = RefreshTokenRequest
// RefreshProviderTokenJSONRequestBody defines body for RefreshProviderToken for application/json ContentType.
type RefreshProviderTokenJSONRequestBody = RefreshProviderTokenRequest
// VerifyTokenJSONRequestBody defines body for VerifyToken for application/json ContentType.
type VerifyTokenJSONRequestBody = VerifyTokenRequest

View File

@@ -128,6 +128,9 @@ type DBClient interface { //nolint:interfacebloat
ctx context.Context,
arg sql.RefreshTokenAndGetUserRolesParams,
) ([]sql.RefreshTokenAndGetUserRolesRow, error)
GetProviderSession(ctx context.Context, arg sql.GetProviderSessionParams) (string, error)
UpdateProviderSession(ctx context.Context, arg sql.UpdateProviderSessionParams) error
}
type Encrypter interface {

View File

@@ -164,6 +164,10 @@ func (response ErrorResponse) VisitVerifySignUpWebauthnResponse(w http.ResponseW
return response.visit(w)
}
func (response ErrorResponse) VisitRefreshProviderTokenResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitRefreshTokenResponse(w http.ResponseWriter) error {
return response.visit(w)
}
@@ -224,6 +228,10 @@ func (response ErrorResponse) VisitGetUserResponse(w http.ResponseWriter) error
return response.visit(w)
}
func (response ErrorResponse) VisitGetProviderTokensResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitVerifySignInPasswordlessSmsResponse(
w http.ResponseWriter,
) error {

View File

@@ -0,0 +1,62 @@
package controller
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/jackc/pgx/v5"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/sql"
)
func (ctrl *Controller) GetProviderTokens( //nolint:ireturn
ctx context.Context,
req api.GetProviderTokensRequestObject,
) (api.GetProviderTokensResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger = logger.With("provider", req.Provider)
user, apiErr := ctrl.wf.GetUserFromJWTInContext(ctx, logger)
if apiErr != nil {
return ctrl.sendError(apiErr), nil
}
sessionEnc, err := ctrl.wf.db.GetProviderSession(
ctx, sql.GetProviderSessionParams{
UserID: sql.UUID(user.ID),
ProviderID: sql.Text(req.Provider),
},
)
if errors.Is(err, pgx.ErrNoRows) {
logger.InfoContext(ctx, "no provider session found")
return api.GetProviderTokens200JSONResponse{
AccessToken: "",
ExpiresIn: 0,
ExpiresAt: time.Time{},
RefreshToken: nil,
}, nil
}
if err != nil {
logger.ErrorContext(ctx, "failed to get provider session", logError(err))
return ctrl.sendError(ErrInternalServerError), nil
}
b, err := ctrl.encrypter.Decrypt([]byte(sessionEnc))
if err != nil {
logger.ErrorContext(ctx, "failed to decrypt provider session", logError(err))
return ctrl.sendError(ErrInternalServerError), nil
}
var session api.ProviderSession
if err := json.Unmarshal(b, &session); err != nil {
logger.ErrorContext(ctx, "failed to unmarshal provider session", logError(err))
return ctrl.sendError(ErrInternalServerError), nil
}
return api.GetProviderTokens200JSONResponse(session), nil
}

View File

@@ -0,0 +1,245 @@
package controller_test
import (
"errors"
"net/http"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/controller"
"github.com/nhost/nhost/services/auth/go/controller/mock"
"github.com/nhost/nhost/services/auth/go/sql"
"github.com/nhost/nhost/services/auth/go/testhelpers"
"go.uber.org/mock/gomock"
)
func TestGetProviderTokens(t *testing.T) {
t.Parallel()
userID := uuid.MustParse("DB477732-48FA-4289-B694-2886A646B6EB")
jwtTokenFn := func() *jwt.Token {
return &jwt.Token{
Raw: "",
Method: jwt.SigningMethodHS256,
Header: map[string]any{
"alg": "HS256",
"typ": "JWT",
},
Claims: jwt.MapClaims{
"exp": float64(time.Now().Add(900 * time.Second).Unix()),
"https://hasura.io/jwt/claims": map[string]any{
"x-hasura-allowed-roles": []any{"anonymous"},
"x-hasura-default-role": "anonymous",
"x-hasura-user-id": "db477732-48fa-4289-b694-2886a646b6eb",
"x-hasura-user-is-anonymous": "true",
},
"iat": float64(time.Now().Unix()),
"iss": "hasura-auth",
"sub": "db477732-48fa-4289-b694-2886a646b6eb",
},
Signature: []byte{},
Valid: true,
}
}
cases := []testRequest[api.GetProviderTokensRequestObject, api.GetProviderTokensResponseObject]{
{
name: "success",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUser(
gomock.Any(),
userID,
).Return(sql.AuthUser{ //nolint:exhaustruct
ID: userID,
Email: sql.Text("jane@acme.com"),
DisplayName: "Jane Doe",
Disabled: false,
}, nil)
mock.EXPECT().GetProviderSession(
gomock.Any(),
sql.GetProviderSessionParams{
UserID: sql.UUID(userID),
ProviderID: sql.Text("fake"),
},
).Return("1c6d0df04134afec1c9a3e95b4bdc48cf62780df72b537b8158845b6b71400c225f7d686cf2ca656553f7e4f771d29f6ba53b12f700d34ddab0386b92a541cdebdb15a294bcb00bbafd5cfb0072aeca0792b81a3be3a2316090b814ac3d04ef6b19eb4246ef89b461ce62abb165c5553a5a1766b1cf3bd19a3ada61abf1347fcaef1b43c134c21d8a6597aa7f2349ae3795ee7edff31ee44933b28e273bd53c768b7a5d8b5e898", nil) //nolint:lll
return mock
},
request: api.GetProviderTokensRequestObject{
Provider: "fake",
},
expectedResponse: api.GetProviderTokens200JSONResponse{
AccessToken: "valid-accesstoken-1",
ExpiresIn: 9000,
ExpiresAt: time.Date(2025, 10, 27, 12, 29, 7, 0, time.UTC),
RefreshToken: ptr("valid-refreshtoken-1"),
},
expectedJWT: nil,
jwtTokenFn: jwtTokenFn,
getControllerOpts: nil,
},
{
name: "user disabled",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUser(
gomock.Any(),
userID,
).Return(sql.AuthUser{ //nolint:exhaustruct
ID: userID,
Email: sql.Text("jane@acme.com"),
DisplayName: "Jane Doe",
Disabled: true,
}, nil)
return mock
},
request: api.GetProviderTokensRequestObject{
Provider: "fake",
},
expectedResponse: controller.ErrorResponse{
Error: "disabled-user",
Message: "User is disabled",
Status: 401,
},
expectedJWT: nil,
jwtTokenFn: jwtTokenFn,
getControllerOpts: nil,
},
{
name: "session not found",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUser(
gomock.Any(),
userID,
).Return(sql.AuthUser{ //nolint:exhaustruct
ID: userID,
Email: sql.Text("jane@acme.com"),
DisplayName: "Jane Doe",
Disabled: false,
}, nil)
mock.EXPECT().GetProviderSession(
gomock.Any(),
sql.GetProviderSessionParams{
UserID: sql.UUID(userID),
ProviderID: sql.Text("fake"),
},
).Return("", pgx.ErrNoRows)
return mock
},
request: api.GetProviderTokensRequestObject{
Provider: "fake",
},
expectedResponse: api.GetProviderTokens200JSONResponse{
AccessToken: "",
ExpiresIn: 0,
ExpiresAt: time.Time{},
RefreshToken: nil,
},
expectedJWT: nil,
jwtTokenFn: jwtTokenFn,
getControllerOpts: nil,
},
{
name: "error",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUser(
gomock.Any(),
userID,
).Return(sql.AuthUser{ //nolint:exhaustruct
ID: userID,
Email: sql.Text("jane@acme.com"),
DisplayName: "Jane Doe",
Disabled: false,
}, nil)
mock.EXPECT().GetProviderSession(
gomock.Any(),
sql.GetProviderSessionParams{
UserID: sql.UUID(userID),
ProviderID: sql.Text("fake"),
},
).Return("", errors.New("database error")) //nolint:err113
return mock
},
request: api.GetProviderTokensRequestObject{
Provider: "fake",
},
expectedResponse: controller.ErrorResponse{
Error: "internal-server-error",
Message: "Internal server error",
Status: 500,
},
expectedJWT: nil,
jwtTokenFn: jwtTokenFn,
getControllerOpts: nil,
},
{
name: "missing jwt",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
return mock
},
request: api.GetProviderTokensRequestObject{
Provider: "fake",
},
expectedResponse: controller.ErrorResponse{
Error: "invalid-request",
Message: "The request payload is incorrect",
Status: http.StatusBadRequest,
},
expectedJWT: nil,
jwtTokenFn: nil,
getControllerOpts: nil,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
c, jwtGetter := getController(t, ctrl, tc.config, tc.db, tc.getControllerOpts...)
ctx := t.Context()
if tc.jwtTokenFn != nil {
ctx = jwtGetter.ToContext(t.Context(), tc.jwtTokenFn())
}
assertRequest(
ctx, t, c.GetProviderTokens, tc.request, tc.expectedResponse,
testhelpers.FilterPathLast(
[]string{".ExpiresAt"}, cmpopts.EquateApproxTime(time.Hour),
),
)
})
}
}

View File

@@ -693,6 +693,21 @@ func (mr *MockDBClientMockRecorder) FindUserProviderByProviderId(ctx, arg any) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindUserProviderByProviderId", reflect.TypeOf((*MockDBClient)(nil).FindUserProviderByProviderId), ctx, arg)
}
// GetProviderSession mocks base method.
func (m *MockDBClient) GetProviderSession(ctx context.Context, arg sql.GetProviderSessionParams) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetProviderSession", ctx, arg)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetProviderSession indicates an expected call of GetProviderSession.
func (mr *MockDBClientMockRecorder) GetProviderSession(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProviderSession", reflect.TypeOf((*MockDBClient)(nil).GetProviderSession), ctx, arg)
}
// GetSecurityKeys mocks base method.
func (m *MockDBClient) GetSecurityKeys(ctx context.Context, userID uuid.UUID) ([]sql.AuthUserSecurityKey, error) {
m.ctrl.T.Helper()
@@ -978,6 +993,20 @@ func (mr *MockDBClientMockRecorder) RefreshTokenAndGetUserRoles(ctx, arg any) *g
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshTokenAndGetUserRoles", reflect.TypeOf((*MockDBClient)(nil).RefreshTokenAndGetUserRoles), ctx, arg)
}
// UpdateProviderSession mocks base method.
func (m *MockDBClient) UpdateProviderSession(ctx context.Context, arg sql.UpdateProviderSessionParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateProviderSession", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateProviderSession indicates an expected call of UpdateProviderSession.
func (mr *MockDBClientMockRecorder) UpdateProviderSession(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProviderSession", reflect.TypeOf((*MockDBClient)(nil).UpdateProviderSession), ctx, arg)
}
// UpdateUserActiveMFAType mocks base method.
func (m *MockDBClient) UpdateUserActiveMFAType(ctx context.Context, arg sql.UpdateUserActiveMFATypeParams) error {
m.ctrl.T.Helper()

View File

@@ -0,0 +1,40 @@
package controller
import (
"context"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"golang.org/x/oauth2"
)
func (ctrl *Controller) RefreshProviderToken( //nolint:ireturn
ctx context.Context, req api.RefreshProviderTokenRequestObject,
) (api.RefreshProviderTokenResponseObject, error) {
logger := middleware.LoggerFromContext(ctx)
logger = logger.With("provider", req.Provider)
provider := ctrl.Providers.Get(string(req.Provider))
if provider == nil {
logger.ErrorContext(ctx, "provider not enabled")
return ctrl.sendError(ErrDisabledEndpoint), nil
}
if !provider.IsOauth2() {
logger.ErrorContext(ctx, "provider does not support OAuth2")
return ctrl.sendError(ErrOauthProviderError), nil
}
token, err := provider.Oauth2().Exchange(
ctx,
"",
oauth2.SetAuthURLParam("grant_type", "refresh_token"),
oauth2.SetAuthURLParam("refresh_token", req.Body.RefreshToken),
)
if err != nil {
logger.ErrorContext(ctx, "failed to exchange code for token", "error", err)
return ctrl.sendError(ErrOauthProviderError), nil
}
return api.RefreshProviderToken200JSONResponse(tokenToProviderSession(token)), nil
}

View File

@@ -61,7 +61,7 @@ func TestSignInIdToken(t *testing.T) { //nolint:maintidx
{
name: "signup - simple",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient { //nolint:dupl
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUserByProviderID(
@@ -169,7 +169,7 @@ func TestSignInIdToken(t *testing.T) { //nolint:maintidx
{
name: "signup - with options",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient { //nolint:dupl
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUserByProviderID(
@@ -433,7 +433,7 @@ func TestSignInIdToken(t *testing.T) { //nolint:maintidx
{
name: "signin - simple - provider id found",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient { //nolint:dupl
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUserByProviderID( //nolint:dupl
@@ -563,7 +563,7 @@ func TestSignInIdToken(t *testing.T) { //nolint:maintidx
{
name: "signin - simple - user id found",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient { //nolint:dupl
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUserByProviderID(
@@ -574,7 +574,7 @@ func TestSignInIdToken(t *testing.T) { //nolint:maintidx
},
).Return(sql.AuthUser{}, pgx.ErrNoRows) //nolint:exhaustruct
mock.EXPECT().GetUserByEmail(
mock.EXPECT().GetUserByEmail( //nolint:dupl
gomock.Any(),
sql.Text("jane@myapp.local"),
).Return(

View File

@@ -67,6 +67,7 @@ func (ctrl *Controller) SignInProvider( //nolint:ireturn
Metadata: req.Params.Metadata,
RedirectTo: req.Params.RedirectTo,
},
"state": req.Params.State,
},
time.Now().Add(time.Minute),
)

View File

@@ -2,13 +2,18 @@ package controller
import (
"context"
"encoding/json"
"log/slog"
"net/url"
"time"
"golang.org/x/oauth2"
"github.com/nhost/nhost/services/auth/go/api"
"github.com/nhost/nhost/services/auth/go/middleware"
"github.com/nhost/nhost/services/auth/go/oidc"
"github.com/nhost/nhost/services/auth/go/providers"
"github.com/nhost/nhost/services/auth/go/sql"
)
type providerCallbackData struct {
@@ -24,6 +29,24 @@ type providerCallbackData struct {
Extras map[string]any
}
func (ctrl *Controller) getStateData(
ctx context.Context, state string, logger *slog.Logger,
) (*providers.State, *APIError) {
stateToken, err := ctrl.wf.jwtGetter.Validate(state)
if err != nil {
logger.ErrorContext(ctx, "invalid state token", logError(err))
return nil, ErrInvalidState
}
stateData := &providers.State{} //nolint:exhaustruct
if err := stateData.Decode(stateToken.Claims); err != nil {
logger.ErrorContext(ctx, "error decoding state token", logError(err))
return nil, ErrInvalidState
}
return stateData, nil
}
func (ctrl *Controller) signinProviderProviderCallbackValidate(
ctx context.Context,
req providerCallbackData,
@@ -31,16 +54,9 @@ func (ctrl *Controller) signinProviderProviderCallbackValidate(
) (*api.SignUpOptions, *string, *url.URL, *APIError) {
redirectTo := ctrl.config.ClientURL
stateToken, err := ctrl.wf.jwtGetter.Validate(req.State)
if err != nil {
logger.ErrorContext(ctx, "invalid state token", logError(err))
return nil, nil, redirectTo, ErrInvalidState
}
stateData := &providers.State{} //nolint:exhaustruct
if err := stateData.Decode(stateToken.Claims); err != nil {
logger.ErrorContext(ctx, "error decoding state token", logError(err))
return nil, nil, redirectTo, ErrInvalidState
stateData, apiErr := ctrl.getStateData(ctx, req.State, logger)
if apiErr != nil {
return nil, nil, redirectTo, apiErr
}
// we just care about the redirect URL for now, the rest is handled by the signin flow
@@ -60,6 +76,11 @@ func (ctrl *Controller) signinProviderProviderCallbackValidate(
values.Add("provider_error", deptr(req.Error))
values.Add("provider_error_description", deptr(req.ErrorDescription))
values.Add("provider_error_url", deptr(req.ErrorURI))
if stateData.State != nil && *stateData.State != "" {
values.Add("state", *stateData.State)
}
redirectTo.RawQuery = values.Encode()
return nil, nil, redirectTo, ErrOauthProviderError
@@ -71,22 +92,49 @@ func (ctrl *Controller) signinProviderProviderCallbackValidate(
return nil, nil, redirectTo, ErrInvalidRequest
}
if stateData.State != nil && *stateData.State != "" {
values := optionsRedirectTo.Query()
values.Add("state", *stateData.State)
optionsRedirectTo.RawQuery = values.Encode()
}
return stateData.Options, stateData.Connect, optionsRedirectTo, nil
}
func tokenToProviderSession(token *oauth2.Token) api.ProviderSession {
expiresIn := int(token.ExpiresIn)
if expiresIn == 0 && !token.Expiry.IsZero() {
expiresIn = int(time.Until(token.Expiry).Seconds())
}
expiresAt := token.Expiry
if token.Expiry.IsZero() {
expiresAt = time.Now().Add(time.Duration(expiresIn) * time.Second)
}
return api.ProviderSession{
AccessToken: token.AccessToken,
ExpiresIn: expiresIn,
ExpiresAt: expiresAt,
RefreshToken: ptr(token.RefreshToken),
}
}
func (ctrl *Controller) signinProviderProviderCallbackOauthFlow(
ctx context.Context,
req providerCallbackData,
logger *slog.Logger,
) (oidc.Profile, *APIError) {
) (oidc.Profile, api.ProviderSession, *APIError) {
p := ctrl.Providers.Get(req.Provider)
if p == nil {
logger.ErrorContext(ctx, "provider not enabled")
return oidc.Profile{}, ErrDisabledEndpoint
return oidc.Profile{}, api.ProviderSession{}, ErrDisabledEndpoint
}
var profile oidc.Profile
var (
profile oidc.Profile
providerSession api.ProviderSession
)
switch {
case p.IsOauth1():
accessTokenValue, accessTokenSecret, err := p.Oauth1().AccessToken(
@@ -94,34 +142,59 @@ func (ctrl *Controller) signinProviderProviderCallbackOauthFlow(
)
if err != nil {
logger.ErrorContext(ctx, "failed to request token", logError(err))
return oidc.Profile{}, ErrOauthProfileFetchFailed
return oidc.Profile{}, api.ProviderSession{}, ErrOauthProfileFetchFailed
}
profile, err = p.Oauth1().GetProfile(ctx, accessTokenValue, accessTokenSecret)
if err != nil {
logger.ErrorContext(ctx, "failed to get user info", logError(err))
return oidc.Profile{}, ErrOauthProfileFetchFailed
return oidc.Profile{}, api.ProviderSession{}, ErrOauthProfileFetchFailed
}
providerSession.AccessToken = accessTokenValue
default:
token, err := p.Oauth2().Exchange(ctx, deptr(req.Code))
if err != nil {
logger.ErrorContext(ctx, "failed to exchange token", logError(err))
return oidc.Profile{}, ErrOauthTokenExchangeFailed
return oidc.Profile{}, api.ProviderSession{}, ErrOauthTokenExchangeFailed
}
profile, err = p.Oauth2().GetProfile(ctx, token.AccessToken, req.IDToken, req.Extras)
if err != nil {
logger.ErrorContext(ctx, "failed to get user info", logError(err))
return oidc.Profile{}, ErrOauthProfileFetchFailed
return oidc.Profile{}, api.ProviderSession{}, ErrOauthProfileFetchFailed
}
providerSession = tokenToProviderSession(token)
}
if profile.ProviderUserID == "" {
logger.ErrorContext(ctx, "provider user id is empty")
return oidc.Profile{}, ErrOauthProfileFetchFailed
return oidc.Profile{}, api.ProviderSession{}, ErrOauthProfileFetchFailed
}
return profile, nil
return profile, providerSession, nil
}
func encryptProviderSession(
ctx context.Context,
encrypter Encrypter,
providerSession api.ProviderSession,
logger *slog.Logger,
) (string, *APIError) {
b, err := json.Marshal(providerSession)
if err != nil {
logger.ErrorContext(ctx, "failed to marshal provider session", logError(err))
return "", ErrInternalServerError
}
providerSessionEnc, err := encrypter.Encrypt(b)
if err != nil {
logger.ErrorContext(ctx, "failed to encrypt provider session", logError(err))
return "", ErrInternalServerError
}
return string(providerSessionEnc), nil
}
func (ctrl *Controller) signinProviderProviderCallback(
@@ -139,7 +212,11 @@ func (ctrl *Controller) signinProviderProviderCallback(
return redirectTo, apiErr
}
profile, apiErr := ctrl.signinProviderProviderCallbackOauthFlow(ctx, req, logger)
profile, providerSession, apiErr := ctrl.signinProviderProviderCallbackOauthFlow(
ctx,
req,
logger,
)
if apiErr != nil {
return redirectTo, apiErr
}
@@ -165,6 +242,22 @@ func (ctrl *Controller) signinProviderProviderCallback(
}
}
providerSessionEnc, apiErr := encryptProviderSession(
ctx, ctrl.encrypter, providerSession, logger,
)
if apiErr != nil {
return redirectTo, apiErr
}
if err := ctrl.wf.db.UpdateProviderSession(ctx, sql.UpdateProviderSessionParams{
ProviderID: req.Provider,
ProviderUserID: profile.ProviderUserID,
AccessToken: providerSessionEnc,
}); err != nil {
logger.ErrorContext(ctx, "failed to update provider tokens", logError(err))
return redirectTo, ErrInternalServerError
}
return redirectTo, nil
}

View File

@@ -37,6 +37,7 @@ func getState(
Metadata: options.Metadata,
RedirectTo: options.RedirectTo,
},
"state": "some-random-state",
},
time.Now().Add(time.Minute),
)
@@ -77,7 +78,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
{ //nolint:dupl
name: "signup",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient { //nolint:dupl
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUserByProviderID(
@@ -120,6 +121,11 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
),
).Return(insertResponse, nil)
mock.EXPECT().UpdateProviderSession(
gomock.Any(),
gomock.Any(),
).Return(nil)
return mock
},
request: api.SignInProviderCallbackGetRequestObject{
@@ -131,7 +137,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: api.SignInProviderCallbackGet302Response{
Headers: api.SignInProviderCallbackGet302ResponseHeaders{
Location: `^http://localhost:3000\?refreshToken=.*$`,
Location: `^http://localhost:3000\?refreshToken=[\w+-]+&state=some-random-state$`,
},
},
expectedJWT: nil,
@@ -142,7 +148,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
{
name: "signup - with options",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient { //nolint:dupl
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUserByProviderID(
@@ -185,6 +191,11 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
),
).Return(insertResponse, nil)
mock.EXPECT().UpdateProviderSession(
gomock.Any(),
gomock.Any(),
).Return(nil)
return mock
},
request: api.SignInProviderCallbackGetRequestObject{
@@ -205,7 +216,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: api.SignInProviderCallbackGet302Response{
Headers: api.SignInProviderCallbackGet302ResponseHeaders{
Location: `^http://localhost:3000/redirect/me/here\?refreshToken=.*$`,
Location: `^http://localhost:3000/redirect/me/here\?refreshToken=[\w+-]+&state=some-random-state$`,
},
},
expectedJWT: nil,
@@ -247,7 +258,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `^http://localhost:3000\?error=signup-disabled&errorDescription=.*$`,
Location: `^http://localhost:3000\?error=signup-disabled&errorDescription=Sign\+up\+is\+disabled.&state=some-random-state$`, //nolint:lll
},
},
expectedJWT: nil,
@@ -314,7 +325,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `^http://localhost:3000\?error=disabled-user&errorDescription=.*$`,
Location: `^http://localhost:3000\?error=disabled-user&errorDescription=User\+is\+disabled&state=some-random-state$`, //nolint:lll
},
},
expectedJWT: nil,
@@ -356,7 +367,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `^http://localhost:3000\?error=invalid-email-password&errorDescription=.*$`,
Location: `^http://localhost:3000\?error=invalid-email-password&errorDescription=Incorrect\+email\+or\+password&state=some-random-state$`, //nolint:lll
},
},
expectedJWT: nil,
@@ -367,7 +378,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
{
name: "signin - simple - provider id found",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient { //nolint:dupl
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUserByProviderID( //nolint:dupl
@@ -432,6 +443,11 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
gomock.Any(), userID,
).Return(sql.TimestampTz(time.Now()), nil)
mock.EXPECT().UpdateProviderSession(
gomock.Any(),
gomock.Any(),
).Return(nil)
return mock
},
request: api.SignInProviderCallbackGetRequestObject{
@@ -443,7 +459,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: api.SignInProviderCallbackGet302Response{
Headers: api.SignInProviderCallbackGet302ResponseHeaders{
Location: `^http://localhost:3000\?refreshToken=.*$`,
Location: `^http://localhost:3000\?refreshToken=[\w+-]+&state=some-random-state$`,
},
},
expectedJWT: nil,
@@ -454,7 +470,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
{
name: "signin - simple - email found",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient { //nolint:dupl
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUserByProviderID(
@@ -465,7 +481,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
).Return(sql.AuthUser{}, pgx.ErrNoRows) //nolint:exhaustruct
mock.EXPECT().GetUserByEmail(
mock.EXPECT().GetUserByEmail( //nolint:dupl
gomock.Any(),
sql.Text("user1@fake.com"),
).Return(
@@ -544,6 +560,11 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
gomock.Any(), userID,
).Return(sql.TimestampTz(time.Now()), nil)
mock.EXPECT().UpdateProviderSession(
gomock.Any(),
gomock.Any(),
).Return(nil)
return mock
},
request: api.SignInProviderCallbackGetRequestObject{
@@ -555,7 +576,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: api.SignInProviderCallbackGet302Response{
Headers: api.SignInProviderCallbackGet302ResponseHeaders{
Location: `^http://localhost:3000\?refreshToken=.*$`,
Location: `^http://localhost:3000\?refreshToken=[\w+-]+&state=some-random-state$`,
},
},
expectedJWT: nil,
@@ -620,7 +641,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `^http://localhost:3000\?error=disabled-user&errorDescription=.*$`,
Location: `^http://localhost:3000\?error=disabled-user&errorDescription=User\+is\+disabled&state=some-random-state$`, //nolint:lll
},
},
expectedJWT: nil,
@@ -697,7 +718,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `http://localhost:3000?error=disabled-endpoint&errorDescription=This+endpoint+is+disabled`,
Location: `^http://localhost:3000\?error=disabled-endpoint&errorDescription=This\+endpoint\+is\+disabled&state=some-random-state$`, //nolint:lll
},
},
expectedJWT: nil,
@@ -725,8 +746,8 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
Provider: "fake",
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: api.SignInProviderCallbackGet302ResponseHeaders{
Location: `http://localhost:3000?error=oauth-provider-error&errorDescription=Provider+returned+an+error&provider_error=error-coming-from-provider&provider_error_description=This+is+an+error+coming+from+the+provider&provider_error_url=https%3A%2F%2Fexample.com%2Ferror`, //nolint:lll
Headers: struct{ Location string }{
Location: `^http://localhost:3000\?error=oauth-provider-error&errorDescription=Provider\+returned\+an\+error&provider_error=error-coming-from-provider&provider_error_description=This\+is\+an\+error\+coming\+from\+the\+provider&provider_error_url=https%3A%2F%2Fexample.com%2Ferror&state=some-random-state$`, //nolint:lll //nolint:lll
},
},
expectedJWT: nil,
@@ -797,6 +818,11 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
}, nil,
)
mock.EXPECT().UpdateProviderSession(
gomock.Any(),
gomock.Any(),
).Return(nil)
return mock
},
request: api.SignInProviderCallbackGetRequestObject{
@@ -810,7 +836,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: api.SignInProviderCallbackGet302Response{
Headers: api.SignInProviderCallbackGet302ResponseHeaders{
Location: `^http://localhost:3000/connect-success$`,
Location: `^http://localhost:3000/connect-success\?state=some-random-state$`,
},
},
expectedJWT: nil,
@@ -873,7 +899,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `^http://localhost:3000/connect-success\?error=disabled-user&errorDescription=.*$`,
Location: `^http://localhost:3000/connect-success\?error=disabled-user&errorDescription=User\+is\+disabled&state=some-random-state$`, //nolint:lll
},
},
expectedJWT: nil,
@@ -905,7 +931,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `^http://localhost:3000/connect-success\?error=invalid-email-password&errorDescription=.*$`,
Location: `^http://localhost:3000/connect-success\?error=invalid-email-password&errorDescription=Incorrect\+email\+or\+password&state=some-random-state$`, //nolint:lll
},
},
expectedJWT: nil,
@@ -980,7 +1006,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `^http://localhost:3000/connect-success\?error=invalid-request&errorDescription=.*$`,
Location: `^http://localhost:3000/connect-success\?error=invalid-request&errorDescription=The\+request\+payload\+is\+incorrect&state=some-random-state$`, //nolint:lll
},
},
expectedJWT: nil,
@@ -1008,7 +1034,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `^http://localhost:3000/connect-success\?error=invalid-request&errorDescription=.*$`,
Location: `^http://localhost:3000/connect-success\?error=invalid-request&errorDescription=The\+request\+payload\+is\+incorrect&state=some-random-state$`, //nolint:lll
},
},
expectedJWT: nil,
@@ -1019,7 +1045,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
{ //nolint:dupl
name: "signup - empty email",
config: getConfig,
db: func(ctrl *gomock.Controller) controller.DBClient { //nolint:dupl
db: func(ctrl *gomock.Controller) controller.DBClient {
mock := mock.NewMockDBClient(ctrl)
mock.EXPECT().GetUserByProviderID(
@@ -1062,6 +1088,11 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
),
).Return(insertResponse, nil)
mock.EXPECT().UpdateProviderSession(
gomock.Any(),
gomock.Any(),
).Return(nil)
return mock
},
request: api.SignInProviderCallbackGetRequestObject{
@@ -1073,7 +1104,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: api.SignInProviderCallbackGet302Response{
Headers: api.SignInProviderCallbackGet302ResponseHeaders{
Location: `^http://localhost:3000\?refreshToken=.*$`,
Location: `^http://localhost:3000\?refreshToken=[\w+-]+&state=some-random-state$`,
},
},
expectedJWT: nil,
@@ -1115,7 +1146,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `^http://localhost:3000\?error=signup-disabled&errorDescription=.*$`,
Location: `^http://localhost:3000\?error=signup-disabled&errorDescription=Sign\+up\+is\+disabled.&state=some-random-state$`, //nolint:lll
},
},
expectedJWT: nil,
@@ -1182,7 +1213,7 @@ func TestSignInProviderCallback(t *testing.T) { //nolint:maintidx
},
expectedResponse: controller.ErrorRedirectResponse{
Headers: struct{ Location string }{
Location: `^http://localhost:3000\?error=disabled-user&errorDescription=.*$`,
Location: `^http://localhost:3000\?error=disabled-user&errorDescription=User\+is\+disabled&state=some-random-state$`, //nolint:lll
},
},
expectedJWT: nil,

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