Skip to content

Commit 2fa4880

Browse files
crystalinMSLclaude
authored
feat: add API key renaming functionality (#156)
* feat: add API key renaming functionality - Add updateTrainApiKeyName database query function - Add UpdateApiKeyRequest type definition - Add PATCH /:projectId/api-keys/:keyId endpoint for renaming - Implement permission model: project owner OR key creator can rename - Add input validation: max 255 chars, trim whitespace, convert empty to null - Add comprehensive unit tests for validation logic Note: Project members can already add and revoke their own API keys (existing functionality). This change only adds the missing renaming capability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: add missing @types/node dependency This resolves TypeScript type checking issues in CI 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: format API key naming test file to pass CI - Fixed formatting issues in tests/unit/api-key-naming.test.ts - All CI checks (typecheck, format:check) now pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix: resolve CI failures for PR #156 - Add missing nanoid dependency to dashboard package.json - Fix TypeScript configuration by adding DOM lib to tsconfig - Remove unnecessary type assertion for AbortController.abort() - Fix test mock typing to work with DOM types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix: add hono dependencies to root package.json for integration tests - Add hono and @hono/node-server to root dependencies - Fixes integration test failures in CI that couldn't find hono package 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: MSL <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 97eb58a commit 2fa4880

File tree

9 files changed

+257
-5
lines changed

9 files changed

+257
-5
lines changed

bun.lock

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
"": {
55
"name": "claude-nexus-monorepo",
66
"dependencies": {
7+
"@hono/node-server": "^1.13.7",
78
"@octokit/rest": "^22.0.0",
89
"dotenv": "^16.4.7",
910
"gray-matter": "^4.0.3",
1011
"handlebars": "^4.7.8",
12+
"hono": "^4.6.13",
1113
"js-yaml": "^4.1.0",
1214
"pg": "^8.16.3",
1315
},
@@ -17,6 +19,7 @@
1719
"@playwright/test": "^1.54.2",
1820
"@types/glob": "^9.0.0",
1921
"@types/js-yaml": "^4.0.9",
22+
"@types/node": "^24.7.2",
2023
"@types/pg": "^8.11.10",
2124
"bun-types": "latest",
2225
"concurrently": "^8.2.2",
@@ -53,6 +56,7 @@
5356
"elkjs": "^0.10.0",
5457
"hono": "^4.6.13",
5558
"marked": "^15.0.12",
59+
"nanoid": "^5.0.9",
5660
"node-cache": "^5.1.2",
5761
"sanitize-html": "^2.17.0",
5862
"zod": "^3.24.1",
@@ -234,7 +238,7 @@
234238

235239
"@types/mdurl": ["@types/[email protected]", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
236240

237-
"@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
241+
"@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="],
238242

239243
"@types/pg": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg=="],
240244

@@ -932,7 +936,7 @@
932936

933937
"underscore": ["[email protected]", "", {}, "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="],
934938

935-
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
939+
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
936940

937941
"universal-user-agent": ["[email protected]", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
938942

@@ -980,10 +984,14 @@
980984

981985
"zod-to-json-schema": ["[email protected]", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
982986

987+
"@agent-prompttrain/proxy/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
988+
983989
"@eslint-community/eslint-utils/eslint-visitor-keys": ["[email protected]", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
984990

985991
"@eslint/plugin-kit/@eslint/core": ["@eslint/[email protected]", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="],
986992

993+
"@grpc/grpc-js/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
994+
987995
"@grpc/proto-loader/protobufjs": ["[email protected]", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw=="],
988996

989997
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/[email protected]", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
@@ -994,10 +1002,18 @@
9941002

9951003
"@isaacs/cliui/wrap-ansi": ["[email protected]", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
9961004

1005+
"@slack/webhook/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
1006+
1007+
"@types/pg/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
1008+
1009+
"@types/rimraf/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
1010+
9971011
"@typescript-eslint/eslint-plugin/ignore": ["[email protected]", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
9981012

9991013
"@typescript-eslint/typescript-estree/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
10001014

1015+
"bun-types/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
1016+
10011017
"chalk/supports-color": ["[email protected]", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
10021018

10031019
"cli-truncate/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
@@ -1032,6 +1048,8 @@
10321048

10331049
"postcss/nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
10341050

1051+
"protobufjs/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
1052+
10351053
"protobufjs-cli/espree": ["[email protected]", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
10361054

10371055
"protobufjs-cli/glob": ["[email protected]", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
@@ -1046,14 +1064,28 @@
10461064

10471065
"wrap-ansi/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
10481066

1067+
"@agent-prompttrain/proxy/@types/node/undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
1068+
1069+
"@grpc/grpc-js/@types/node/undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
1070+
1071+
"@grpc/proto-loader/protobufjs/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
1072+
10491073
"@isaacs/cliui/string-width/emoji-regex": ["[email protected]", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
10501074

10511075
"@isaacs/cliui/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
10521076

10531077
"@isaacs/cliui/wrap-ansi/ansi-styles": ["[email protected]", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
10541078

1079+
"@slack/webhook/@types/node/undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
1080+
1081+
"@types/pg/@types/node/undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
1082+
1083+
"@types/rimraf/@types/node/undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
1084+
10551085
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
10561086

1087+
"bun-types/@types/node/undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
1088+
10571089
"cli-truncate/string-width/emoji-regex": ["[email protected]", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
10581090

10591091
"cli-truncate/string-width/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
@@ -1080,10 +1112,14 @@
10801112

10811113
"protobufjs-cli/glob/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
10821114

1115+
"protobufjs/@types/node/undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
1116+
10831117
"wrap-ansi/string-width/emoji-regex": ["[email protected]", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
10841118

10851119
"wrap-ansi/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
10861120

1121+
"@grpc/proto-loader/protobufjs/@types/node/undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
1122+
10871123
"cli-truncate/string-width/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
10881124

10891125
"protobufjs-cli/glob/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"@playwright/test": "^1.54.2",
9494
"@types/glob": "^9.0.0",
9595
"@types/js-yaml": "^4.0.9",
96+
"@types/node": "^24.7.2",
9697
"@types/pg": "^8.11.10",
9798
"bun-types": "latest",
9899
"concurrently": "^8.2.2",
@@ -105,10 +106,12 @@
105106
"typescript-eslint": "^8.18.0"
106107
},
107108
"dependencies": {
109+
"@hono/node-server": "^1.13.7",
108110
"@octokit/rest": "^22.0.0",
109111
"dotenv": "^16.4.7",
110112
"gray-matter": "^4.0.3",
111113
"handlebars": "^4.7.8",
114+
"hono": "^4.6.13",
112115
"js-yaml": "^4.1.0",
113116
"pg": "^8.16.3"
114117
}

packages/shared/src/database/queries/api-key-queries.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,26 @@ export async function revokeTrainApiKey(
187187
return (result.rowCount ?? 0) > 0
188188
}
189189

190+
/**
191+
* Update an API key name
192+
*/
193+
export async function updateTrainApiKeyName(
194+
pool: Pool,
195+
keyId: string,
196+
name: string | null
197+
): Promise<boolean> {
198+
const result = await pool.query(
199+
`
200+
UPDATE project_api_keys
201+
SET name = $2
202+
WHERE id = $1 AND revoked_at IS NULL
203+
`,
204+
[keyId, name]
205+
)
206+
207+
return (result.rowCount ?? 0) > 0
208+
}
209+
190210
/**
191211
* Delete an API key
192212
*/

packages/shared/src/types/credentials.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ export interface CreateApiKeyRequest {
124124
created_by?: string
125125
}
126126

127+
export interface UpdateApiKeyRequest {
128+
name?: string | null
129+
}
130+
127131
export interface GeneratedApiKey {
128132
id: string
129133
api_key: string // Full key, shown only once

services/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"elkjs": "^0.10.0",
2727
"hono": "^4.6.13",
2828
"marked": "^15.0.12",
29+
"nanoid": "^5.0.9",
2930
"node-cache": "^5.1.2",
3031
"sanitize-html": "^2.17.0",
3132
"zod": "^3.24.1"

services/dashboard/src/layout/__tests__/theme.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('Theme Management', () => {
4040

4141
// Mock global objects
4242
global.localStorage = localStorageMock
43-
global.document = mockDocument
43+
global.document = mockDocument as any
4444
})
4545

4646
it('should default to light theme when no preference is stored', () => {

services/dashboard/src/routes/api-keys.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import {
44
listTrainApiKeys,
55
createTrainApiKey,
66
revokeTrainApiKey,
7+
updateTrainApiKeyName,
78
getProjectByProjectId,
89
getTrainApiKeySafe,
910
isProjectOwner,
1011
isProjectMember,
1112
} from '@agent-prompttrain/shared/database/queries'
12-
import type { CreateApiKeyRequest } from '@agent-prompttrain/shared'
13+
import type { CreateApiKeyRequest, UpdateApiKeyRequest } from '@agent-prompttrain/shared'
1314
import type { AuthContext } from '../middleware/auth.js'
1415

1516
const apiKeys = new Hono<{ Variables: { auth: AuthContext } }>()
@@ -90,6 +91,80 @@ apiKeys.post('/:projectId/api-keys', async c => {
9091
}
9192
})
9293

94+
// PATCH /api/projects/:projectId/api-keys/:keyId - Update API key name (owner or key creator only)
95+
apiKeys.patch('/:projectId/api-keys/:keyId', async c => {
96+
try {
97+
const pool = container.getPool()
98+
const auth = c.get('auth')
99+
100+
if (!auth.isAuthenticated) {
101+
return c.json({ error: 'Unauthorized' }, 401)
102+
}
103+
104+
const projectId = c.req.param('projectId')
105+
const keyId = c.req.param('keyId')
106+
107+
const train = await getProjectByProjectId(pool, projectId)
108+
if (!train) {
109+
return c.json({ error: 'Project not found' }, 404)
110+
}
111+
112+
// Check project membership
113+
const isMember = await isProjectMember(pool, train.id, auth.principal)
114+
if (!isMember) {
115+
return c.json({ error: 'Access denied: You are not a member of this project' }, 403)
116+
}
117+
118+
// Get the API key to check ownership
119+
const apiKey = await getTrainApiKeySafe(pool, keyId)
120+
if (!apiKey) {
121+
return c.json({ error: 'API key not found' }, 404)
122+
}
123+
124+
// Check if key belongs to this project
125+
if (apiKey.project_id !== train.id) {
126+
return c.json({ error: 'API key does not belong to this project' }, 403)
127+
}
128+
129+
// Check if user is project owner OR key creator
130+
const isOwner = await isProjectOwner(pool, train.id, auth.principal)
131+
const isKeyCreator = apiKey.created_by === auth.principal
132+
133+
if (!isOwner && !isKeyCreator) {
134+
return c.json(
135+
{ error: 'Access denied: Only project owners or key creators can update API keys' },
136+
403
137+
)
138+
}
139+
140+
const body = await c.req.json<UpdateApiKeyRequest>()
141+
142+
// Validate name if provided
143+
if (body.name !== undefined && body.name !== null && typeof body.name === 'string') {
144+
if (body.name.trim().length === 0) {
145+
body.name = null // Convert empty string to null
146+
} else if (body.name.length > 255) {
147+
return c.json({ error: 'Name must be 255 characters or less' }, 400)
148+
} else {
149+
body.name = body.name.trim() // Trim whitespace
150+
}
151+
}
152+
153+
const success = await updateTrainApiKeyName(pool, keyId, body.name ?? null)
154+
155+
if (!success) {
156+
return c.json({ error: 'API key not found or update failed' }, 404)
157+
}
158+
159+
// Return the updated key
160+
const updatedKey = await getTrainApiKeySafe(pool, keyId)
161+
return c.json({ api_key: updatedKey })
162+
} catch (error) {
163+
console.error('Failed to update API key:', error)
164+
return c.json({ error: 'Failed to update API key' }, 500)
165+
}
166+
})
167+
93168
// DELETE /api/projects/:projectId/api-keys/:keyId - Revoke API key (owner or key creator only)
94169
apiKeys.delete('/:projectId/api-keys/:keyId', async c => {
95170
try {

services/dashboard/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
"target": "ES2022",
44
"module": "ES2022",
5-
"lib": ["ES2022"],
5+
"lib": ["ES2022", "DOM"],
66
"outDir": "./dist",
77
"rootDir": "./src",
88
"strict": true,

0 commit comments

Comments
 (0)