From 64c3f464ac80f4cf8e8b191a72a7411abe53f287 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 11:28:40 +0000 Subject: [PATCH] feat: add project-scoped API tokens Add optional project scope to API tokens, allowing tokens to be restricted to a specific project and optionally its sub-projects. This builds on the existing API token system by adding two new fields: project_id and include_sub_projects. Key changes: - Database migration adding project_id and include_sub_projects columns - ProjectScopedAuth wrapper type implementing web.Auth with scope info - AuthUnwrapper interface for transparent auth type unwrapping - Scope enforcement in project/task permission checks and list queries - Middleware resolves scoped project IDs (with recursive CTE for sub-projects) - Frontend: project selector in token creation form, scope display in list - Tests for scope resolution, permission enforcement, and token creation https://claude.ai/code/session_015JjPNeSkwxYQNCeMf2PYTi --- .gitignore | 1 + frontend/src/i18n/lang/en.json | 7 +- frontend/src/modelTypes/IApiToken.ts | 2 + frontend/src/models/apiTokenModel.ts | 2 + .../src/views/user/settings/ApiTokens.vue | 49 ++++++ pkg/db/fixtures/api_tokens.yml | 24 +++ pkg/migration/20260322120000.go | 44 ++++++ pkg/models/api_tokens.go | 20 +++ pkg/models/api_tokens_project_scope.go | 70 +++++++++ pkg/models/api_tokens_project_scope_test.go | 134 ++++++++++++++++ pkg/models/api_tokens_test.go | 143 +++++++++++++++++- pkg/models/project.go | 13 ++ pkg/models/project_permissions.go | 23 +++ pkg/models/project_scoped_auth.go | 48 ++++++ pkg/models/task_collection.go | 17 ++- pkg/modules/auth/auth.go | 9 ++ pkg/routes/api_tokens.go | 8 + pkg/user/user.go | 5 + pkg/web/web.go | 6 + 19 files changed, 621 insertions(+), 4 deletions(-) create mode 100644 pkg/migration/20260322120000.go create mode 100644 pkg/models/api_tokens_project_scope.go create mode 100644 pkg/models/api_tokens_project_scope_test.go create mode 100644 pkg/models/project_scoped_auth.go diff --git a/.gitignore b/.gitignore index c9089f1b9..60adfca77 100644 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,6 @@ devenv.local.nix # AI Tools /.claude/settings.local.json PLAN.md +plans/ /.crush/ /.playwright-mcp diff --git a/frontend/src/i18n/lang/en.json b/frontend/src/i18n/lang/en.json index 3d4b43c12..c65726dd9 100644 --- a/frontend/src/i18n/lang/en.json +++ b/frontend/src/i18n/lang/en.json @@ -212,11 +212,16 @@ "text1": "Are you sure you want to delete the token \"{token}\"?", "text2": "This will revoke access to all applications or integrations using it. You cannot undo this." }, + "projectScopeExplanation": "Optionally restrict this token to a specific project. Leave empty for access to all projects.", + "includeSubProjects": "Include sub-projects", + "allProjects": "All projects", + "withSubProjects": "+ sub-projects", "attributes": { "title": "Title", "titlePlaceholder": "Enter a title you will recognize later", "expiresAt": "Expires at", - "permissions": "Permissions" + "permissions": "Permissions", + "projectScope": "Project scope" } }, "sessions": { diff --git a/frontend/src/modelTypes/IApiToken.ts b/frontend/src/modelTypes/IApiToken.ts index 189549c4d..1ee14d2e4 100644 --- a/frontend/src/modelTypes/IApiToken.ts +++ b/frontend/src/modelTypes/IApiToken.ts @@ -10,5 +10,7 @@ export interface IApiToken extends IAbstract { token: string permissions: IApiPermission expiresAt: Date + projectId: number + includeSubProjects: boolean created: Date } diff --git a/frontend/src/models/apiTokenModel.ts b/frontend/src/models/apiTokenModel.ts index a23227004..c17dac368 100644 --- a/frontend/src/models/apiTokenModel.ts +++ b/frontend/src/models/apiTokenModel.ts @@ -7,6 +7,8 @@ export default class ApiTokenModel extends AbstractModel { token = '' permissions = null expiresAt: Date = null + projectId = 0 + includeSubProjects = false created: Date = null constructor(data: Partial = {}) { diff --git a/frontend/src/views/user/settings/ApiTokens.vue b/frontend/src/views/user/settings/ApiTokens.vue index 6c7f81624..d61543398 100644 --- a/frontend/src/views/user/settings/ApiTokens.vue +++ b/frontend/src/views/user/settings/ApiTokens.vue @@ -16,6 +16,9 @@ import {useI18n} from 'vue-i18n' import Message from '@/components/misc/Message.vue' import FormField from '@/components/input/FormField.vue' import type {IApiToken} from '@/modelTypes/IApiToken' +import type {IProject} from '@/modelTypes/IProject' +import ProjectSearch from '@/components/tasks/partials/ProjectSearch.vue' +import {useProjectStore} from '@/stores/projects' const service = new ApiTokenService() const tokens = ref([]) @@ -32,9 +35,14 @@ const newTokenPermissionValid = ref(true) const apiTokenTitle = ref() const tokenCreatedSuccessMessage = ref('') +const selectedProject = ref(null) +const includeSubProjects = ref(false) + const showDeleteModal = ref(false) const tokenToDelete = ref() +const projectStore = useProjectStore() + const {t} = useI18n() const route = useRoute() @@ -158,11 +166,16 @@ async function createToken() { newToken.value.expiresAt = new Date(newTokenExpiryCustom.value) } + newToken.value.projectId = selectedProject.value?.id || 0 + newToken.value.includeSubProjects = selectedProject.value ? includeSubProjects.value : false + const token = await service.create(newToken.value) tokenCreatedSuccessMessage.value = t('user.settings.apiTokens.tokenCreatedSuccess', {token: token.token}) newToken.value = new ApiTokenModel() newTokenExpiry.value = 30 newTokenExpiryCustom.value = new Date() + selectedProject.value = null + includeSubProjects.value = false resetPermissions() tokens.value.push(token) showCreateForm.value = false @@ -226,6 +239,7 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) { {{ $t('misc.id') }} {{ $t('user.settings.apiTokens.attributes.title') }} + {{ $t('user.settings.apiTokens.attributes.projectScope') }} {{ $t('user.settings.apiTokens.attributes.permissions') }} {{ $t('user.settings.apiTokens.attributes.expiresAt') }} {{ $t('misc.created') }} @@ -241,6 +255,23 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) { > {{ tk.id }} {{ tk.title }} + + + + {{ $t('user.settings.apiTokens.allProjects') }} + +