/** * @fileoverview User management endpoints for Aurora API. * Provides CRUD operations for users and user inventory. */ import type { RouteContext, RouteModule } from "./types"; import { jsonResponse, errorResponse, parseBody, parseIdFromPath, parseStringIdFromPath, withErrorHandling } from "./utils"; import { UpdateUserSchema, InventoryAddSchema } from "./schemas"; /** * Users routes handler. * * Endpoints: * - GET /api/users - List users with filters * - GET /api/users/:id - Get single user * - PUT /api/users/:id - Update user * - GET /api/users/:id/inventory - Get user inventory * - POST /api/users/:id/inventory - Add item to inventory * - DELETE /api/users/:id/inventory/:itemId - Remove item from inventory */ async function handler(ctx: RouteContext): Promise { const { pathname, method, req, url } = ctx; // Only handle requests to /api/users* if (!pathname.startsWith("/api/users")) { return null; } /** * @route GET /api/users * @description Returns a paginated list of users with optional filtering and sorting. * * @query search - Filter by username (partial match) * @query sortBy - Sort field: balance, level, xp, username (default: balance) * @query sortOrder - Sort direction: asc, desc (default: desc) * @query limit - Max results (default: 50) * @query offset - Pagination offset (default: 0) * * @response 200 - `{ users: User[], total: number }` * @response 500 - Error fetching users * * @example * // Request * GET /api/users?sortBy=level&sortOrder=desc&limit=10 */ if (pathname === "/api/users" && method === "GET") { return withErrorHandling(async () => { const { users } = await import("@shared/db/schema"); const { DrizzleClient } = await import("@shared/db/DrizzleClient"); const { ilike, desc, asc, sql } = await import("drizzle-orm"); const search = url.searchParams.get("search") || undefined; const sortBy = url.searchParams.get("sortBy") || "balance"; const sortOrder = url.searchParams.get("sortOrder") || "desc"; const limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")!) : 50; const offset = url.searchParams.get("offset") ? parseInt(url.searchParams.get("offset")!) : 0; let query = DrizzleClient.select().from(users); if (search) { query = query.where(ilike(users.username, `%${search}%`)) as typeof query; } const sortColumn = sortBy === "level" ? users.level : sortBy === "xp" ? users.xp : sortBy === "username" ? users.username : users.balance; const orderFn = sortOrder === "asc" ? asc : desc; const result = await query .orderBy(orderFn(sortColumn)) .limit(limit) .offset(offset); const countResult = await DrizzleClient.select({ count: sql`count(*)` }).from(users); const total = Number(countResult[0]?.count || 0); return jsonResponse({ users: result, total }); }, "fetch users"); } /** * @route GET /api/users/:id * @description Returns a single user by Discord ID. * Includes related class information if the user has a class assigned. * * @param id - Discord User ID (snowflake) * @response 200 - Full user object with class relation * @response 404 - User not found * @response 500 - Error fetching user * * @example * // Response * { * "id": "123456789012345678", * "username": "Player1", * "balance": "1000", * "level": 5, * "class": { "id": "1", "name": "Warrior" } * } */ if (pathname.match(/^\/api\/users\/\d+$/) && method === "GET") { const id = parseStringIdFromPath(pathname); if (!id) return null; return withErrorHandling(async () => { const { userService } = await import("@shared/modules/user/user.service"); const user = await userService.getUserById(id); if (!user) { return errorResponse("User not found", 404); } return jsonResponse(user); }, "fetch user"); } /** * @route PUT /api/users/:id * @description Updates user fields. Only provided fields will be updated. * * @param id - Discord User ID (snowflake) * @body { * username?: string, * balance?: string | number, * xp?: string | number, * level?: number, * dailyStreak?: number, * isActive?: boolean, * settings?: object, * classId?: string | number * } * @response 200 - `{ success: true, user: User }` * @response 404 - User not found * @response 500 - Error updating user */ if (pathname.match(/^\/api\/users\/\d+$/) && method === "PUT") { const id = parseStringIdFromPath(pathname); if (!id) return null; return withErrorHandling(async () => { const { userService } = await import("@shared/modules/user/user.service"); const data = await req.json() as Record; const existing = await userService.getUserById(id); if (!existing) { return errorResponse("User not found", 404); } // Build update data (only allow safe fields) const updateData: any = {}; if (data.username !== undefined) updateData.username = data.username; if (data.balance !== undefined) updateData.balance = BigInt(data.balance); if (data.xp !== undefined) updateData.xp = BigInt(data.xp); if (data.level !== undefined) updateData.level = parseInt(data.level); if (data.dailyStreak !== undefined) updateData.dailyStreak = parseInt(data.dailyStreak); if (data.isActive !== undefined) updateData.isActive = Boolean(data.isActive); if (data.settings !== undefined) updateData.settings = data.settings; if (data.classId !== undefined) updateData.classId = BigInt(data.classId); const updatedUser = await userService.updateUser(id, updateData); return jsonResponse({ success: true, user: updatedUser }); }, "update user"); } /** * @route GET /api/users/:id/inventory * @description Returns user's inventory with item details. * * @param id - Discord User ID (snowflake) * @response 200 - `{ inventory: InventoryEntry[] }` * @response 500 - Error fetching inventory * * @example * // Response * { * "inventory": [ * { * "userId": "123456789", * "itemId": 1, * "quantity": "5", * "item": { "id": 1, "name": "Health Potion", ... } * } * ] * } */ if (pathname.match(/^\/api\/users\/\d+\/inventory$/) && method === "GET") { const id = pathname.split("/")[3] || "0"; return withErrorHandling(async () => { const { inventoryService } = await import("@shared/modules/inventory/inventory.service"); const inventory = await inventoryService.getInventory(id); return jsonResponse({ inventory }); }, "fetch inventory"); } /** * @route POST /api/users/:id/inventory * @description Adds an item to user's inventory. * * @param id - Discord User ID (snowflake) * @body { itemId: number, quantity: string | number } * @response 201 - `{ success: true, entry: InventoryEntry }` * @response 400 - Missing required fields * @response 500 - Error adding item */ if (pathname.match(/^\/api\/users\/\d+\/inventory$/) && method === "POST") { const id = pathname.split("/")[3] || "0"; return withErrorHandling(async () => { const { inventoryService } = await import("@shared/modules/inventory/inventory.service"); const data = await req.json() as Record; if (!data.itemId || !data.quantity) { return errorResponse("Missing required fields: itemId, quantity", 400); } const entry = await inventoryService.addItem(id, data.itemId, BigInt(data.quantity)); return jsonResponse({ success: true, entry }, 201); }, "add item to inventory"); } /** * @route DELETE /api/users/:id/inventory/:itemId * @description Removes an item from user's inventory. * * @param id - Discord User ID (snowflake) * @param itemId - Item ID to remove * @query amount - Quantity to remove (default: 1) * @response 204 - Item removed (no content) * @response 500 - Error removing item */ if (pathname.match(/^\/api\/users\/\d+\/inventory\/\d+$/) && method === "DELETE") { const parts = pathname.split("/"); const userId = parts[3] || ""; const itemId = parseInt(parts[5] || "0"); if (!userId) return null; return withErrorHandling(async () => { const { inventoryService } = await import("@shared/modules/inventory/inventory.service"); const amount = url.searchParams.get("amount"); const quantity = amount ? BigInt(amount) : 1n; await inventoryService.removeItem(userId, itemId, quantity); return new Response(null, { status: 204 }); }, "remove item from inventory"); } return null; } export const usersRoutes: RouteModule = { name: "users", handler };