From 1105b4a41b2a91a24a164c70264b294a80afe97b Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sat, 12 Jul 2025 23:37:52 +0000 Subject: feat(api): Expose the update user API in the openapi specs --- packages/api/index.ts | 2 + packages/api/middlewares/auth.ts | 22 ++++ packages/api/routes/admin.ts | 24 +++++ packages/open-api/index.ts | 2 + packages/open-api/karakeep-openapi-spec.json | 152 +++++++++++++++++++++++++++ packages/open-api/lib/admin.ts | 100 ++++++++++++++++++ 6 files changed, 302 insertions(+) create mode 100644 packages/api/routes/admin.ts create mode 100644 packages/open-api/lib/admin.ts (limited to 'packages') diff --git a/packages/api/index.ts b/packages/api/index.ts index 82beca53..39075548 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -6,6 +6,7 @@ import { poweredBy } from "hono/powered-by"; import { Context } from "@karakeep/trpc"; import trpcAdapter from "./middlewares/trpcAdapter"; +import admin from "./routes/admin"; import assets from "./routes/assets"; import bookmarks from "./routes/bookmarks"; import health from "./routes/health"; @@ -58,6 +59,7 @@ const app = new Hono<{ .route("/health", health) .route("/trpc", trpc) .route("/v1", v1) + .route("/admin", admin) .route("/assets", assets) .route("/public", publicRoute) .route("/metrics", metrics); diff --git a/packages/api/middlewares/auth.ts b/packages/api/middlewares/auth.ts index 42bca6c8..92f591ad 100644 --- a/packages/api/middlewares/auth.ts +++ b/packages/api/middlewares/auth.ts @@ -35,3 +35,25 @@ export const authMiddleware = createMiddleware<{ c.set("api", createCaller(c.get("ctx"))); await next(); }); + +export const adminAuthMiddleware = createMiddleware<{ + Variables: { + ctx: AuthedContext; + api: ReturnType; + }; +}>(async (c, next) => { + if (!c.var.ctx || !c.var.ctx.user || c.var.ctx.user === null) { + throw new HTTPException(401, { + message: "Unauthorized", + }); + } + + if (c.var.ctx.user.role !== "admin") { + throw new HTTPException(403, { + message: "Forbidden - Admin access required", + }); + } + + c.set("api", createCaller(c.get("ctx"))); + await next(); +}); diff --git a/packages/api/routes/admin.ts b/packages/api/routes/admin.ts new file mode 100644 index 00000000..4b5438d6 --- /dev/null +++ b/packages/api/routes/admin.ts @@ -0,0 +1,24 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; + +import { updateUserSchema } from "@karakeep/shared/types/admin"; + +import { adminAuthMiddleware } from "../middlewares/auth"; + +const app = new Hono() + .use(adminAuthMiddleware) + + // PUT /admin/users/:userId + .put("/users/:userId", zValidator("json", updateUserSchema), async (c) => { + const userId = c.req.param("userId"); + const body = c.req.valid("json"); + + // Ensure the userId from the URL matches the one in the body + const input = { ...body, userId }; + + await c.var.api.admin.updateUser(input); + + return c.json({ success: true }, 200); + }); + +export default app; diff --git a/packages/open-api/index.ts b/packages/open-api/index.ts index 057a823f..6f14807d 100644 --- a/packages/open-api/index.ts +++ b/packages/open-api/index.ts @@ -5,6 +5,7 @@ import { OpenAPIRegistry, } from "@asteasolutions/zod-to-openapi"; +import { registry as adminRegistry } from "./lib/admin"; import { registry as assetsRegistry } from "./lib/assets"; import { registry as bookmarksRegistry } from "./lib/bookmarks"; import { registry as commonRegistry } from "./lib/common"; @@ -22,6 +23,7 @@ function getOpenApiDocumentation() { highlightsRegistry, userRegistry, assetsRegistry, + adminRegistry, ]); const generator = new OpenApiGeneratorV3(registry.definitions); diff --git a/packages/open-api/karakeep-openapi-spec.json b/packages/open-api/karakeep-openapi-spec.json index ac74abbf..69bf27f7 100644 --- a/packages/open-api/karakeep-openapi-spec.json +++ b/packages/open-api/karakeep-openapi-spec.json @@ -3281,6 +3281,158 @@ } } } + }, + "/admin/users/{userId}": { + "put": { + "description": "Update a user's role, bookmark quota, or storage quota. Admin access required.", + "summary": "Update user", + "tags": [ + "Admin" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "The ID of the user to update", + "example": "user_123" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "user", + "admin" + ] + }, + "bookmarkQuota": { + "type": "integer", + "nullable": true, + "minimum": 0 + }, + "storageQuota": { + "type": "integer", + "nullable": true, + "minimum": 0 + } + }, + "description": "User update data", + "example": { + "role": "admin", + "bookmarkQuota": 1000, + "storageQuota": 5000000000 + } + } + } + } + }, + "responses": { + "200": { + "description": "User updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": [ + "success" + ] + } + } + } + }, + "400": { + "description": "Bad request - Invalid input data or cannot update own user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + }, + "403": { + "description": "Forbidden - Admin access required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + }, + "404": { + "description": "User not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + } + } + } } } } \ No newline at end of file diff --git a/packages/open-api/lib/admin.ts b/packages/open-api/lib/admin.ts new file mode 100644 index 00000000..80f786f3 --- /dev/null +++ b/packages/open-api/lib/admin.ts @@ -0,0 +1,100 @@ +import { + extendZodWithOpenApi, + OpenAPIRegistry, +} from "@asteasolutions/zod-to-openapi"; +import { z } from "zod"; + +import { updateUserSchema } from "@karakeep/shared/types/admin"; + +import { BearerAuth } from "./common"; + +export const registry = new OpenAPIRegistry(); +extendZodWithOpenApi(z); + +const updateUserRequestSchema = updateUserSchema.omit({ userId: true }); + +const updateUserResponseSchema = z.object({ + success: z.boolean(), +}); + +registry.registerPath({ + method: "put", + path: "/admin/users/{userId}", + description: + "Update a user's role, bookmark quota, or storage quota. Admin access required.", + summary: "Update user", + tags: ["Admin"], + security: [{ [BearerAuth.name]: [] }], + request: { + params: z.object({ + userId: z.string().openapi({ + description: "The ID of the user to update", + example: "user_123", + }), + }), + body: { + content: { + "application/json": { + schema: updateUserRequestSchema.openapi({ + description: "User update data", + example: { + role: "admin", + bookmarkQuota: 1000, + storageQuota: 5000000000, + }, + }), + }, + }, + }, + }, + responses: { + 200: { + description: "User updated successfully", + content: { + "application/json": { + schema: updateUserResponseSchema, + }, + }, + }, + 400: { + description: "Bad request - Invalid input data or cannot update own user", + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + }, + 401: { + description: "Unauthorized - Authentication required", + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + }, + 403: { + description: "Forbidden - Admin access required", + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + }, + 404: { + description: "User not found", + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + }, + }, +}); -- cgit v1.2.3-70-g09d2