Compare commits
20 Commits
@nhost/das
...
cli@1.35.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8c9e1e239 | ||
|
|
c662d063a7 | ||
|
|
0cc0f74404 | ||
|
|
dd04d64a6b | ||
|
|
b518132349 | ||
|
|
b677d3768f | ||
|
|
51ec151752 | ||
|
|
223322d654 | ||
|
|
add2c20c95 | ||
|
|
961bc5feea | ||
|
|
0ca89974b9 | ||
|
|
e8d52859a3 | ||
|
|
67740ebe3d | ||
|
|
d6f7b01aee | ||
|
|
0fc65df78d | ||
|
|
52e3db7f61 | ||
|
|
235449d68c | ||
|
|
323834d212 | ||
|
|
f7bd250f73 | ||
|
|
579f9dbf31 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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.
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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 [...]
|
||||
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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:
|
||||
|
||||
7
.github/workflows/auth_checks.yaml
vendored
7
.github/workflows/auth_checks.yaml
vendored
@@ -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:
|
||||
|
||||
9
.github/workflows/cli_checks.yaml
vendored
9
.github/workflows/cli_checks.yaml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
7
.github/workflows/codegen_checks.yaml
vendored
7
.github/workflows/codegen_checks.yaml
vendored
@@ -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:
|
||||
|
||||
10
.github/workflows/dashboard_checks.yaml
vendored
10
.github/workflows/dashboard_checks.yaml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
4
.github/workflows/docs_checks.yaml
vendored
4
.github/workflows/docs_checks.yaml
vendored
@@ -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 }}
|
||||
|
||||
7
.github/workflows/examples_demos_checks.yaml
vendored
7
.github/workflows/examples_demos_checks.yaml
vendored
@@ -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"]'
|
||||
|
||||
@@ -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"]'
|
||||
|
||||
@@ -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"]'
|
||||
|
||||
2
.github/workflows/gen_ai_review.yaml
vendored
2
.github/workflows/gen_ai_review.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: "gen: AI review"
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
|
||||
7
.github/workflows/nhost-js_checks.yaml
vendored
7
.github/workflows/nhost-js_checks.yaml
vendored
@@ -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:
|
||||
|
||||
7
.github/workflows/nixops_checks.yaml
vendored
7
.github/workflows/nixops_checks.yaml
vendored
@@ -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:
|
||||
|
||||
7
.github/workflows/storage_checks.yaml
vendored
7
.github/workflows/storage_checks.yaml
vendored
@@ -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:
|
||||
|
||||
4
.github/workflows/wf_build_artifacts.yaml
vendored
4
.github/workflows/wf_build_artifacts.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/wf_docker_push_image.yaml
vendored
2
.github/workflows/wf_docker_push_image.yaml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 245 KiB |
@@ -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>
|
||||
|
||||
41
docs/products/auth/providers/connect.mdx
Normal file
41
docs/products/auth/providers/connect.mdx
Normal 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:
|
||||
|
||||

|
||||
|
||||
Using the following GraphQL query:
|
||||
|
||||
``` js
|
||||
const { error, data } = await nhost.graphql.request(
|
||||
gql`
|
||||
query getAuthUserProviders {
|
||||
authUserProviders {
|
||||
id
|
||||
providerId
|
||||
}
|
||||
}
|
||||
`,
|
||||
)
|
||||
```
|
||||
@@ -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.
|
||||
61
docs/products/auth/providers/overview.mdx
Normal file
61
docs/products/auth/providers/overview.mdx
Normal 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
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Sign In with WorkOS
|
||||
description: Follow this guide to sign in users with WorkOS.
|
||||
icon: workos
|
||||
icon: W
|
||||
---
|
||||
|
||||
|
||||
137
docs/products/auth/providers/tokens.mdx
Normal file
137
docs/products/auth/providers/tokens.mdx
Normal 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
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
Using the following GraphQL query:
|
||||
|
||||
``` js
|
||||
const { error, data } = await nhost.graphql.request(
|
||||
gql`
|
||||
query getAuthUserProviders {
|
||||
authUserProviders {
|
||||
id
|
||||
providerId
|
||||
}
|
||||
}
|
||||
`,
|
||||
)
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "getProviderTokens"
|
||||
openapi: get /signin/provider/{provider}/callback/tokens
|
||||
sidebarTitle: /signin/provider/{provider}/callback/tokens
|
||||
---
|
||||
5
docs/reference/auth/post-token-provider-{provider}.mdx
Normal file
5
docs/reference/auth/post-token-provider-{provider}.mdx
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "refreshProviderToken"
|
||||
openapi: post /token/provider/{provider}
|
||||
sidebarTitle: /token/provider/{provider}
|
||||
---
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -468,6 +468,30 @@ This method may return different T based on the response code:
|
||||
|
||||
`Promise`<[`FetchResponse`](./fetch#fetchresponse)<[`JWKSet`](#jwkset)>>
|
||||
|
||||
#### 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`<[`FetchResponse`](./fetch#fetchresponse)<[`ProviderSession`](#providersession)>>
|
||||
|
||||
#### 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`<[`FetchResponse`](./fetch#fetchresponse)<[`ProviderSession`](#providersession)>>
|
||||
|
||||
#### 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
|
||||
|
||||
@@ -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
18
flake.lock
generated
@@ -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
2
go.mod
@@ -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
8
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ in
|
||||
name = "root";
|
||||
paths = with pkgs; [
|
||||
coreutils
|
||||
nix
|
||||
nixVersions.nix_2_28
|
||||
bash
|
||||
gnugrep
|
||||
gnumake
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(), ¶ms.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=",
|
||||
"H4sIAAAAAAAC/+x96XbbOLrgq+Do3jmVzBXlNemK58+osjqJY7etVO6ZqkwfiIQktEmABYB2VBm/+xxs",
|
||||
"JECCFCUvsV3VP7pikcTy4ds3fB/ENMspQUTwwcH3wQLBBDH1z1OUYIZi8ZHGUGBK5G8J4jHDuf5z8Pn0",
|
||||
"IxAUMPMiEHQwHDD0R4EZSgYHghVoOODxAmVQfjyjLINicDAoGB4MB2KZo8HBgAuGyXxwdXU1HOSQwQyJ",
|
||||
"2gIm9J8FYsvm/BPI5kgAuYwZZUAsULmWwXCA5St/qC+HAwIzORkrh+xc6TrToG8wy1M5+EKInB9sbWXL",
|
||||
"COb5KKbZVgxFvIjs23K44SowDAdneE4OyQmjFzhBLLCeBQJyP4DO1GI4jTFMQW4/MFvPoVhUO3eetu8b",
|
||||
"kSIbHPw2gLnc0XAwx2JRTOU/KJ2rX1JMzlGC5T4SzGPKksFwwHMq8EyCWVxiES/0lymUX06xmBbxOZKg",
|
||||
"uqTsnPLBcAD/LBiC8lNEBINYDSIYvIASPjBGU0rP5QeYJPSSp/gCmcEFYoOvIaBNsJykDVGwWUEIJ4R9",
|
||||
"2Bsf7AfV0V8ghmfL1xnE6cE3879B+zInyxw5S80ZiqGoJq5Nt8zLo9ZrHYFX5TdDQChIKZkjBgqOkrZN",
|
||||
"ypV0bKkxhzobjQ1IbutXtUP5q/zrJSUzzLKXC0jmalw8J5icQM4vKUtSxOUp5+bPU8SRkKdWwcsfMsAJ",
|
||||
"9EIVGxgLgbhQLOiNIZ0QRcDqNSD/izJEBDDEVm0mh/G5gpLIM4mJJGEUJ9E5Wjp/cThDYkkUFGY4oVGx",
|
||||
"O5OPDVkQSlAAC4eDcSEWiAisGebrbwIRjilR24BJguWvMD1hNEdMYMTDBz4u3wQVQwQSOxEXmMwBdF5g",
|
||||
"NEacy1+nS3V8cYrlxiFJAKyWQ1kFZzr9t2RdHet9qcY4LkReiDUXfwRziUjIjgWoHgXMGM2cBUr8cIb6",
|
||||
"LoGLk+bRjvM8NesDOJGLnWHEGuNXu5tSmiJI5PZihhK5XjX+fzI0GxwM/mOrEnhbBsu2XjKkhna3p3cv",
|
||||
"h1lkMH7JEBToDMUMBfDv3dH4JeDqYZ+VXXUfBWVjzuUiKDlFPKeEo/YzmMGUowYs3cFeQRGg+F8gR8/3",
|
||||
"C5YCRGKaoBq2gER+FUBxfXpyzPdnx5/6jGsQUg4I1DeBUSX/gKJgqNdCLXRA9VlgzIIj9g6SJO01qHwb",
|
||||
"LPTrwwEp0hRO5Zcax5uMvBIWv9VhMgzA393i15XHLwSMF5J/tbA676Rg+TbIaAJTLJYuv0uhkExwIMmB",
|
||||
"ch6VP6xgYGoZlqNuiofVCMd6qysI8fPpx9f6QPQJyTUFkXndQZpou+4IeTFNcfwBLa/18TidU4bFIguf",
|
||||
"rH4PnKMlgPZNh+252iMm4vl+hfeYCDRHTE4mGCQ8p0yz7lXo47w9HGCBMvVVAzPMD5AxuOxBAI2DX4n0",
|
||||
"Zyg1KvI1eJ1POV2n1EZw1c5OEVeQN0fuw/HLAokF0saAD8+s4ALESlwAaJhfJEeKmBnQPeW4lDyA04LF",
|
||||
"KCjImL+Urn05qz7V+7DbkhxOqVy4suc68bj2vjfcShE2sWi1Hg4CsYACxJCAKbL6rGVlBZcGBZnF0qxQ",
|
||||
"bJpnkIkohsoKWSynDGv9VyBGYBrkcC8puUBLSGJ0wtAMMURiIx9msEglYSntbrhCw4zLYUBejVOt1QyC",
|
||||
"SWknVgajXF7OMA/rkFrTOBlPTrXCtyY5oG85ZoiPA3B/LR/p5ScSO42+fzKeuHxFPooEzoIyNUMCJoYF",
|
||||
"d+mEpab/3Rog2TLKlSIuDzWaLvVPMM+jOMWDJjrVOEy1rRAjcWC2kZwKKZ6Hr3wAVcbLbrz3bPp8thfF",
|
||||
"+9MX0f7PaC968Y+fYZTsJ9uznWR/F+3uK/NHWqtyqN9/n/62Hb2A0ezr95+vfv99GpV/7l+1/tv9amdX",
|
||||
"fhY6kRwxLvc4jqUdMKHnKOCoucc7qJ2zIuDQnlqO3TDPG1WYN1W/2tXl0rI6RbxIBV9DOHXYZVfDIOLK",
|
||||
"JVZi5Sfu6w+NFTJ4eZhsotMwB9T9JW3joErS794HkC8BhnKGuOSgibZ3MQcGH3phlnGD6D07W2hg13Dw",
|
||||
"LZrTyPyYMypoTNNRF8Y5n0Q4s6Kvcr+pETRVLQYHxremvIRzGl2iqUQrslX+o/ziysN0xer+RvT7j+gB",
|
||||
"4+mBonoD5e4G00+s72tdFE/nbRBe5oLOGcwXOG6zrwLmlDmyfg6kiXy7fiDmLOTKuuVYw/20auv+LquB",
|
||||
"QAWTkEfKhxg7D6g/JJGIjDjAWgty8BNzAEFpzWjXaR8fVw1KwUO6gClO6sTAXYeGMp6UwzakPr9mjLLe",
|
||||
"zNGf/0xAkkCW4D9RApAcCLAK52t6tnwc0LHVV5KBWLRaYjLXYZocxdKYAtBxaephqt0ZMyRiNEWRNCWj",
|
||||
"KYowiWCa0kuUqN+5Dr7AaYqSCJEkp5gI9zdp6VlPfQRThmCylIMUah/+zypygZWdNaNsipMEkQgSSpYZ",
|
||||
"LbhjTUUcsQvEIrtiTNRRRXo46+t3HhiH9WA4SGkMUxQRKuw+nOhAJCiN+EIyEedHTKIFnuaRNDamUK27",
|
||||
"it3VRlKw8n/ieE6KPLIQkWYHsTu14JH/0Z95u9WL17ZKtZUZQ3wRCaWKVr+XoZIS9NkMRoKKXEUJ1L8i",
|
||||
"7Rd2v9LP1auShco1zGhBFNeWX9izgbHQkS/7pYprDIYDKhmnXk2EYhWEiWYQ653qhzmjM5yiaIZEvAg8",
|
||||
"VMHAxmHqlcWQyDVxRJKIZzxIZxniHM4DRPyuyCCJZgwjkqRLQ0b2bdcIOdRzAoVAVaCm6RsWUBQBV9a7",
|
||||
"yeQE6IdmFkl27hT729tNhl5jzWb0akNDQ9ohRn2YKGPEDc12spdGPFWHUUPwfP/lw5rc6v3Z8SfwBU3B",
|
||||
"B7RUgen3XybgwnXw9JKMpUdSuVvAJRYLrWxovl6d1+nZ7rPnoQMKIMHp2dh6udA3LSy9scb/HP8SGuo8",
|
||||
"pOfJ/R2+8r4/R8sIJ9FOcAyxDI9hpLC7o3FoABLeT0aTIlWoUo0Ap3Gys7u3PxqNWkIQ4aUUDWrgeL5S",
|
||||
"r5PnJ8Gt4aR3KperJwoh7PsvH86QuA5inSEVRNWIpWSZRLMymMEbSHaOlgFiHTMGl4DOHN+n52/uUq4k",
|
||||
"aazyQKvxQhD4iMm5IdvNHGo4aXGqjCVRg8NXwEqFJiLR0rnofvhJ/qypLSnkuwqcABMbzQ36eRye0wWs",
|
||||
"OouqA8rNEknaXStHb8YvFzBNEZmjE7hMKUzW1UXt5yDX3ys0yopU4GgGY2VRetZfA5OMdG3JvwCCShiC",
|
||||
"ywUiQAIiRcKqWkdvxiC283tkls3ghIr8AE7jnd29BM32Qzytrr7rhYTgdPyht65phcHxh6AAOFbb41XS",
|
||||
"05p4yrwPbz4rqbF1i2JniPM+YRv/EI+ltbxbJi0BrkcBMSUCYqLSHJT7T2UyGAVMk1qT58Au52d9JjOs",
|
||||
"Gkrh5PjkEMQwTX3WvoS7L0Zwezx79/zs6JdvLRy+w90+wRniAma5RlEVKXKnNp/6jtnt3f1oZzfa25ns",
|
||||
"7h08e3Hw7MX/6e2cNwMeBmDwqcimiEn+y1FMScJBQQROey1q79mLFyG72JxJT6h7J6jATqf2oAm69FbB",
|
||||
"wRM8s58mNrHFDvXUg9jO1tb2/MPP+njWC9u7WONCb7gi2nBig7lNF4kh4s3D5CsdDaHw1ZUXcNVJUi2x",
|
||||
"Xx2sYijpzJfqLZqbuVkNQV0L4Hth3t7etOqrq+Gg4uwbOPPQtzgtElQdXUhZASnmQlJL4KhfmVcVBkuU",
|
||||
"4JWrTiJzMLhr4pqQIUCoUMieC4mpUoop74rcEusL9s5lhY4Aealom3hkVSIUJiG8eid/lhtZoDQH8wIn",
|
||||
"SO1JpdSIBaPFfKF+QN9yJO1CFSnddKNqttAe82JqXlSewxYCSBCX9N/wMCkxIxYIa58PUqZ3zWXopCf3",
|
||||
"XH/ImxlYOstXh/VTqXyfQCaWr4nAQn0nJQEtRAiD5aOh1CkznKbYcP2hRsMK4QDm4FK+IBUnCi4hFmWO",
|
||||
"s3xD/mh0KxR0jipfSo9EArvmGgtmuXHEDFyyDpxlX691D958m97rbsLcJCi9Ln/bNPknlHjRTwSEsz4C",
|
||||
"WH5zjnSc9BXM726SZVkNnqO4YFgsTdawyfFJ0AVWr5lElJCSH1ihsUo31BzSlF72lGXXFmF3LrWuKegf",
|
||||
"v8hj+WFLXPT0RHkm/LgNX9AiTSSB85jmKNEFO81Uv3shVW4yVc3LVSyR6hoypUa0tylSTrXhZK3ttTxZ",
|
||||
"NSem/kqXbGlrrG6ltdjX1zH1tJmHEl0AkDN0gWnBm76fFpOu24TzFhYSCafOCzcGNUg8c3VNcJ164FFu",
|
||||
"QEHBHBHEdM5o3Rx+IKlbPc6iocNeP01vDAqC/yiQWxxieY2ZEKgZAVJTDsHlAscLwJHSeQynDHpvFfk2",
|
||||
"51uoAFcOU6hloSrFs1PqSVbCSo3dqse05PB6Waqq+q5gcK40taYAcMPzgDmjOLFmb4jSM+BWwoV0mM38",
|
||||
"fZJF14g+5PMz/h9pjClZion2SATDWV1ev/dfJo6vyZ2YzJXHz4Snff8bWr5fTN/G+Bi/P/z85+HOJ3zI",
|
||||
"D8nps/jl4fPD8/y/f335/kWLJ9BZzet2R5yTgytlqU0x9XxwmFgvnbu2F9vbvbL/u5nPxGM6JaeuLeH+",
|
||||
"pru6uwvpPodNLuCJo/u7s76W9Ao3ZhANa1jRAGOICRky3zD4oynanIKl8y5i5hVX6YKAZT7BiIQunB7b",
|
||||
"dI7N4n0J5nkKl58M96+w5T1dEHCWYVVV3Tg+neMS1JkvaRQvIIOxquM0L3pcR4Ijg98+IjKXCuLucJBh",
|
||||
"4vx1E3n4M8y40LtSWxkMByksf9H7Cqbht4BZFTuflCW+11StHAYtrSjJpHVeiJQETnJRLQlLvhLoS8AR",
|
||||
"+4nbAZKE6ZhqBfB/0wUZcbnl/00WlIsRpm6URQ8bCsLahbRN6ay0mu1MsG2p9nB++R+UJdGL/f/3P/wD",
|
||||
"f7btnfjeKvXBLrCc7mvfY9ooL85+pojZz/eqi3RlCWZwCTBRfnYAS+qnrBGX9U8zm60s7QuFpa+GN8g8",
|
||||
"HkOqAK1cSZ3QwHPyObcm7N2nGGiAH+mA/GYApyIPmKMEad3KIcaAj6Mrt8AT+v/X5gyM/ud/9k0VGKql",
|
||||
"te/6eHKiCHPDGq8w2xsDJ6HuRvjdZpgUZFarYaHbQdx3iGyIcTWY6DzLdSGzUcXHzfHGjWsSH1mNWt/y",
|
||||
"NAM1pynK3zQfAMpZxo9vWgBEHjkChmKEL3QqydnRWVC1W1CCdJpMADflQ0DKJBrr6vcA/l87u3v7z57/",
|
||||
"4+cXqzHImWyVqAiBaiNGcB/Uq9pmNjz0TfWbH3XE7Yf7xbj87zVPuFq5/OuI7io+1T82HKwidaDR3VDL",
|
||||
"g8+KXlo3ADsv+FRttg0pjgvhALIR7vW8wOEqJmkh0ELogAtMUxBTQlAspBGhYtS8pclE/6hFGc0rGENE",
|
||||
"WBuvP+58zm/Wf8DQHHOBmAmfKN+xKmXY3Ivw2nUflDsuR4dxTItaWcPdytwuf4QFbL91/0APRbWfDTIf",
|
||||
"UHKq6uFcP91vAxVaUbz7a/+ONkNLVXJE3/FnxEBjgH6eQgeIe7ueXfnb77/n3z9eyf//pP7/7AoMRz9F",
|
||||
"X//rP/9CHsbh3Weua7x7ELL3TvTxChZ3LcgbpeVXwwHB8Xk49vrJPCmZmk3E8muQbxh2K2T2hIr8rQne",
|
||||
"X9e/6gRCJ8eTE8CRKHI3bKJ2fvRm3JBhOINz9JmlrU16/3lqqpLlizoqE0OiplJyEpJ6u4c89/BXMoMD",
|
||||
"9fVWTub/a6oSrYb411+OTy+3P7ydt4RFBRV5W9tEs0fVNvHcFDFmkBQwNTvvt7LxLy9fvX7z9t3h+w9K",
|
||||
"PV/dH8ECy1te6HAbiWXtnQQj20lwiglkS9tAsSTw6VIEqzY+8x6FpIFYuinw1Y0+tUxfES0X+AIdzWC4",
|
||||
"9n6sE56P3ox1+wlLYkb4raisGA7gBRSQdWGgHe0nXq49x7Hp3hji+pbp66H5lvx4Z3dv9O98HuxAoppA",
|
||||
"JT1LcVxdCFxCblq2JfVynL1oeyfaeTbZ2T3Y2z949rx/OU5Nn/BX9Eo/VJhNGf5TUzejaQPya+shwcCU",
|
||||
"eQeYxJO+Uc27DrBVfYExSrq77hXuGhaQgylCBDjtDMrVeBjrGDyhdKLPrclEjeO4V9kDmJdh7y6wYa6a",
|
||||
"dxBQ9jxoNQoM52mCrU0PtSHQssJIvwiepJDMCyl1JH98ekd6aS0foeCCZsB+DCBXTdNFVfDePODNFdpO",
|
||||
"R5MFk+NvqvuYtnefPXu2vbO7t8JRuRahuBN200vryTNrbPmTfTQJ5uqxBC2eE51fFALrb2W9hzqT9ay0",
|
||||
"eupLKXRc9u+zXp8/1lnMUDdLcsmnxHAH2cJgtxAJag4csVdIkxn+E22oUmvHTXn5Qrdfy3VkXeI0BVME",
|
||||
"8JxQndPXl7ffFwumy7sxrlz7dAYyTHBWZGAPVEbwTbs3dEuZQ3KExIIGCU4lleI5iTCRfGZBE1OhXu+k",
|
||||
"7/bKyd2O+V9X6a3eErrCh6riSvXpV71hNkM+gi5f3zMUadbINxJr7aI7wXKGSOLWCzyiuNxqEK1Am00S",
|
||||
"szs10BUZ067+MQSYCESkFUVJqg1CM3ZQ6WkpfnF6ubuhnFLdb0vSvpOcb18atWSAy5M4msHrp9VJq071",
|
||||
"+mUgQeVf63ThWGE32ptDlOFYTTgCnzkCKMvFEmh4yKemTZZ8eeSwRdMQy78ixPzYNPNoEliGS83a0VHe",
|
||||
"N9FwHmgj0KxUrkw7VJwClH5uBLWSttNbN7LR4L0n90r6re7FwhBHugjMrm5o6w4S20NQp/RzXQPvZHwm",
|
||||
"fsKVd3HM77/3SrxyIbb6TDgSf7P7rqo5r9bELQ2p79spLVEMUR2x24WsZJMSjGWxl6H8cqF+/YlbmRIq",
|
||||
"QdFu6nGSnBkHsKmUuY9Oa31CMAVkPe/1el7oMERurBm6lKpVQ3SCLtMlgImU07VNeEwU7T97/o8I/fxi",
|
||||
"Gu3sJnsR3H/2PNrfff58Z3/nH/vb29tBEdwKSXXvmQWivfvMmR44TWP69Mhth+M1coDFqnIkQU0vtT6x",
|
||||
"KpV8o3d4JvFQz/ELggyxcSGZdcMfrZ7V07KVk0Ouwa3yUTI6aWTf28Io+SBnVOjMAdtnlI/sNWLKOaBm",
|
||||
"q3ayECKXYKxW+DpFF9pk7bdSlUBuDooDZL4GOWIZVjkG3CxbF5YQjpXfumQuvEpBN6O412WV6JIhyAs5",
|
||||
"Ay/iBYBcpYgRUVvNCLxRipOAOOWAIwSsdzqhMR9Zhr6VM5oUseBb8vMtu+jIWfRqoMmzxmRGjd0voL4t",
|
||||
"xwicAS/ynDLhChFTX/xJ/gLO9PPBcFCw1HGjl+9fNWt0spyhhQThBWqW5rELHCMbkoFzqSdp8a3YkET3",
|
||||
"oU374MP6dWNyCG0EK38KjpHhQ2bNR4cT8NH8Wl8xzRHRd6GMKJtvmY/51tHhRGsiIq227dfug/HJ4WA4",
|
||||
"uEBM56sNdkbbo20tThGBOR4cDPbUT7oWW1HT1ugSpWl0Tugl2fr35Tkf/Ztrh8s8pPGcIsEwutD1/o1W",
|
||||
"iU/ef/lw9tQN5DkND8u6O80Aap0UR2CywLwkNKknqfenS3OLjKJIpWeoOmG3YZYkypIEDpPBweAtEu+/",
|
||||
"fOBO52+12d3tbYtgRsw73Yi37MarawJX9GU8Q0JjbtdVRhxgAt5/+WB7SZo2UaV+cUPL8Ts/B1Y1Nu2W",
|
||||
"AY1V3lICLhcqClXdoafrBTXfV8y3yDLIlhqe3pZCDVcD+xwOBJxz5bxZcoGywVc5rGURZcm/SvWiPIBu",
|
||||
"b6u6cBukr0podEGOGqukT5/p+EhhmLEd6DaRY3XThMAJVT0judUnze50568HgTJGxAwOfvMl9W9fr766",
|
||||
"GGUOwxKyKlMmwPR+BsaJjok+VV0S9+bw1fEucI6vRC47aRi9tozG0YplL003jgrHSrhL/lM1f60Z1Cpo",
|
||||
"WbVI97FN61IhnFPQ+oUmyxs7ya581MC5fkHTsSalqjtKda0g8+rdSkDUeiv718Ve3SIt1UpxA/uxWpZU",
|
||||
"ZyQ2zYo0XT46itHHWqOCOjpqSqluCXB7uC1rLKWVgBYIpmLxZ6sSYFZi3BotuhPmlVoKU7Owt68nRjNq",
|
||||
"0Ms7NenLBYrP35oLiW8JoZyutYEjPKvWr+GwVOqds5eHJ741bEEsgQuevH09eRoSzUN1//lNHve71+NX",
|
||||
"Pc77nb4NO3Tgf7WzkRB72qY3pZicb+GkNLLD0uwjJuf1kzJXvP7Eq6Qfk1yGvukLLnQzJK9PsHpPHyQk",
|
||||
"Zf3uCJx22qeNg3Z6kN+S+At0OQ+ckt2AToe3+6ynF9p9S1hjMr9TSdfNmHRzD7EE+hb6xyTsKjdJXegp",
|
||||
"ZIa1fDWbs2IP0UHrEl1xUpWbh6VcNoNbgop8yzafapV3jhUywRmKplBapscERfJPUFYbPJkcT06e2pxO",
|
||||
"7aIR2jbJV0SdfJLRgWsTBrtNURhM3w2ZtE6uqot3ZeOu5NGpW+WhO3t3kElXWChEyvWl/CstWIIuwYmp",
|
||||
"ngW6fBZMyoKinEktLZOsKFYtmrRlNAIn4wlXvZNTSuZRqio5TaOoetdSgAkXCKrIGEPzIoUND6PpB0Uz",
|
||||
"rTAr8cLX5unllaO3xNEb18AGDj8MytiEJ+yhmzqoqvGU5P5OZtPdcffmNa0h7dMlLpPUBWAYbR4t039p",
|
||||
"748OH/GTk7GvwPp8neM5wWQLuhmhLXa/maeREGqbtkjcoYUwgkbuzekIOwJj7ytuKVHdjsyEbTKmyVC/",
|
||||
"kUKBGLjAUAEpqTLkSsdnk9ZqfZ1u1YXQ6B4VwIAypFe4ifgNPaqEjUkLG/xYH4FHWKV76eGZC2emA04J",
|
||||
"33TpkEJNr/AIopZ+10oVYzccBp1iUt0fvllSKqWHKBjhbrsjFcJrdD1SRuQlDatBQPn/1U1ybVTglc3e",
|
||||
"KiUEC3QDh6mqUdw+0WWXKA9GTY3vDh1oHa2wQuhZM/BLwhmBQ53pVJ3TEEDncG3aLVPo4KsjJWqMHi7N",
|
||||
"tZdU96G/lYb7uNkFzrG7TUE7MKFQaYD4FvsT3WeLMvBWXbT3dAS0gONuWXhZLDUDlCCQUMTJTwKgb5i3",
|
||||
"yp7btd6Drcc2t99/IJ395YSQdSK5vd16UIK1vHtEZOQaOixnPzzTyzwPSSs8s5FlI4YcptcS2PH6t90q",
|
||||
"ZdR6xAXOUmWf6lRESRvKYNVZoN13zt3nmI7ckxddro5kWLVONlWLD46AjEtdnVRV1NyHdqjIt8o0zDDx",
|
||||
"HBIssJQhWuPT5EAbbaHqyT5niCSqOk4uypQvmVubUeJXOypNoExp9WTIsGrhbpx1WiewtmzdeZcAk+vZ",
|
||||
"JnxsG7hbpbF6R8JQkNFriKLDDq4hZLahjlNC8AcKom4/snZnEXuFZFVG+iAdyl1K2vHkZF2q6p8rUE7R",
|
||||
"KZJU6m+d9G5UBN0pfaxMLShFjyQQ1Ogh9OMETldLyZD4gXMcq0jLY6QVI37WpRK3Xq+3EHI/CpAKRyTR",
|
||||
"ultWgdzvl2A6ld2dyGk0rbxV2mptkXlNIeTA897KIofMDF2pfmqPRxBltQ1uQmk84zdJZ01NsNaexCmV",
|
||||
"/1FUd5bxO6M5pxNnKMrkNg7oJrizo7MHp/l5fREeEd2Zs9iQ3Lb6OSc8kpMz/lhN8AcS0HGnf6JBQ56D",
|
||||
"QgLuB+uFHV2G21z+XhaEX835UNXBdWlGrOfKDkdz18tvaCRKlHUganvjk8NW6XJruQqN9vC9cxX+9lf/",
|
||||
"CNHQL6mgE/VN8GHru/3XVWvuWKmfmXsi6+k4Kb00bnSgmjKlZWhDygZd18yd21VpI/PN66GWwzlqJYHq",
|
||||
"ZhLnLu+D38InU72yVfv8atggd8bgUkX5dF9a0wyp0TwsT1X7AlM6iuWnfxSILatyPq+x7dBBnut3uOVi",
|
||||
"qUrnZpRlgT3YnnShLnShlfpdlgILbelYF5jZ6U7Xa2avh0Zo5ttowRs49dbWu6FVlw9DC163E5pcTG++",
|
||||
"c7tXhDU5VVVwW3Zc89q9PFF1k7Zhpt7S0xaolZlqTfB/Pj3UCUaaSejrjENjOB2Gw9C/oVbDDdY3AxyJ",
|
||||
"oW68lyFoBbrbC8MWfPm56lJHvISm5jM1+ey4DF6bWlGCUKLemCIATQuKRpV3C0xMbzEPICs3dJzDPwoE",
|
||||
"uJD8/AKmBTKzlwkP06XHnlsmVwN0Tv21Jvb3tndDtbjlyddlx0DXcij+/n3wkXZfJG1e3bIDlu8b7H7I",
|
||||
"oena9cybS/itGKbpFMbnraL+neq5xMuLvuXLOmujtggO4Ewg0y3Ek98jcKI3aYbxhXvpWI/LvA43XXGV",
|
||||
"3H9p1vTWXPF1sypAc6Wl48dQRU31WUEkqr/QWuRZJohcb2Kc/MsmM6wx+ZnQTjgDMdVX4oLiBLw8O30D",
|
||||
"oBAwPucr+IGv+q/HmlQukH89RZUZhEbz0RD8d5uQoRJAm2xaz2q6aLJNJ7bfrze3YiMgQ5xDnVdY16wh",
|
||||
"TnWLy8DEir+sN98r1QcDJYY3OQ83mvxf7uhrLUTKfe1bpszPu4VTWmjxavfXPr2W4TcogXSXBuDIBcPl",
|
||||
"Kk9J09j9iwqpmjyopIXNAm8XU1JlVXeLZkiVbIb9L1YUtU3EjVNG4s6/5BBV1XdGE2SazEyXjshK8TkC",
|
||||
"OsVQaXEckUS1eVfp7CfHZxM33VPhXMUOeV/ZdCK3c13h9LWvo+dbdHl5GUkgRAVLjUbe33iot6sNtea7",
|
||||
"llxc2Xde0/nBxryx3wQerzq4GcbYc2bJpg6uwwFXzlOK+4Mb0CdWzqZF/cGm2kPrvem1flvSwDSNJ51m",
|
||||
"PNrocsD1RPUZrQJ0BCgjONg/S5H+09V7rHftVRv+2sNubmWKisnoG5WVI7lBUcMyAVW3YgrrU1d/S7Qf",
|
||||
"KNHAk1LWPO0r3XoaZFs6WtBql43rhxZ0xQ6lFV/1tip3UuaaVnTkOhjkZ87dcBoBnXLDen66n1VZ+Xon",
|
||||
"FCDCC6Zn1yivgu1cQCVy3eZYWcGFirXDVDJenGUowVCgdGkwVI6hlfPyIAQFdCp3oB6axllgoroImsAj",
|
||||
"B3GKoDz5sn2sXMgUcmQv4FbrkGMOAaeAF1Mu0YEI9RvXSQCSx5elc3OqP2O0mOt8ANsmXLm+4RxiMgKH",
|
||||
"quGXk4RgKBVPcYrFUnk4BDXAsevlcCZ3bIwMTMCU0UvJ4pSrUX0A5+hpsDuYVRYmGnVuROO4rV5SZpbq",
|
||||
"gtHuOI1F46SBw4+uRrvsRldnPyZ+qNC49Nv0c/2sbkpWhnScpmQWqc2u/IybqkLKz7TR91FqIlSyWdH1",
|
||||
"Al5I6KALrKr7yssdVSFw1QWtLDlrU63vtOdUn3pRPx1UULBAaW4vm1lWbmHJJssWVbWjurrfTdseeGg0",
|
||||
"0FutD6n0T5qW6+iiGT9dpupEZsVRjXBuMHnmYXZoq8HFb/auoXTfUgoMvlXn8mDzZeqI3IdqaNGRNfOa",
|
||||
"JN69wl4vgukSYGJxm8x9tZOPgGWzJlPDvwPZ3HwcFhXHhbhFrHdudG5Bh0gu1SZSVnaet0EdErT3OtH2",
|
||||
"Ld6jdMtQ9ow8/0eghQ2/+4qYvXXb68qhdc4K94u8dw+C08CF1l77p35dCPDMvOj3qdVXMFg0GQIqseoS",
|
||||
"cxvI5YAUadodVqtd4X2LBNRyWXhbYqJWFwPWZ0VZCiZD56YKL6/ZdvS48wY5q4XHqbu3WmOC8EFXh1z1",
|
||||
"dNF56vKIH2ovAns57Ga9CIr8OkZOkd+EkSMJUBk6qgUh5sL4VVR6RwvB3YGK1rwX+5pFOKUi55LlnVJV",
|
||||
"wKqxV3g8drPGksk6Zo1DHNcxa3wq+VFmzR3TzEZmjdOwzYClSVGBJlP30awp8sdn1hR5l7feq9vQRLSi",
|
||||
"yU2tH2I9X68sFtDpfJ4NoGWIbxaoqoCq79AFPTfpg3p4SqquRJjzItRZ6lQPeJstbtwpOsjj1NuaoAB9",
|
||||
"i1UnUF0pYMvtSmD9CBoIYp17gNz3RasdPcSiGHsYNYCHzRv1tK0uoM3EMYe9aLqv+1NEJSokcjSCVkr8",
|
||||
"FXmis2rbw1bgCZ4BRoV6z0n7eNoVz6pf2aJy9znIIC4jZU5JT1WpU65yfHLIy0CRxmR7JrbYgaHITQ1u",
|
||||
"pV4vnnN3CSQbsQFvrV1FQxZO7D7zhfXDU4Yl1KM1jzA6pY8tHJxapYxqlrJKBTUyW65R8QcsdB1O4Lqz",
|
||||
"wxkgtGyLO6XJUiqSNvljqAvovNwKnaBQCtCyAs+V+m3q522K08A1dV1xp8Dtc4rfSRu02v7KrT+9dvCp",
|
||||
"qrg4/hAooGjs4NeypEE8gn6/Qcfhr9VdYN0S1iY6rb6VLHjtQahNreNqpiniw7Jcx1zoZvyNXEBRhO8Y",
|
||||
"+6yLtG6Nt6rx2xx9rjH0+Jqvi0a2Wrj5uvznltNIuctaV22ZA+2eBQXQa9QsVSCYVL5SzyRNl1XnDL8d",
|
||||
"NIgpmWGLXvpLy0FU3wVsr+1kF7r8SX0wL5juFZ1QwGkT0V5VuysR7uaZqhzamamDsY5DNx3aupSyD66g",
|
||||
"thF2ANwusO9RxEbRlYNMj/xGKee4m1SxSj9RhKd7pmlltMvUKa9kN3prK5+utRms9YAMkJRxNisVuMos",
|
||||
"MYSoo6lqyvWvOCjv4LjNFlDl+Hq6Dqr7hC5rGTSen6wsA/18+tHpQW2O5v6Q2GtnWRZrUTIC4+p0VTG8",
|
||||
"d+6qBHQBOZgiRNrO/fFeh6CBpXhnvYlUXRJqguSIJJELwWhFt7YzJBUeEgqiuQ3ZWtuBfvavdvWemkYA",
|
||||
"Tq8oDmr4GUpMQCRxr2e/EyIMTrpZJKhBj01Gdu+oMsBrH0M/NonbQei3EVE2gx2tbWKB1c2lKsMK2r+6",
|
||||
"2izbvO+mxGsxnpv3P90O0h/NYAd2q+7EHuDKAuSjN2Ngtq4vAVTXXN2n5Bu5Qm25+R5p4wx9dLrcESRw",
|
||||
"jlbcMtZyY45C+tVJOS8r3c1a1WXujR/dn9ZxXWcrBPQuoBs6XeCk6uFQWlcMcSRMZ/MuBe2Wk3DcKVYo",
|
||||
"aFVzbS+jJrShH939qZt8yqb5WkA/3tsG6w4pV9kKJNXURYV9ZUsd7WoTCNaRQXvw3QS3PrpwyAQKWVDa",
|
||||
"FnKyFGqzG+CF1S/nshgkblv/8ia7IZXL3+x9pC59CqUZ9BCjpBqvG6BuI5gyuQYmHaLmTEAmbB2eCmLQ",
|
||||
"mfXI6XhXmUpiiRyco2WNFEqCqqKkTpbaAjqFdIZGyhLfjoT6tT0J4yQ5M4v8gJaDe5zrZXv3SuKxZUcV",
|
||||
"yF1IP1pjW+c9psolpvFNhdPk/i/LxBgXEN1a1Wa5ZOshfSPBzMdai21r4622RQLYe1sRPX+qm80j8yBG",
|
||||
"cHxOdPu8uxMJ4T12XUTvrNhvN5skD96CaadAExK8JvVV9BaMGNq4uVLFecn4psuAC2zY3kp/CBpqhoUF",
|
||||
"4l0NPGGeM5ozlWGdIC4w0chb5F5me79Qu9rE2kkv+rN/quZAV8Oer0+WOer9yWnZeNB8sl5TIfvqX7er",
|
||||
"gndPh4pJ+44lg74OFQQSMi8Q4wZe3cFz82JLQ5Xa1BwxU13YiIr/aia8Ji8Nd94xvWP91jvOFmsurEW1",
|
||||
"LTrr3keVnbEz2h3tDVZ1NrGT9ult8msAtDxYtP/wTIC3SFgoWli7bFndMC5RUckCdmG5U815sqBcgFpk",
|
||||
"eXxyCM7UJ4PhoGCp06r0Oy+mCc0gJlcjeaKj71JfpeRqRORII1aQrYsdxXHMSr6HYr01ZChR2a1DNNWd",
|
||||
"Q5sPrbNDLiDDtGjctKGD4Rw80YGYqsTKvStgqFPChqU+NwRHb8ZPnS7I9aL37y26QcRQqgRXcOXBTte8",
|
||||
"mhZkymmYISKGZbqL1g2VbHOzYKTwk1RQNS208ja0Op19Wg0fXp9OyjIJgcOazFWNfEx1qDur7WQRPE+b",
|
||||
"5uOufNUq/JOyEamhvrjY5Gc4a7JTaEcyr5amLN0AMBQBhKdeIJiKBYgXKD7nwzoVmfmUTaeUQJtp60xq",
|
||||
"yCvQHrGUGcbd6DmRnNVIW8+Ey8seUTDWxmI5jSdWmpNNFogjd1DIkEqvw5J7JTo2aDMItVhOld2iWyxp",
|
||||
"Pw5f0CJN5GumB5BpQ2PaMJ29+uAsqGoTdPX16v8HAAD//7oaO/PHBQEA",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification 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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
62
services/auth/go/controller/get_provider_tokens.go
Normal file
62
services/auth/go/controller/get_provider_tokens.go
Normal 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
|
||||
}
|
||||
245
services/auth/go/controller/get_provider_tokens_test.go
Normal file
245
services/auth/go/controller/get_provider_tokens_test.go
Normal 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),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
40
services/auth/go/controller/refresh_provider_token.go
Normal file
40
services/auth/go/controller/refresh_provider_token.go
Normal 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
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user