From cb8280249f7a2deeadb33a54131ce1215bb61759 Mon Sep 17 00:00:00 2001 From: Kessler Dev Date: Wed, 8 Apr 2026 12:45:16 +0200 Subject: [PATCH 01/87] chore(chart): use appVersion as default image tag --- chart/Chart.yaml | 2 +- chart/templates/deployment.yaml | 2 +- chart/values.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 886ba48f..28fcae00 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -2,4 +2,4 @@ apiVersion: v2 name: trek version: 0.1.0 description: Minimal Helm chart for TREK app -appVersion: "latest" +appVersion: "2.9.11" diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 0ab074ba..d79ae344 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -27,7 +27,7 @@ spec: fsGroup: 1000 containers: - name: trek - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- with .Values.resources }} resources: diff --git a/chart/values.yaml b/chart/values.yaml index 47a941c7..464e6d8c 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -1,7 +1,7 @@ image: repository: mauriceboe/trek - tag: latest + # tag: latest pullPolicy: IfNotPresent # Optional image pull secrets for private registries From dba655d6e8bfc54a50bfb4ff5a8b99dbf26f1889 Mon Sep 17 00:00:00 2001 From: Kessler Dev Date: Wed, 8 Apr 2026 13:01:14 +0200 Subject: [PATCH 02/87] chore: implement helm chart release automation to gh-pages --- .github/workflows/docker.yml | 6 +++-- .github/workflows/helm-release.yml | 25 +++++++++++++++++++ {chart => charts}/README.md | 0 {chart => charts/trek}/Chart.yaml | 0 {chart => charts/trek}/templates/NOTES.txt | 0 {chart => charts/trek}/templates/_helpers.tpl | 0 .../trek}/templates/configmap.yaml | 0 .../trek}/templates/deployment.yaml | 0 {chart => charts/trek}/templates/ingress.yaml | 0 {chart => charts/trek}/templates/pvc.yaml | 0 {chart => charts/trek}/templates/secret.yaml | 0 {chart => charts/trek}/templates/service.yaml | 0 {chart => charts/trek}/values.yaml | 0 13 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/helm-release.yml rename {chart => charts}/README.md (100%) rename {chart => charts/trek}/Chart.yaml (100%) rename {chart => charts/trek}/templates/NOTES.txt (100%) rename {chart => charts/trek}/templates/_helpers.tpl (100%) rename {chart => charts/trek}/templates/configmap.yaml (100%) rename {chart => charts/trek}/templates/deployment.yaml (100%) rename {chart => charts/trek}/templates/ingress.yaml (100%) rename {chart => charts/trek}/templates/pvc.yaml (100%) rename {chart => charts/trek}/templates/secret.yaml (100%) rename {chart => charts/trek}/templates/service.yaml (100%) rename {chart => charts/trek}/values.yaml (100%) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0a7c8f38..40af54fc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -54,14 +54,16 @@ jobs: echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT echo "$CURRENT → $NEW_VERSION ($BUMP)" - # Update both package.json files + # Update package.json files and Helm chart cd server && npm version "$NEW_VERSION" --no-git-tag-version && cd .. cd client && npm version "$NEW_VERSION" --no-git-tag-version && cd .. + sed -i "s/^version: .*/version: $NEW_VERSION/" charts/trek/Chart.yaml + sed -i "s/^appVersion: .*/appVersion: \"$NEW_VERSION\"/" charts/trek/Chart.yaml # Commit and tag git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add server/package.json server/package-lock.json client/package.json client/package-lock.json + git add server/package.json server/package-lock.json client/package.json client/package-lock.json charts/trek/Chart.yaml git commit -m "chore: bump version to $NEW_VERSION [skip ci]" git tag "v$NEW_VERSION" git push origin main --follow-tags diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml new file mode 100644 index 00000000..d3f93c86 --- /dev/null +++ b/.github/workflows/helm-release.yml @@ -0,0 +1,25 @@ +name: Release Helm Chart + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Helm Chart Releaser + uses: helm/chart-releaser-action@v1.7.0 + with: + config: | + pages_branch: gh-pages + charts_dir: charts diff --git a/chart/README.md b/charts/README.md similarity index 100% rename from chart/README.md rename to charts/README.md diff --git a/chart/Chart.yaml b/charts/trek/Chart.yaml similarity index 100% rename from chart/Chart.yaml rename to charts/trek/Chart.yaml diff --git a/chart/templates/NOTES.txt b/charts/trek/templates/NOTES.txt similarity index 100% rename from chart/templates/NOTES.txt rename to charts/trek/templates/NOTES.txt diff --git a/chart/templates/_helpers.tpl b/charts/trek/templates/_helpers.tpl similarity index 100% rename from chart/templates/_helpers.tpl rename to charts/trek/templates/_helpers.tpl diff --git a/chart/templates/configmap.yaml b/charts/trek/templates/configmap.yaml similarity index 100% rename from chart/templates/configmap.yaml rename to charts/trek/templates/configmap.yaml diff --git a/chart/templates/deployment.yaml b/charts/trek/templates/deployment.yaml similarity index 100% rename from chart/templates/deployment.yaml rename to charts/trek/templates/deployment.yaml diff --git a/chart/templates/ingress.yaml b/charts/trek/templates/ingress.yaml similarity index 100% rename from chart/templates/ingress.yaml rename to charts/trek/templates/ingress.yaml diff --git a/chart/templates/pvc.yaml b/charts/trek/templates/pvc.yaml similarity index 100% rename from chart/templates/pvc.yaml rename to charts/trek/templates/pvc.yaml diff --git a/chart/templates/secret.yaml b/charts/trek/templates/secret.yaml similarity index 100% rename from chart/templates/secret.yaml rename to charts/trek/templates/secret.yaml diff --git a/chart/templates/service.yaml b/charts/trek/templates/service.yaml similarity index 100% rename from chart/templates/service.yaml rename to charts/trek/templates/service.yaml diff --git a/chart/values.yaml b/charts/trek/values.yaml similarity index 100% rename from chart/values.yaml rename to charts/trek/values.yaml From 8c7d1f8fa69bbc01f8618e11a008d1ad2b8c329e Mon Sep 17 00:00:00 2001 From: Kessler Dev Date: Wed, 8 Apr 2026 13:28:22 +0200 Subject: [PATCH 03/87] chore: use helm-publisher action for chart release --- .github/workflows/docker.yml | 15 +++++++++++++++ .github/workflows/helm-release.yml | 25 ------------------------- 2 files changed, 15 insertions(+), 25 deletions(-) delete mode 100644 .github/workflows/helm-release.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 40af54fc..9cc0e762 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -153,3 +153,18 @@ jobs: - name: Inspect manifest run: docker buildx imagetools inspect mauriceboe/trek:latest + + release-helm: + runs-on: ubuntu-latest + needs: version-bump + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: main + + - name: Publish Helm chart + uses: stefanprodan/helm-gh-pages@v1.7.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + charts_dir: charts diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml deleted file mode 100644 index d3f93c86..00000000 --- a/.github/workflows/helm-release.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Release Helm Chart - -on: - push: - tags: - - 'v*' - -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Helm Chart Releaser - uses: helm/chart-releaser-action@v1.7.0 - with: - config: | - pages_branch: gh-pages - charts_dir: charts From 830f6c070624a2749dc369af4774209c67b11cfc Mon Sep 17 00:00:00 2001 From: jubnl Date: Thu, 9 Apr 2026 22:25:58 +0200 Subject: [PATCH 04/87] feat(mcp): introduce OAuth 2.1 auth and enforce addon gating OAuth 2.1 authentication for MCP: - Add OAuth 2.1 authorization server with PKCE support (routes/oauth.ts) - Add OAuth service for client CRUD, auth-code flow, and token management (services/oauthService.ts) - Add typed scope definitions and enforcement helpers (mcp/scopes.ts) - Add OAuth consent UI page (OAuthAuthorizePage.tsx) - Add client-side scope labels and descriptions (api/oauthScopes.ts) - Integrate OAuth token auth into MCP handler alongside existing static tokens - All OAuth endpoints gated on `mcp` addon Addon gating across MCP tools, resources, and prompts: - Add typed ADDON_IDS constant (server/src/addons.ts) replacing all string literals - Gate budget tools and resources (trip-budget, per-person, settlement) on `budget` addon - Gate packing tools and resources (trip-packing, trip-packing-bags, trip-todos) on `packing` addon - Gate todos tools on `packing` addon (mirrors web UI Lists tab behavior) - Expand atlas gate to cover full tool body (bucket-list + country tools no longer leak) - Expand collab gate to cover full tool body (collab notes no longer leak) - Gate packing-list and budget-overview MCP prompts on their respective addons - Gate get_trip_summary sections per addon; blank packing/budget/collab_notes/todos when disabled - Remove trip-files resource and files field from get_trip_summary - Replace all isAddonEnabled('literal') calls with ADDON_IDS constants Co-Authored-By: Claude Sonnet 4.6 --- client/package-lock.json | 860 +++++++++--------- client/src/App.tsx | 3 + client/src/api/client.ts | 37 + client/src/api/oauthScopes.ts | 43 + .../components/Settings/IntegrationsTab.tsx | 582 +++++++++++- client/src/i18n/translations/en.ts | 41 + client/src/pages/OAuthAuthorizePage.tsx | 248 +++++ server/package-lock.json | 24 +- server/src/addons.ts | 11 + server/src/app.ts | 6 + server/src/db/migrations.ts | 42 + server/src/mcp/index.ts | 50 +- server/src/mcp/resources.ts | 39 +- server/src/mcp/scopes.ts | 89 ++ server/src/mcp/tools.ts | 32 +- server/src/mcp/tools/assignments.ts | 20 +- server/src/mcp/tools/atlas.ts | 31 +- server/src/mcp/tools/budget.ts | 19 +- server/src/mcp/tools/collab.ts | 37 +- server/src/mcp/tools/days.ts | 5 +- server/src/mcp/tools/mapsWeather.ts | 5 +- server/src/mcp/tools/notifications.ts | 16 +- server/src/mcp/tools/packing.ts | 40 +- server/src/mcp/tools/places.ts | 18 +- server/src/mcp/tools/prompts.ts | 28 +- server/src/mcp/tools/reservations.ts | 5 +- server/src/mcp/tools/tags.ts | 14 +- server/src/mcp/tools/todos.ts | 26 +- server/src/mcp/tools/trips.ts | 67 +- server/src/mcp/tools/vacay.ts | 53 +- server/src/routes/oauth.ts | 296 ++++++ server/src/services/oauthService.ts | 471 ++++++++++ 32 files changed, 2589 insertions(+), 669 deletions(-) create mode 100644 client/src/api/oauthScopes.ts create mode 100644 client/src/pages/OAuthAuthorizePage.tsx create mode 100644 server/src/addons.ts create mode 100644 server/src/mcp/scopes.ts create mode 100644 server/src/routes/oauth.ts create mode 100644 server/src/services/oauthService.ts diff --git a/client/package-lock.json b/client/package-lock.json index f1ac488c..78ad94bf 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1877,6 +1877,397 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@exodus/bytes": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", @@ -5373,6 +5764,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -11133,436 +11563,6 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, "node_modules/vitest": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", diff --git a/client/src/App.tsx b/client/src/App.tsx index 0ca00b63..06640508 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -12,6 +12,7 @@ import VacayPage from './pages/VacayPage' import AtlasPage from './pages/AtlasPage' import SharedTripPage from './pages/SharedTripPage' import InAppNotificationsPage from './pages/InAppNotificationsPage.tsx' +import OAuthAuthorizePage from './pages/OAuthAuthorizePage' import { ToastContainer } from './components/shared/Toast' import { TranslationProvider, useTranslation } from './i18n' import { authApi } from './api/client' @@ -163,6 +164,8 @@ export default function App() { } /> } /> } /> + {/* OAuth 2.1 consent page — intentionally outside ProtectedRoute */} + } /> apiClient.get('/oauth/authorize/validate', { params }).then(r => r.data), + + /** Submit user consent (approve or deny) */ + authorize: (body: { + client_id: string + redirect_uri: string + scope: string + state?: string + code_challenge: string + code_challenge_method: string + approved: boolean + }) => apiClient.post('/oauth/authorize', body).then(r => r.data), + + clients: { + list: () => apiClient.get('/oauth/clients').then(r => r.data), + create: (data: { name: string; redirect_uris: string[]; allowed_scopes: string[] }) => + apiClient.post('/oauth/clients', data).then(r => r.data), + rotate: (id: string) => apiClient.post(`/oauth/clients/${id}/rotate`).then(r => r.data), + delete: (id: string) => apiClient.delete(`/oauth/clients/${id}`).then(r => r.data), + }, + + sessions: { + list: () => apiClient.get('/oauth/sessions').then(r => r.data), + revoke: (id: number) => apiClient.delete(`/oauth/sessions/${id}`).then(r => r.data), + }, +} + export const tripsApi = { list: (params?: Record) => apiClient.get('/trips', { params }).then(r => r.data), create: (data: Record) => apiClient.post('/trips', data).then(r => r.data), diff --git a/client/src/api/oauthScopes.ts b/client/src/api/oauthScopes.ts new file mode 100644 index 00000000..aacd992a --- /dev/null +++ b/client/src/api/oauthScopes.ts @@ -0,0 +1,43 @@ +// Human-readable scope definitions for the OAuth consent page. +// Must stay in sync with server/src/mcp/scopes.ts + +export interface ScopeInfo { + label: string + description: string + group: string +} + +export const SCOPE_GROUPS: Record = { + 'trips:read': { label: 'View trips & itineraries', description: 'Read trips, days, day notes, members, and share links', group: 'Trips' }, + 'trips:write': { label: 'Edit trips & itineraries', description: 'Create and update trips, days, notes, and manage members', group: 'Trips' }, + 'trips:delete': { label: 'Delete trips', description: 'Permanently delete entire trips — this action is irreversible', group: 'Trips' }, + 'places:read': { label: 'View places & map data', description: 'Read places, day assignments, tags, categories, and visited countries', group: 'Places' }, + 'places:write': { label: 'Manage places', description: 'Create, update, and delete places, assignments, tags, and atlas entries', group: 'Places' }, + 'packing:read': { label: 'View packing lists', description: 'Read packing items, bags, and category assignees', group: 'Packing' }, + 'packing:write': { label: 'Manage packing lists', description: 'Add, update, delete, toggle, and reorder packing items and bags', group: 'Packing' }, + 'budget:read': { label: 'View budget', description: 'Read budget items and expense breakdown', group: 'Budget' }, + 'budget:write': { label: 'Manage budget', description: 'Create, update, and delete budget items', group: 'Budget' }, + 'reservations:read': { label: 'View reservations', description: 'Read reservations and accommodation details', group: 'Reservations' }, + 'reservations:write': { label: 'Manage reservations', description: 'Create, update, delete, and reorder reservations', group: 'Reservations' }, + 'collab:read': { label: 'View collaboration', description: 'Read collab notes, polls, messages, and to-do items', group: 'Collaboration' }, + 'collab:write': { label: 'Manage collaboration', description: 'Create, update, and delete collab notes, todos, polls, and messages', group: 'Collaboration' }, + 'notifications:read': { label: 'View notifications', description: 'Read in-app notifications and unread counts', group: 'Notifications' }, + 'notifications:write': { label: 'Manage notifications', description: 'Mark notifications as read and respond to them', group: 'Notifications' }, + 'vacay:read': { label: 'View vacation plans', description: 'Read vacation planning data, entries, and stats', group: 'Vacation' }, + 'vacay:write': { label: 'Manage vacation plans', description: 'Create and manage vacation entries, holidays, and team plans', group: 'Vacation' }, + 'media:read': { label: 'Maps & weather data', description: 'Search locations, resolve map URLs, and fetch weather forecasts', group: 'Media' }, +} + +export const ALL_SCOPES = Object.keys(SCOPE_GROUPS) + +// Group all scopes for the client registration form +export const SCOPE_GROUP_NAMES = [...new Set(Object.values(SCOPE_GROUPS).map(s => s.group))] + +export function getScopesByGroup(): Record> { + const groups: Record> = {} + for (const [scope, info] of Object.entries(SCOPE_GROUPS)) { + if (!groups[info.group]) groups[info.group] = [] + groups[info.group].push({ scope, ...info }) + } + return groups +} diff --git a/client/src/components/Settings/IntegrationsTab.tsx b/client/src/components/Settings/IntegrationsTab.tsx index f56dbcdf..afd2c40c 100644 --- a/client/src/components/Settings/IntegrationsTab.tsx +++ b/client/src/components/Settings/IntegrationsTab.tsx @@ -2,11 +2,85 @@ import Section from './Section' import React, { useEffect, useState } from 'react' import { useTranslation } from '../../i18n' import { useToast } from '../shared/Toast' -import { Trash2, Copy, Terminal, Plus, Check } from 'lucide-react' -import { authApi } from '../../api/client' +import { Trash2, Copy, Terminal, Plus, Check, KeyRound, ShieldCheck, ChevronDown, ChevronRight, RefreshCw } from 'lucide-react' +import { authApi, oauthApi } from '../../api/client' import { useAddonStore } from '../../store/addonStore' import PhotoProvidersSection from './PhotoProvidersSection' +import { getScopesByGroup, ALL_SCOPES } from '../../api/oauthScopes' +interface OAuthPreset { + id: string + label: string + name: string + uris: string + scopes: string[] +} + +const OAUTH_PRESETS: OAuthPreset[] = [ + { + id: 'claude-web', + label: 'Claude.ai', + name: 'Claude.ai', + uris: 'https://claude.ai/api/mcp/auth_callback', + scopes: ALL_SCOPES.filter(s => !s.includes(':delete')), + }, + { + id: 'claude-desktop', + label: 'Claude Desktop', + name: 'Claude Desktop', + uris: 'http://localhost', + scopes: ALL_SCOPES.filter(s => !s.includes(':delete')), + }, + { + id: 'cursor', + label: 'Cursor', + name: 'Cursor', + uris: 'http://localhost', + scopes: ALL_SCOPES.filter(s => !s.includes(':delete')), + }, + { + id: 'vscode', + label: 'VS Code', + name: 'VS Code / Copilot', + uris: 'http://localhost', + scopes: ALL_SCOPES.filter(s => s.endsWith(':read')), + }, + { + id: 'windsurf', + label: 'Windsurf', + name: 'Windsurf', + uris: 'http://localhost', + scopes: ALL_SCOPES.filter(s => !s.includes(':delete')), + }, + { + id: 'zed', + label: 'Zed', + name: 'Zed', + uris: 'http://localhost', + scopes: ALL_SCOPES.filter(s => !s.includes(':delete')), + }, +] + + +interface OAuthClient { + id: string + name: string + client_id: string + redirect_uris: string[] + allowed_scopes: string[] + created_at: string + client_secret?: string // only present on create +} + +interface OAuthSession { + id: number + client_id: string + client_name: string + scopes: string[] + access_token_expires_at: string + refresh_token_expires_at: string + created_at: string +} interface McpToken { id: number @@ -26,6 +100,23 @@ export default function IntegrationsTab(): React.ReactElement { loadAddons() }, [loadAddons]) + // OAuth clients state + const [oauthClients, setOauthClients] = useState([]) + const [oauthSessions, setOauthSessions] = useState([]) + const [oauthCreateOpen, setOauthCreateOpen] = useState(false) + const [oauthNewName, setOauthNewName] = useState('') + const [oauthNewUris, setOauthNewUris] = useState('') + const [oauthNewScopes, setOauthNewScopes] = useState([]) + const [oauthCreating, setOauthCreating] = useState(false) + const [oauthCreatedClient, setOauthCreatedClient] = useState(null) + const [oauthDeleteId, setOauthDeleteId] = useState(null) + const [oauthRevokeId, setOauthRevokeId] = useState(null) + const [oauthRotateId, setOauthRotateId] = useState(null) + const [oauthRotatedSecret, setOauthRotatedSecret] = useState(null) + const [oauthRotating, setOauthRotating] = useState(false) + const [oauthScopesOpen, setOauthScopesOpen] = useState>({}) + const [oauthScopesExpanded, setOauthScopesExpanded] = useState>({}) + // MCP state const [mcpTokens, setMcpTokens] = useState([]) const [mcpModalOpen, setMcpModalOpen] = useState(false) @@ -89,6 +180,69 @@ export default function IntegrationsTab(): React.ReactElement { }) } + // Load OAuth clients and sessions + useEffect(() => { + if (mcpEnabled) { + oauthApi.clients.list().then(d => setOauthClients(d.clients || [])).catch(() => {}) + oauthApi.sessions.list().then(d => setOauthSessions(d.sessions || [])).catch(() => {}) + } + }, [mcpEnabled]) + + const handleCreateOAuthClient = async () => { + if (!oauthNewName.trim() || !oauthNewUris.trim()) return + setOauthCreating(true) + try { + const uris = oauthNewUris.split('\n').map(u => u.trim()).filter(Boolean) + const d = await oauthApi.clients.create({ name: oauthNewName.trim(), redirect_uris: uris, allowed_scopes: oauthNewScopes }) + setOauthCreatedClient(d.client) + setOauthClients(prev => [...prev, { ...d.client, client_secret: undefined }]) + setOauthNewName('') + setOauthNewUris('') + setOauthNewScopes([]) + } catch { + toast.error(t('settings.oauth.toast.createError')) + } finally { + setOauthCreating(false) + } + } + + const handleDeleteOAuthClient = async (id: string) => { + try { + await oauthApi.clients.delete(id) + setOauthClients(prev => prev.filter(c => c.id !== id)) + setOauthDeleteId(null) + toast.success(t('settings.oauth.toast.deleted')) + } catch { + toast.error(t('settings.oauth.toast.deleteError')) + } + } + + const handleRotateSecret = async (id: string) => { + setOauthRotating(true) + try { + const d = await oauthApi.clients.rotate(id) + setOauthRotatedSecret(d.client_secret) + setOauthRotateId(null) + } catch { + toast.error(t('settings.oauth.toast.rotateError')) + } finally { + setOauthRotating(false) + } + } + + const handleRevokeSession = async (id: number) => { + try { + await oauthApi.sessions.revoke(id) + setOauthSessions(prev => prev.filter(s => s.id !== id)) + setOauthRevokeId(null) + toast.success(t('settings.oauth.toast.revoked')) + } catch { + toast.error(t('settings.oauth.toast.revokeError')) + } + } + + const scopesByGroup = getScopesByGroup() + return ( <> @@ -126,46 +280,146 @@ export default function IntegrationsTab(): React.ReactElement {

{t('settings.mcp.clientConfigHint')}

- {/* Token list */} + {/* OAuth Clients */}
-
- -
- {mcpTokens.length === 0 ? ( + {oauthClients.length === 0 ? (

- {t('settings.mcp.noTokens')} + {t('settings.oauth.noClients')}

) : (
- {mcpTokens.map((token, i) => ( -
-
-

{token.name}

-

- {token.token_prefix}... - {t('settings.mcp.tokenCreatedAt')} {new Date(token.created_at).toLocaleDateString(locale)} - {token.last_used_at && ( - · {t('settings.mcp.tokenUsedAt')} {new Date(token.last_used_at).toLocaleDateString(locale)} - )} -

+ {oauthClients.map((client, i) => ( +
+
+ +
+

{client.name}

+

+ {t('settings.oauth.clientId')}: {client.client_id} + {t('settings.mcp.tokenCreatedAt')} {new Date(client.created_at).toLocaleDateString(locale)} +

+
+ {(oauthScopesExpanded[client.id] ? client.allowed_scopes : client.allowed_scopes.slice(0, 5)).map(s => ( + {s} + ))} + {client.allowed_scopes.length > 5 && ( + + )} +
+
+ +
-
))}
)}
+ + {/* Token list — deprecated */} +
+
+
+ + + Deprecated + +
+ +
+
+ +

{t('settings.mcp.apiTokensDeprecated')}

+
+
+ {mcpTokens.length === 0 ? ( +

+ {t('settings.mcp.noTokens')} +

+ ) : ( +
+ {mcpTokens.map((token, i) => ( +
+
+

{token.name}

+

+ {token.token_prefix}... + {t('settings.mcp.tokenCreatedAt')} {new Date(token.created_at).toLocaleDateString(locale)} + {token.last_used_at && ( + · {t('settings.mcp.tokenUsedAt')} {new Date(token.last_used_at).toLocaleDateString(locale)} + )} +

+
+ +
+ ))} +
+ )} +
+
+ + {/* Active OAuth Sessions */} + {oauthSessions.length > 0 && ( +
+ +
+ {oauthSessions.map((session, i) => ( +
+
+

{session.client_name}

+

+ {t('settings.oauth.sessionScopes')}: {session.scopes.join(', ')} + {t('settings.oauth.sessionExpires')} {new Date(session.access_token_expires_at).toLocaleDateString(locale)} +

+
+ +
+ ))} +
+
+ )} )} @@ -248,6 +502,282 @@ export default function IntegrationsTab(): React.ReactElement {
)} + + {/* Create OAuth Client modal */} + {oauthCreateOpen && ( +
{ if (e.target === e.currentTarget && !oauthCreatedClient) setOauthCreateOpen(false) }}> +
+ {!oauthCreatedClient ? ( + <> +

{t('settings.oauth.modal.createTitle')}

+ +
+ +
+ {OAUTH_PRESETS.map(preset => ( + + ))} +
+
+ +
+ + setOauthNewName(e.target.value)} + placeholder={t('settings.oauth.modal.clientNamePlaceholder')} + className="w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-300" + style={{ borderColor: 'var(--border-primary)', background: 'var(--bg-secondary)', color: 'var(--text-primary)' }} + autoFocus /> +
+ +
+ +