feat: implement user inventory management and class update endpoints

This commit is contained in:
syntaxbullet
2026-02-08 16:55:04 +01:00
parent fbf1e52c28
commit 4232674494
2 changed files with 831 additions and 17 deletions

View File

@@ -149,17 +149,24 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
if (url.pathname === "/api/quests" && req.method === "POST") {
try {
const { questService } = await import("@shared/modules/quest/quest.service");
const data = await req.json();
const { CreateQuestSchema } = await import("@shared/modules/quest/quest.types");
// Basic validation could be added here or rely on service/DB
const rawBody = await req.json();
const parsed = CreateQuestSchema.safeParse(rawBody);
if (!parsed.success) {
return Response.json({ error: "Invalid payload", issues: parsed.error.issues }, { status: 400 });
}
const data = parsed.data;
const result = await questService.createQuest({
name: data.name,
description: data.description || "",
triggerEvent: data.triggerEvent,
requirements: { target: Number(data.target) || 1 },
requirements: { target: data.target },
rewards: {
xp: Number(data.xpReward) || 0,
balance: Number(data.balanceReward) || 0
xp: data.xpReward,
balance: data.balanceReward
}
});
@@ -209,11 +216,11 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
const { questService } = await import("@shared/modules/quest/quest.service");
const result = await questService.deleteQuest(id);
if (result.length === 0) {
if (!result || result.length === 0) {
return Response.json({ error: "Quest not found" }, { status: 404 });
}
return Response.json({ success: true, deleted: result[0].id });
return Response.json({ success: true, deleted: (result[0] as { id: number }).id });
} catch (error) {
logger.error("web", "Error deleting quest", error);
return Response.json(
@@ -232,20 +239,30 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
try {
const { questService } = await import("@shared/modules/quest/quest.service");
const data = await req.json();
const { UpdateQuestSchema } = await import("@shared/modules/quest/quest.types");
const rawBody = await req.json();
const parsed = UpdateQuestSchema.safeParse(rawBody);
if (!parsed.success) {
return Response.json({ error: "Invalid payload", issues: parsed.error.issues }, { status: 400 });
}
const data = parsed.data;
const result = await questService.updateQuest(id, {
name: data.name,
description: data.description,
triggerEvent: data.triggerEvent,
requirements: { target: Number(data.target) || 1 },
rewards: {
xp: Number(data.xpReward) || 0,
balance: Number(data.balanceReward) || 0
}
...(data.name !== undefined && { name: data.name }),
...(data.description !== undefined && { description: data.description }),
...(data.triggerEvent !== undefined && { triggerEvent: data.triggerEvent }),
...(data.target !== undefined && { requirements: { target: data.target } }),
...((data.xpReward !== undefined || data.balanceReward !== undefined) && {
rewards: {
xp: data.xpReward ?? 0,
balance: data.balanceReward ?? 0
}
})
});
if (result.length === 0) {
if (!result || result.length === 0) {
return Response.json({ error: "Quest not found" }, { status: 404 });
}
@@ -713,6 +730,482 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
return new Response("Not found", { status: 404 });
}
// =====================================
// Users Management API
// =====================================
// GET /api/users - List all users with filtering
if (url.pathname === "/api/users" && req.method === "GET") {
try {
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;
}
// Build order clause
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);
// Get total count
const countResult = await DrizzleClient.select({ count: sql<number>`count(*)` }).from(users);
const total = Number(countResult[0]?.count || 0);
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ users: result, total }, jsonReplacer), {
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error fetching users", error);
return Response.json(
{ error: "Failed to fetch users", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// GET /api/users/:id - Get single user
if (url.pathname.match(/^\/api\/users\/\d+$/) && req.method === "GET") {
const id = url.pathname.split("/").pop()!;
try {
const { userService } = await import("@shared/modules/user/user.service");
const user = await userService.getUserById(id);
if (!user) {
return Response.json({ error: "User not found" }, { status: 404 });
}
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify(user, jsonReplacer), {
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error fetching user", error);
return Response.json(
{ error: "Failed to fetch user", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// PUT /api/users/:id - Update user
if (url.pathname.match(/^\/api\/users\/\d+$/) && req.method === "PUT") {
const id = url.pathname.split("/").pop()!;
try {
const { userService } = await import("@shared/modules/user/user.service");
const data = await req.json() as Record<string, any>;
// Check if user exists
const existing = await userService.getUserById(id);
if (!existing) {
return Response.json({ error: "User not found" }, { status: 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);
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ success: true, user: updatedUser }, jsonReplacer), {
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error updating user", error);
return Response.json(
{ error: "Failed to update user", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// GET /api/users/:id/inventory - Get user inventory
if (url.pathname.match(/^\/api\/users\/\d+\/inventory$/) && req.method === "GET") {
const id = url.pathname.split("/")[3] || "0";
try {
const { inventoryService } = await import("@shared/modules/inventory/inventory.service");
const inventory = await inventoryService.getInventory(id);
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ inventory }, jsonReplacer), {
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error fetching inventory", error);
return Response.json(
{ error: "Failed to fetch inventory", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// POST /api/users/:id/inventory - Add item to inventory
if (url.pathname.match(/^\/api\/users\/\d+\/inventory$/) && req.method === "POST") {
const id = url.pathname.split("/")[3] || "0";
try {
const { inventoryService } = await import("@shared/modules/inventory/inventory.service");
const data = await req.json() as Record<string, any>;
if (!data.itemId || !data.quantity) {
return Response.json(
{ error: "Missing required fields: itemId, quantity" },
{ status: 400 }
);
}
const entry = await inventoryService.addItem(id, data.itemId, BigInt(data.quantity));
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ success: true, entry }, jsonReplacer), {
status: 201,
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error adding item to inventory", error);
return Response.json(
{ error: "Failed to add item", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// DELETE /api/users/:id/inventory/:itemId - Remove item from inventory
if (url.pathname.match(/^\/api\/users\/\d+\/inventory\/\d+$/) && req.method === "DELETE") {
const parts = url.pathname.split("/");
const userId = parts[3];
const itemId = parseInt(parts[5]);
try {
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 });
} catch (error) {
logger.error("web", "Error removing item from inventory", error);
return Response.json(
{ error: "Failed to remove item", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// =====================================
// Classes Management API
// =====================================
// GET /api/classes - List all classes
if (url.pathname === "/api/classes" && req.method === "GET") {
try {
const { classService } = await import("@shared/modules/class/class.service");
const classes = await classService.getAllClasses();
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ classes }, jsonReplacer), {
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error fetching classes", error);
return Response.json(
{ error: "Failed to fetch classes", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// POST /api/classes - Create new class
if (url.pathname === "/api/classes" && req.method === "POST") {
try {
const { classService } = await import("@shared/modules/class/class.service");
const data = await req.json() as Record<string, any>;
if (!data.id || !data.name) {
return Response.json(
{ error: "Missing required fields: id and name are required" },
{ status: 400 }
);
}
const newClass = await classService.createClass({
id: BigInt(data.id),
name: data.name,
balance: data.balance ? BigInt(data.balance) : 0n,
roleId: data.roleId || null,
});
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ success: true, class: newClass }, jsonReplacer), {
status: 201,
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error creating class", error);
return Response.json(
{ error: "Failed to create class", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// PUT /api/classes/:id - Update class
if (url.pathname.match(/^\/api\/classes\/\d+$/) && req.method === "PUT") {
const id = url.pathname.split("/").pop()!;
try {
const { classService } = await import("@shared/modules/class/class.service");
const data = await req.json() as Record<string, any>;
const updateData: any = {};
if (data.name !== undefined) updateData.name = data.name;
if (data.balance !== undefined) updateData.balance = BigInt(data.balance);
if (data.roleId !== undefined) updateData.roleId = data.roleId;
const updatedClass = await classService.updateClass(BigInt(id), updateData);
if (!updatedClass) {
return Response.json({ error: "Class not found" }, { status: 404 });
}
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ success: true, class: updatedClass }, jsonReplacer), {
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error updating class", error);
return Response.json(
{ error: "Failed to update class", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// DELETE /api/classes/:id - Delete class
if (url.pathname.match(/^\/api\/classes\/\d+$/) && req.method === "DELETE") {
const id = url.pathname.split("/").pop()!;
try {
const { classService } = await import("@shared/modules/class/class.service");
await classService.deleteClass(BigInt(id));
return new Response(null, { status: 204 });
} catch (error) {
logger.error("web", "Error deleting class", error);
return Response.json(
{ error: "Failed to delete class", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// =====================================
// Moderation API
// =====================================
// GET /api/moderation - List moderation cases
if (url.pathname === "/api/moderation" && req.method === "GET") {
try {
const { ModerationService } = await import("@shared/modules/moderation/moderation.service");
const filter: any = {};
if (url.searchParams.get("userId")) filter.userId = url.searchParams.get("userId");
if (url.searchParams.get("moderatorId")) filter.moderatorId = url.searchParams.get("moderatorId");
if (url.searchParams.get("type")) filter.type = url.searchParams.get("type");
const activeParam = url.searchParams.get("active");
if (activeParam !== null) filter.active = activeParam === "true";
filter.limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")!) : 50;
filter.offset = url.searchParams.get("offset") ? parseInt(url.searchParams.get("offset")!) : 0;
const cases = await ModerationService.searchCases(filter);
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ cases }, jsonReplacer), {
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error fetching moderation cases", error);
return Response.json(
{ error: "Failed to fetch moderation cases", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// GET /api/moderation/:caseId - Get single case
if (url.pathname.match(/^\/api\/moderation\/CASE-\d+$/i) && req.method === "GET") {
const caseId = url.pathname.split("/").pop()!.toUpperCase();
try {
const { ModerationService } = await import("@shared/modules/moderation/moderation.service");
const moderationCase = await ModerationService.getCaseById(caseId);
if (!moderationCase) {
return Response.json({ error: "Case not found" }, { status: 404 });
}
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify(moderationCase, jsonReplacer), {
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error fetching moderation case", error);
return Response.json(
{ error: "Failed to fetch case", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// POST /api/moderation - Create new case
if (url.pathname === "/api/moderation" && req.method === "POST") {
try {
const { ModerationService } = await import("@shared/modules/moderation/moderation.service");
const data = await req.json() as Record<string, any>;
if (!data.type || !data.userId || !data.username || !data.moderatorId || !data.moderatorName || !data.reason) {
return Response.json(
{ error: "Missing required fields: type, userId, username, moderatorId, moderatorName, reason" },
{ status: 400 }
);
}
const newCase = await ModerationService.createCase({
type: data.type,
userId: data.userId,
username: data.username,
moderatorId: data.moderatorId,
moderatorName: data.moderatorName,
reason: data.reason,
metadata: data.metadata || {},
});
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ success: true, case: newCase }, jsonReplacer), {
status: 201,
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error creating moderation case", error);
return Response.json(
{ error: "Failed to create case", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// PUT /api/moderation/:caseId/clear - Clear/resolve a case
if (url.pathname.match(/^\/api\/moderation\/CASE-\d+\/clear$/i) && req.method === "PUT") {
const caseId = (url.pathname.split("/")[3] || "").toUpperCase();
try {
const { ModerationService } = await import("@shared/modules/moderation/moderation.service");
const data = await req.json() as Record<string, any>;
if (!data.clearedBy || !data.clearedByName) {
return Response.json(
{ error: "Missing required fields: clearedBy, clearedByName" },
{ status: 400 }
);
}
const updatedCase = await ModerationService.clearCase({
caseId,
clearedBy: data.clearedBy,
clearedByName: data.clearedByName,
reason: data.reason || "Cleared via API",
});
if (!updatedCase) {
return Response.json({ error: "Case not found" }, { status: 404 });
}
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ success: true, case: updatedCase }, jsonReplacer), {
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error clearing moderation case", error);
return Response.json(
{ error: "Failed to clear case", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// =====================================
// Transactions API
// =====================================
// GET /api/transactions - List transactions
if (url.pathname === "/api/transactions" && req.method === "GET") {
try {
const { transactions } = await import("../../shared/db/schema");
const { DrizzleClient } = await import("@shared/db/DrizzleClient");
const { eq, desc } = await import("drizzle-orm");
const userId = url.searchParams.get("userId");
const type = url.searchParams.get("type");
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(transactions);
if (userId) {
query = query.where(eq(transactions.userId, BigInt(userId))) as typeof query;
}
if (type) {
query = query.where(eq(transactions.type, type)) as typeof query;
}
const result = await query
.orderBy(desc(transactions.createdAt))
.limit(limit)
.offset(offset);
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ transactions: result }, jsonReplacer), {
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error fetching transactions", error);
return Response.json(
{ error: "Failed to fetch transactions", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// No frontend - return 404 for unknown routes
return new Response("Not Found", { status: 404 });
},