Skip to content

feat: API token management in workspace settings#10624

Open
dnplkndll wants to merge 5 commits intohcengineering:developfrom
ledoent:feat/api-token-management
Open

feat: API token management in workspace settings#10624
dnplkndll wants to merge 5 commits intohcengineering:developfrom
ledoent:feat/api-token-management

Conversation

@dnplkndll
Copy link
Contributor

@dnplkndll dnplkndll commented Mar 11, 2026

Summary

  • Add full API token CRUD (create, list, revoke) as a new workspace settings page
  • Tokens are workspace-scoped JWTs with configurable expiry (7–365 days), stored in a new api_tokens table
  • Phase 1 token scopes: read:*, write:*, delete:* with server-side enforcement
  • New createApiToken, listApiTokens, revokeApiToken account service RPC methods
  • Frontend: settings page with token list, status/permissions badges, create popup with scope preset selector

Changes

Backend (server/account/):

  • ApiToken type with scopes?: string[] + apiToken DB collection
  • V25 migration: api_tokens table; V26 migration: scopes TEXT[] column
  • Three new RPC methods registered in AccountMethods and getMethods()
  • createApiToken validates scopes, embeds in JWT extra.scopes, persists to DB
  • AccountClient interface + implementation extended with optional scopes param

Scope Enforcement (pods/server/src/rpc.ts):

  • withSession() checks decodedToken.extra.scopes against method requirements
  • read:* required for find-all, search, load-model, account
  • write:* required for tx (create/update), domain requests, ensure-person
  • delete:* additionally required for TxRemoveDoc transactions
  • No scopes (legacy tokens) = full access (backward compatible)

Frontend (plugins/setting-resources/):

  • ApiTokens.svelte — workspace settings page with permissions column (i18n)
  • ApiTokenCreatePopup.svelte — creation modal with scope preset dropdown (default: Read Only)
  • Registered as WorkspaceSettingCategory in the model

Also:

  • Fixed 3 pre-existing TS2322/TS2345 type errors in operations.ts

Test plan

  • Create a token with "Read Only" scope → verify find-all works, tx returns 403
  • Create a token with "Read & Write" → verify find-all and tx work, TxRemoveDoc returns 403
  • Create a token with "Full Access" → verify all operations work
  • Create a token with no scopes (legacy path) → verify full access (backward compat)
  • Token list shows correct permissions badge ("Read Only", "Read & Write", "Full Access")
  • Revoke a token → status changes to "revoked", API returns 401
  • V25+V26 migrations run on fresh and existing databases

Ref: #10622

🤖 Generated with Claude Code

@huly-github-staging
Copy link

Connected to Huly®: UBERF-15850

@dnplkndll dnplkndll force-pushed the feat/api-token-management branch 2 times, most recently from c84d786 to efdbe1b Compare March 13, 2026 01:59
@dnplkndll
Copy link
Contributor Author

Follow-up: REST API should apply sensible defaults for TxCreateDoc

While testing the token management flow end-to-end (mint token → create issues via REST), I discovered that POST /api/v1/tx/{workspace} passes TxCreateDoc transactions through with zero default-filling. Issues created this way are missing fields that the UI auto-populates, causing them to not appear in project views (only in all-issues).

Missing fields for tracker:class:Issue

When the UI creates an issue, it uses addCollection() which wraps the TxCreateDoc in a TxCollectionCUD, setting:

  • attachedTo: tracker:ids:NoParent
  • attachedToClass: tracker:class:Issue
  • collection: "subIssues"

Plus these attributes that get no server-side defaults:

  • kind → must be tracker:taskTypes:Issue (not tracker:ids:ClassingProjectType)
  • childInfo: [], parents: [], subIssues: 0, comments: 0, reports: 0
  • assignee: null, component: null, dueDate: null
  • estimation: 0, remainingTime: 0, reportedTime: 0

Impact

Issues created via bare TxCreateDoc (the natural REST usage) are invisible in project views because the Svelte live query filters on attachedTo/collection. They show up in the all-issues aggregated view only.

Possible approaches

  1. Document it — update REST API docs to show the required TxCollectionCUD wrapper pattern for issue creation
  2. Server-side defaults middleware — a new middleware in the tx pipeline that merges missing fields from the model schema for known classes (significant arch change)
  3. Higher-level REST endpoints — add POST /api/v1/create-issue/{workspace} that accepts a simplified payload and constructs the full TxCollectionCUD server-side, like the importer does

Option 3 seems most API-friendly without changing the platform's "caller provides everything" philosophy. Happy to contribute this if there's interest.

@dnplkndll dnplkndll force-pushed the feat/api-token-management branch 3 times, most recently from c7d2bf2 to aaa8348 Compare March 16, 2026 18:45
@ArtyomSavchenko
Copy link
Member

ArtyomSavchenko commented Mar 17, 2026

Hi @dnplkndll
Thank you for your contribution.
Could you please check formatting for server/account/src/operations.ts?
The corresponding CI step is failed: https://github.com/hcengineering/platform/actions/runs/23164707679/job/67348370400?pr=10624
You can use rush format to format all project files or rushx format for a specific package.

@dnplkndll dnplkndll force-pushed the feat/api-token-management branch 2 times, most recently from 8663ec8 to 865fd71 Compare March 17, 2026 14:42
@dnplkndll
Copy link
Contributor Author

@ArtyomSavchenko Thanks for the review! Formatting has been fixed — the issue was a prettier version mismatch (local 3.8.1 vs project's 3.6.2). New code now matches the existing codebase style with no formatting noise on existing lines.

Changes in this update

3 commits:

  1. feat: API token management — core feature (create/list/revoke tokens, UI, DB, i18n)
  2. docs: token scope roadmap — roadmap comments for future scope-based access control
  3. feat: enforce API token revocation — addresses a gap where revoked tokens remained usable until JWT expiry

What's new since last push

  • Token revocation enforcement — the transactor now checks a per-token revocation cache (60s TTL) before granting API access. Revoked tokens are rejected within ~60 seconds instead of remaining valid until expiry.
  • Owner-level workspace token visibility — workspace owners can list and revoke any member's API tokens via listWorkspaceApiTokens / revokeWorkspaceApiToken (OWNER role required).
  • Ledo instance URL removedopenapi.yaml now uses a generic https://{host} server variable.

Suggestions for future consideration

  1. Token scopes — the roadmap commit documents a proposed extra.scopes JWT field for fine-grained access control (read-only tokens, class-restricted access). This would enable safe MCP/AI agent integrations without MCP-layer changes.

  2. Higher-level REST endpoints — as noted in my earlier comment, POST /api/v1/tx requires the caller to construct full TxCollectionCUD wrappers. A POST /api/v1/create-issue style endpoint would make the API much more accessible for integrations.

  3. Revocation cache scaling — the current per-token cache works well for moderate usage. For high-traffic deployments, a Redis-backed revocation set or short-lived tokens (< 1 hour) with refresh would be more robust.

Add UI and backend support for creating, listing, and revoking
API tokens scoped to workspaces. Includes owner-level workspace
token visibility, OpenAPI documentation, Mongo/Postgres persistence,
and i18n translations.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Don Kendall <[email protected]>
@dnplkndll dnplkndll force-pushed the feat/api-token-management branch from 4e76ecd to 377fd42 Compare March 17, 2026 15:05
dnplkndll and others added 3 commits March 17, 2026 12:28
Embed apiTokenId in JWT extra field and add a per-token revocation
cache (60s TTL) in the transactor REST handler. Revoked tokens are
now rejected within ~60 seconds instead of remaining valid until
JWT expiry.

Adds checkApiTokenRevoked account service method for the transactor
to query individual token revocation status.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Don Kendall <[email protected]>
Add coarse-grained scope enforcement for API tokens. Tokens can now
be created with scopes ['read:*'], ['read:*','write:*'], or
['read:*','write:*','delete:*']. Existing tokens without scopes
retain full access (backward compatible).

- DB: v26 migration adds scopes TEXT[] column to api_tokens
- Types: add scopes field to ApiToken and ApiTokenInfo
- Operations: createApiToken accepts/validates/persists scopes,
  embeds in JWT via extra.scopes
- Enforcement: withSession checks scopes against method; tx handler
  additionally requires delete:* for TxRemoveDoc
- Client: createApiToken signature accepts optional scopes param
- UI: scope preset dropdown in create popup (default: Read Only),
  permissions column in token list with i18n labels
- Also fixes 3 pre-existing TS2322/TS2345 errors in operations.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Don Kendall <[email protected]>
- scopes.test.ts: 8 tests for hasScope() and getRequiredScope() logic
- apiTokenScopes.test.ts: 7 tests for createApiToken scope validation
  (valid scopes, multiple scopes, no scopes backward compat, invalid
  format rejection, empty array rejection, domain-scope rejection)
  and listApiTokens scopes inclusion
- Export hasScope/getRequiredScope from rpc.ts for testability

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Don Kendall <[email protected]>
@dnplkndll dnplkndll force-pushed the feat/api-token-management branch from c39720c to 058c7da Compare March 17, 2026 17:02
…tting

- Restrict API token creation/revocation to AccountRole.User or higher
  (guests cannot use API tokens), per reviewer suggestion
- Add 5 missing translation keys (ApiTokenPermissions, ApiTokenScopePreset,
  ApiTokenScopeReadOnly, ApiTokenScopeReadWrite, ApiTokenScopeFullAccess)
  to all non-en locale files to fix locale parity CI test
- Fix prettier formatting in apiTokenScopes.test.ts
- Rename local `extra` to `tokenExtra` in createApiToken to avoid
  shadowing the decoded token's `extra` field

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Don Kendall <[email protected]>

import { AccountRole, type MeasureContext, type PersonUuid, type WorkspaceUuid } from '@hcengineering/core'
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
import { decodeTokenVerbose, generateToken } from '@hcengineering/server-token'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please check warnings in this file?
CI formatting step is failed due to this:
https://github.com/hcengineering/platform/actions/runs/23317689234/job/67851578625?pr=10624

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants