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

@@ -2,6 +2,35 @@
REST API server for Aurora bot management. Base URL: `http://localhost:3000`
## Common Response Formats
**Success Responses:**
- Single resource: `{ ...resource }` or `{ success: true, resource: {...} }`
- List operations: `{ items: [...], total: number }`
- Mutations: `{ success: true, resource: {...} }`
**Error Responses:**
```json
{
"error": "Brief error message",
"details": "Optional detailed error information"
}
```
**HTTP Status Codes:**
| Code | Description |
|------|-------------|
| 200 | Success |
| 201 | Created |
| 204 | No Content (successful DELETE) |
| 400 | Bad Request (validation error) |
| 404 | Not Found |
| 409 | Conflict (e.g., duplicate name) |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
---
## Health
### `GET /api/health`
@@ -29,6 +58,21 @@ List all items with optional filtering.
### `GET /api/items/:id`
Get single item by ID.
**Response:**
```json
{
"id": 1,
"name": "Health Potion",
"description": "Restores HP",
"type": "CONSUMABLE",
"rarity": "C",
"price": "100",
"iconUrl": "/assets/items/1.png",
"imageUrl": "/assets/items/1.png",
"usageData": { "consume": true, "effects": [] }
}
```
### `POST /api/items`
Create new item. Supports JSON or multipart/form-data with image.
@@ -40,10 +84,16 @@ Create new item. Supports JSON or multipart/form-data with image.
"type": "CONSUMABLE",
"rarity": "C",
"price": "100",
"iconUrl": "/assets/items/placeholder.png",
"imageUrl": "/assets/items/placeholder.png",
"usageData": { "consume": true, "effects": [] }
}
```
**Body (Multipart):**
- `data`: JSON string with item fields
- `image`: Image file (PNG, JPEG, WebP, GIF, max 15MB)
### `PUT /api/items/:id`
Update existing item.
@@ -55,11 +105,273 @@ Upload/replace item image. Accepts multipart/form-data with `image` field.
---
## Users
### `GET /api/users`
List all users with optional filtering and sorting.
| Query Param | Type | Description |
|-------------|------|-------------|
| `search` | string | Filter by username (partial match) |
| `sortBy` | string | Sort field: `balance`, `level`, `xp`, `username` (default: `balance`) |
| `sortOrder` | string | Sort order: `asc`, `desc` (default: `desc`) |
| `limit` | number | Max results (default: 50) |
| `offset` | number | Pagination offset |
**Response:** `{ "users": [...], "total": number }`
### `GET /api/users/:id`
Get single user by Discord ID.
**Response:**
```json
{
"id": "123456789012345678",
"username": "Player1",
"balance": "1000",
"xp": "500",
"level": 5,
"dailyStreak": 3,
"isActive": true,
"classId": "1",
"class": { "id": "1", "name": "Warrior", "balance": "5000" },
"settings": {},
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-15T12:00:00Z"
}
```
### `PUT /api/users/:id`
Update user fields.
**Body:**
```json
{
"username": "NewName",
"balance": "2000",
"xp": "750",
"level": 10,
"dailyStreak": 5,
"classId": "1",
"isActive": true,
"settings": {}
}
```
### `GET /api/users/:id/inventory`
Get user's inventory with item details.
**Response:**
```json
{
"inventory": [
{
"userId": "123456789012345678",
"itemId": 1,
"quantity": "5",
"item": { "id": 1, "name": "Health Potion", ... }
}
]
}
```
### `POST /api/users/:id/inventory`
Add item to user inventory.
**Body:**
```json
{
"itemId": 1,
"quantity": "5"
}
```
### `DELETE /api/users/:id/inventory/:itemId`
Remove item from user inventory. Use query param `amount` to specify quantity (default: 1).
| Query Param | Type | Description |
|-------------|------|-------------|
| `amount` | number | Amount to remove (default: 1) |
```
---
## Classes
### `GET /api/classes`
List all classes.
**Response:**
```json
{
"classes": [
{ "id": "1", "name": "Warrior", "balance": "5000", "roleId": "123456789" }
]
}
```
### `POST /api/classes`
Create new class.
**Body:**
```json
{
"id": "2",
"name": "Mage",
"balance": "0",
"roleId": "987654321"
}
```
### `PUT /api/classes/:id`
Update class.
**Body:**
```json
{
"name": "Updated Name",
"balance": "10000",
"roleId": "111222333"
}
```
### `DELETE /api/classes/:id`
Delete class.
---
## Moderation
### `GET /api/moderation`
List moderation cases with optional filtering.
| Query Param | Type | Description |
|-------------|------|-------------|
| `userId` | string | Filter by target user ID |
| `moderatorId` | string | Filter by moderator ID |
| `type` | string | Filter by case type: `warn`, `timeout`, `kick`, `ban`, `note`, `prune` |
| `active` | boolean | Filter by active status |
| `limit` | number | Max results (default: 50) |
| `offset` | number | Pagination offset |
**Response:**
```json
{
"cases": [
{
"id": "1",
"caseId": "CASE-0001",
"type": "warn",
"userId": "123456789",
"username": "User1",
"moderatorId": "987654321",
"moderatorName": "Mod1",
"reason": "Spam",
"metadata": {},
"active": true,
"createdAt": "2024-01-15T12:00:00Z",
"resolvedAt": null,
"resolvedBy": null,
"resolvedReason": null
}
]
}
```
### `GET /api/moderation/:caseId`
Get single case by case ID (e.g., `CASE-0001`).
### `POST /api/moderation`
Create new moderation case.
**Body:**
```json
{
"type": "warn",
"userId": "123456789",
"username": "User1",
"moderatorId": "987654321",
"moderatorName": "Mod1",
"reason": "Rule violation",
"metadata": { "duration": "24h" }
}
```
### `PUT /api/moderation/:caseId/clear`
Clear/resolve a moderation case.
**Body:**
```json
{
"clearedBy": "987654321",
"clearedByName": "Mod1",
"reason": "Appeal accepted"
}
```
---
## Transactions
### `GET /api/transactions`
List economy transactions.
| Query Param | Type | Description |
|-------------|------|-------------|
| `userId` | string | Filter by user ID |
| `type` | string | Filter by transaction type |
| `limit` | number | Max results (default: 50) |
| `offset` | number | Pagination offset |
**Response:**
```json
{
"transactions": [
{
"id": "1",
"userId": "123456789",
"relatedUserId": null,
"amount": "100",
"type": "DAILY_REWARD",
"description": "Daily reward (Streak: 3)",
"createdAt": "2024-01-15T12:00:00Z"
}
]
}
```
**Transaction Types:**
- `DAILY_REWARD` - Daily claim reward
- `TRANSFER_IN` - Received from another user
- `TRANSFER_OUT` - Sent to another user
- `LOOTDROP_CLAIM` - Claimed lootdrop
- `SHOP_BUY` - Item purchase
- `QUEST_REWARD` - Quest completion reward
---
## Quests
### `GET /api/quests`
List all quests.
**Response:**
```json
{
"success": true,
"data": [
{
"id": 1,
"name": "Daily Login",
"description": "Login once",
"triggerEvent": "login",
"requirements": { "target": 1 },
"rewards": { "xp": 50, "balance": 100 }
}
]
}
```
### `POST /api/quests`
Create new quest.
@@ -94,6 +406,15 @@ Update configuration (partial merge supported).
### `GET /api/settings/meta`
Get Discord metadata (roles, channels, commands).
**Response:**
```json
{
"roles": [{ "id": "123", "name": "Admin", "color": "#FF0000" }],
"channels": [{ "id": "456", "name": "general", "type": 0 }],
"commands": [{ "name": "daily", "category": "economy" }]
}
```
---
## Admin Actions

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 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
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, {
...(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 });
},