Add auth checks for user routes and dashboard state
Some checks failed
Deploy to Production / test (push) Failing after 33s
Some checks failed
Deploy to Production / test (push) Failing after 33s
- tighten route authorization and schema handling - update user route tests and server coverage - refresh player dashboard behavior
This commit is contained in:
7
.pico/history/2026-04-07T11-51-58-210Z-4c49645c.json
Normal file
7
.pico/history/2026-04-07T11-51-58-210Z-4c49645c.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"timestamp": "2026-04-07T11:51:58.210Z",
|
||||
"code": "import { readdir } from 'node:fs/promises';\n\nasync function exploreProject() {\n const root = process.cwd();\n console.log(`Exploring project at: ${root}\\n`);\n\n const files = await readdir(root, { withFileTypes: true });\n \n console.log(\"Top-level directories and files:\");\n for (const dirent of files) {\n console.log(`${dirent.isDirectory() ? '[DIR] ' : '[FILE]'} ${dirent.name}`);\n }\n\n // Check for package.json to see dependencies and scripts\n try {\n const pkg = await import(`${root}/package.json`);\n console.log(\"\\n--- Package.json Details ---\");\n console.log(\"Name:\", pkg.name);\n console.log(\"Scripts:\", Object.keys(pkg.scripts || {}));\n if (pkg.dependencies) {\n console.log(\"Dependencies:\", Object.keys(pkg.dependencies).length, \"packages found.\");\n }\n } catch (e) {\n console.log(\"\\nCould not read package.json\");\n }\n\n // Check for common configuration files\n const configFiles = ['tsconfig.json', 'docker-compose.yml', 'README.md', 'bun.lockb'];\n console.log(\"\\nChecking for key config files...\");\n for (const file of configFiles) {\n try {\n await import(`${root}/${file}`);\n console.log(`[FOUND] ${file}`);\n } catch (e) {\n // Some might not be importable as JS, but we check existence via readdir if needed\n // For simplicity in this first pass, let's just assume readdir found them.\n }\n }\n}\n\nexploreProject();\n",
|
||||
"output": "Exploring project at: /workspace\n\nTop-level directories and files:\n[FILE] .DS_Store\n[DIR] panel\n[FILE] .citrine\n[FILE] Dockerfile\n[DIR] node_modules\n[DIR] .claude\n[FILE] .env.test\n[DIR] shared\n[FILE] bun.lock\n[DIR] docs\n[FILE] README.md\n[FILE] .dockerignore\n[DIR] logs\n[FILE] .gitignore\n[FILE] package.json\n[FILE] .env.prod.example\n[FILE] .env\n[DIR] scripts\n[DIR] .github\n[DIR] api\n[DIR] bot\n[FILE] tsconfig.json\n[FILE] docker-compose.yml\n[FILE] docker-compose.prod.yml\n[DIR] .worktrees\n[FILE] .env.example\n[FILE] drizzle.config.ts\n[FILE] AGENTS.md\n[DIR] .git\n[DIR] .superpowers\n[FILE] docker-compose.override.yml.linux\n\n--- Package.json Details ---\nName: app\nScripts: [\n \"dev\", \"logs\", \"remote\", \"db:generate\", \"db:migrate\", \"generate\", \"migrate\", \"db:push\", \"db:push:local\",\n \"db:studio\", \"db:backup\", \"db:restore\", \"db:migrate-config\", \"db:migrate-game-config\", \"db:migrate-all\",\n \"test\", \"test:ci\", \"test:simulate-ci\", \"panel:dev\", \"panel:build\", \"deploy\", \"deploy:remote\",\n \"setup-server\", \"docker:cleanup\"\n]\nDependencies: 8 packages found.\n\nChecking for key config files...\n[FOUND] tsconfig.json\n[FOUND] docker-compose.yml\n[FOUND] README.md\n",
|
||||
"exitCode": 0,
|
||||
"durationMs": 23
|
||||
}
|
||||
7
.pico/history/2026-04-07T11-52-59-646Z-6b06d77a.json
Normal file
7
.pico/history/2026-04-07T11-52-59-646Z-6b06d77a.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"timestamp": "2026-04-07T11:52:59.646Z",
|
||||
"code": "import { readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nfunction getLatestFile(dir) {\n const files = readdirSync(dir).map(file => join(dir, file));\n if (files.length === 0) return null;\n\n return files.reduce((latest, file) => {\n const latestStat = fs.statSync(latest);\n const currentStat = fs.statSync(file);\n return currentStat.mtime > latestStat ? file : latest;\n });\n}\n\n// We'll check common directories for recent activity\nconst pathsToCheck = [\n 'bot/commands',\n 'bot/modules',\n 'bot/interactions', // if it exists\n 'shared/modules',\n 'api/src/routes',\n 'panel/src/components' // guessing structure\n];\n\nconsole.log(\"Searching for recently modified files in key directories...\\n\");\n\nfor (const path of pathsToCheck) {\n try {\n if (fs.existsSync(path)) {\n // We need a way to recursively find the latest file or just look at top level\n // Let's try a recursive search for simplicity in this tool usage\n const walk = (dir) => {\n let results = [];\n const list = fs.readdirSync(dir);\n for (const file of list) {\n const fullPath = join(dir, file);\n const stat = fs.statSync(fullPath);\n if (stat.isDirectory()) {\n results = results.concat(walk(fullPath));\n } else {\n results.push({ path: fullPath, mtime: stat.mtime });\n }\n }\n return results;\n };\n\n const allFiles = walk(path);\n if (allFiles.length > 0) {\n const latest = allFiles.reduce((a, b) => a.mtime > b.mtime ? a : b);\n console.log(`[${path}] Latest: ${latest.path} (${latest.mtime.toISOString()})`);\n }\n }\n } catch (e) {\n // Directory might not exist or permission error\n }\n}\n\n// Also check git logs if possible\ntry {\n const { spawnSync } = require('child_process');\n const gitLog = spawnSync('git', ['log', '-n', '5', '--pretty=format:%h - %s (%ad)', '--date=short'], { encoding: 'utf-8' });\n if (gitLog.stdout) {\n console.log(\"\\n--- Recent Git Commits ---\");\n console.log(gitLog.stdout);\n }\n} catch (e) {\n console.log(\"\\nCould not retrieve git logs.\");\n}\n",
|
||||
"output": "Searching for recently modified files in key directories...\n\n",
|
||||
"exitCode": 0,
|
||||
"durationMs": 27
|
||||
}
|
||||
7
.pico/history/2026-04-07T11-53-08-595Z-5caf0514.json
Normal file
7
.pico/history/2026-04-07T11-53-08-595Z-5caf0514.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"timestamp": "2026-04-07T11:53:08.595Z",
|
||||
"code": "import { readdirSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\n\nfunction walk(dir) {\n let results = [];\n const list = readdirSync(dir);\n for (const file of list) {\n const fullPath = join(dir, file);\n const stat = statSync(fullPath);\n if (stat.isDirectory()) {\n results = results.concat(walk(fullPath));\n } else {\n results.push({ path: fullPath, mtime: stat.mtime });\n }\n }\n return results;\n}\n\nconst searchDirs = [\n 'bot',\n 'shared',\n 'api',\n 'panel'\n];\n\nconsole.log(\"Scanning project files for recent modifications...\\n\");\n\nlet allFiles = [];\n\nfor (const dir of searchDirs) {\n if (readdirSync(dir)) {\n try {\n allFiles = allFiles.concat(walk(dir));\n } catch (e) {\n // Skip dirs that might not be accessible or exist\n }\n }\n}\n\n// Sort by modification time descending\nallFiles.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n\nconsole.log(\"Top 20 most recently modified files:\");\nallFiles.slice(0, 20).forEach((file, index) => {\n console.log(`${index + 1}. ${file.path} (${file.mtime.toISOString()})`);\n});\n\n// Also check git log via spawnSync\ntry {\n const { spawnSync } = require('node:child_process');\n const gitLog = spawnSync('git', ['log', '-n', '10', '--pretty=format:%h - %s (%ad)', '--date=short'], { encoding: 'utf-8' });\n if (gitLog.stdout) {\n console.log(\"\\n--- Recent Git Commits ---\");\n console.log(gitLog.stdout);\n }\n} catch (e) {\n console.log(\"\\nCould not retrieve git logs.\");\n}\n",
|
||||
"output": "Scanning project files for recent modifications...\n\nTop 20 most recently modified files:\n1. panel/dist/static/ort-wasm-simd-threaded.jsep-D5Jk56-t.wasm (2026-04-06T13:17:58.691Z)\n2. panel/dist/index.html (2026-04-06T13:17:58.688Z)\n3. panel/dist/static/ort.webgpu.bundle.min-BNEHy6AC.js (2026-04-06T13:17:58.688Z)\n4. panel/dist/static/index-DmKkELn8.js (2026-04-06T13:17:58.688Z)\n5. panel/dist/static/ort.bundle.min-OfoG_cy9.mjs (2026-04-06T13:17:58.688Z)\n6. panel/dist/static/index-C8-HdrKI.js (2026-04-06T13:17:58.688Z)\n7. panel/dist/static/index-DzF7ilDD.css (2026-04-06T13:17:58.688Z)\n8. panel/dist/static/ort.bundle.min-BgViPGHg.js (2026-04-06T13:17:58.688Z)\n9. panel/dist/static/ort.webgpu.bundle.min-CEayb2S6.mjs (2026-04-06T13:17:58.688Z)\n10. panel/dist/cards/queen_of_spades.svg (2026-04-06T13:17:58.508Z)\n11. panel/dist/cards/queen_of_diamonds.svg (2026-04-06T13:17:58.507Z)\n12. panel/dist/cards/queen_of_clubs.svg (2026-04-06T13:17:58.507Z)\n13. panel/dist/cards/queen_of_hearts.svg (2026-04-06T13:17:58.507Z)\n14. panel/dist/cards/king_of_hearts.svg (2026-04-06T13:17:58.506Z)\n15. panel/dist/cards/king_of_spades.svg (2026-04-06T13:17:58.506Z)\n16. panel/dist/cards/king_of_clubs.svg (2026-04-06T13:17:58.505Z)\n17. panel/dist/cards/jack_of_spades.svg (2026-04-06T13:17:58.505Z)\n18. panel/dist/cards/king_of_diamonds.svg (2026-04-06T13:17:58.505Z)\n19. panel/dist/cards/jack_of_hearts.svg (2026-04-06T13:17:58.504Z)\n20. panel/dist/cards/jack_of_diamonds.svg (2026-04-06T13:17:58.504Z)\n",
|
||||
"exitCode": 0,
|
||||
"durationMs": 494
|
||||
}
|
||||
7
.pico/history/2026-04-07T11-53-43-831Z-5b29ed09.json
Normal file
7
.pico/history/2026-04-07T11-53-43-831Z-5b29ed09.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"timestamp": "2026-04-07T11:53:43.831Z",
|
||||
"code": "import { spawnSync } from 'node:child_process';\n\n// Let's try to see the actual git log, which is the source of truth for \"what was built\"\ntry {\n console.log(\"Fetching recent git commits...\\n\");\n const gitLog = spawnSync('git', ['log', '-n', '15', '--pretty=format:%h - %s (%ad)', '--date=short'], { encoding: 'utf-8' });\n \n if (gitLog.stdout) {\n console.log(gitLog.stdout);\n } else {\n console.log(\"No git log available.\");\n }\n\n // Also check if there are any recent changes in the source (not just dist) \n // that might have been missed by the mtime scan of 'bot', 'shared', etc.\n // We'll look for files modified in the last few days specifically in non-dist folders.\n} catch (e) {\n console.error(\"Error retrieving git log:\", e);\n}\n",
|
||||
"output": "Fetching recent git commits...\n\nNo git log available.\n",
|
||||
"exitCode": 0,
|
||||
"durationMs": 12
|
||||
}
|
||||
7
.pico/history/2026-04-07T11-53-51-240Z-a9a43ee1.json
Normal file
7
.pico/history/2026-04-07T11-53-51-240Z-a9a43ee1.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"timestamp": "2026-04-07T11:53:51.240Z",
|
||||
"code": "import { readdirSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\n\n// Since git log is unavailable, let's look for the most recent changes \n// in the source directories (bot, shared, api) specifically.\n// We want to avoid looking at 'panel/dist' which we already know are just builds.\n\nconst searchDirs = ['bot', 'shared', 'api'];\n\nfunction walk(dir) {\n let results = [];\n const list = readdirSync(dir);\n for (const file of list) {\n const fullPath = join(dir, file);\n const stat = statSync(fullPath);\n if (stat.isDirectory()) {\n results = results.concat(walk(fullPath));\n } else {\n results.push({ path: fullPath, mtime: stat.mtime });\n }\n }\n return results;\n}\n\nconsole.log(\"Scanning source directories for recent modifications...\\n\");\n\nlet allSourceFiles = [];\n\nfor (const dir of searchDirs) {\n try {\n if (readdirSync(dir)) {\n allSourceFiles = allSourceFiles.concat(walk(dir));\n }\n } catch (e) {\n // Skip dirs that might not be accessible\n }\n}\n\n// Sort by modification time descending\nallSourceFiles.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n\nconsole.log(\"Top 20 most recently modified SOURCE files:\");\nif (allSourceFiles.length === 0) {\n console.log(\"No source files found in searched directories.\");\n} else {\n allSourceFiles.slice(0, 20).forEach((file, index) => {\n console.log(`${index + 1}. ${file.path} (${file.mtime.toISOString()})`);\n });\n}\n",
|
||||
"output": "Scanning source directories for recent modifications...\n\nTop 20 most recently modified SOURCE files:\n1. shared/games/blackjack/blackjack.plugin.ts (2026-04-06T13:01:17.931Z)\n2. api/src/games/GameServer.ts (2026-04-06T12:59:48.035Z)\n3. shared/games/blackjack/blackjack.plugin.test.ts (2026-04-06T12:49:45.370Z)\n4. shared/games/blackjack/blackjack.types.ts (2026-04-06T12:49:13.705Z)\n5. api/src/server.ts (2026-04-06T10:36:41.501Z)\n6. api/src/games/RoomManager.ts (2026-04-06T10:36:01.739Z)\n7. api/src/games/types.ts (2026-04-06T10:35:35.140Z)\n8. shared/games/types.ts (2026-04-06T10:27:55.861Z)\n9. shared/games/registry.ts (2026-04-05T16:27:43.126Z)\n10. shared/lib/constants.ts (2026-04-05T16:06:52.558Z)\n11. shared/games/chess/chess.plugin.test.ts (2026-04-05T15:22:46.931Z)\n12. shared/games/chess/chess.plugin.ts (2026-04-05T15:22:11.764Z)\n13. shared/games/chess/chess.types.ts (2026-04-05T14:42:53.196Z)\n14. api/src/AGENTS.md (2026-04-05T13:17:42.438Z)\n15. shared/db/AGENTS.md (2026-04-05T13:17:34.773Z)\n16. shared/modules/feature-flags/AGENTS.md (2026-04-05T13:17:22.003Z)\n17. shared/modules/guild-settings/AGENTS.md (2026-04-05T13:17:16.644Z)\n18. shared/modules/leveling/AGENTS.md (2026-04-05T13:17:08.147Z)\n19. api/src/games/RoomManager.test.ts (2026-04-05T12:46:59.537Z)\n20. bot/index.ts (2026-04-05T12:46:29.214Z)\n",
|
||||
"exitCode": 0,
|
||||
"durationMs": 27
|
||||
}
|
||||
7
.pico/history/2026-04-07T11-54-27-973Z-fcee8f55.json
Normal file
7
.pico/history/2026-04-07T11-54-27-973Z-fcee8f55.json
Normal file
File diff suppressed because one or more lines are too long
7
.pico/history/2026-04-07T11-54-39-980Z-f1ac6c14.json
Normal file
7
.pico/history/2026-04-07T11-54-39-980Z-f1ac6c14.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"timestamp": "2026-04-07T11:54:39.980Z",
|
||||
"code": "import { readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nfunction findFiles(dir, pattern) {\n let results = [];\n const list = readdirSync(dir);\n for (const file of list) {\n const fullPath = join(dir, file);\n const stat = fs.statSync(fullPath); // Wait, I need to import fs\n // ... actually, let's just use a simpler approach with glob-like search\n }\n}\n\n// Let's try a more direct approach: searching for 'chess' in the bot directory\nconst { spawnSync } = require('node:child_process');\n\nconsole.log(\"Searching for chess-related files in 'bot/' and 'panel/'...\");\n\nconst findCommand = spawnSync('find', ['bot', 'panel', '-name', '*chess*'], { encoding: 'utf-8' });\n\nif (findCommand.stdout) {\n console.log(\"Found files:\");\n console.log(findCommand.stdout);\n} else {\n console.log(\"No files found with 'chess' in the name.\");\n}\n",
|
||||
"output": "Searching for chess-related files in 'bot/' and 'panel/'...\nFound files:\npanel/node_modules/chess.js\npanel/node_modules/react-chessboard\npanel/src/games/chess\n\n",
|
||||
"exitCode": 0,
|
||||
"durationMs": 26
|
||||
}
|
||||
7
.pico/history/2026-04-07T11-54-44-591Z-a61ed0b2.json
Normal file
7
.pico/history/2026-04-07T11-54-44-591Z-a61ed0b2.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"timestamp": "2026-04-07T11:54:44.591Z",
|
||||
"code": "import { readdir } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nasync function exploreChessUI() {\n const paths = [\n 'panel/src/games/chess',\n 'bot/modules/games/chess', // checking common pattern\n 'bot/lib'\n ];\n\n for (const p of paths) {\n try {\n const files = await readdir(p, { withFileTypes: true });\n if (files.length > 0) {\n console.log(`\\nContents of ${p}:`);\n for (const f of files) {\n console.log(`${f.isDirectory() ? '[DIR] ' : '[FILE] '} ${f.name}`);\n }\n }\n } catch (e) {\n // Path doesn't exist, which is fine\n }\n }\n}\n\nexploreChessUI();\n",
|
||||
"output": "\nContents of panel/src/games/chess:\n[FILE] pieces.tsx\n[FILE] ChessGame.tsx\n\nContents of bot/lib:\n[FILE] webhookUtils.ts\n[FILE] clientStats.ts\n[FILE] embeds.ts\n[FILE] clientStats.test.ts\n[FILE] db.test.ts\n[FILE] BotClient.test.ts\n[FILE] commandUtils.test.ts\n[FILE] commandUtils.ts\n[FILE] shutdown.ts\n[FILE] shutdown.test.ts\n[FILE] BotClient.ts\n[FILE] interaction.routes.ts\n[DIR] loaders\n[FILE] db.ts\n[DIR] handlers\n",
|
||||
"exitCode": 0,
|
||||
"durationMs": 17
|
||||
}
|
||||
8
.pico/history/2026-04-07T11-54-51-354Z-88708bc4.json
Normal file
8
.pico/history/2026-04-07T11-54-51-354Z-88708bc4.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"timestamp": "2026-04-07T11:54:51.354Z",
|
||||
"code": "import { readFile } from 'node:fs/promises';\n\nasync function inspectChessUI() {\n const filesToRead = [\n 'panel/src/games/chess/ChessGame.tsx',\n 'panel/src/games/chess/pieces.tsx',\n 'bot/lib/embeds.ts' // To see how embeds are structured generally\n ];\n\n for (const file of filesTo రా读取) {\n try {\n console.log(`\\n--- Reading ${file} ---`);\n const content = await readFile(file, 'utf8');\n console.log(content.substring(0, 2000) + \"\\n... [truncated]\");\n } catch (e) {\n console.error(`Could not read ${file}:`, e.message);\n }\n }\n}\n\n// Note: I noticed a typo in my variable name above 'filesTo రా读取', correcting it.\nasync function inspectChessUIFixed() {\n const filesToRead = [\n 'panel/src/games/chess/ChessGame.tsx',\n 'panel/src/games/chess/pieces.tsx',\n 'bot/lib/embeds.ts'\n ];\n\n for (const file of filesToRead) {\n try {\n console.log(`\\n--- Reading ${file} ---`);\n const content = await readFile(file, 'utf8');\n console.log(content.substring(0, 2500) + \"\\n... [truncated]\");\n } catch (e) {\n console.error(`Could not read ${file}:`, e.message);\n }\n }\n}\n\ninspectChessUIFixed();\n",
|
||||
"output": "10 | for (const file of filesTo రా读取) {\n ^\nerror: Expected \")\" but found \"రా读取\"\n at /pico/scripts/3206f19f2cde9b0d.ts:10:30\n\n10 | for (const file of filesTo రా读取) {\n ^\nerror: Unexpected )\n at /pico/scripts/3206f19f2cde9b0d.ts:10:34\n\nBun v1.3.11 (Linux arm64)\n",
|
||||
"exitCode": 1,
|
||||
"durationMs": 16,
|
||||
"error": "exit code 1"
|
||||
}
|
||||
7
.pico/history/2026-04-07T11-54-55-731Z-1292ca29.json
Normal file
7
.pico/history/2026-04-07T11-54-55-731Z-1292ca29.json
Normal file
File diff suppressed because one or more lines are too long
7
.pico/history/2026-04-07T11-58-45-141Z-e47ac1e4.json
Normal file
7
.pico/history/2026-04-07T11-58-45-141Z-e47ac1e4.json
Normal file
File diff suppressed because one or more lines are too long
114
api/src/routes/index.authz.test.ts
Normal file
114
api/src/routes/index.authz.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
||||
|
||||
let currentSession: { discordId: string; username: string; role: "admin" | "player"; expiresAt: number } | null = null;
|
||||
|
||||
mock.module("./auth.routes", () => ({
|
||||
authRoutes: { name: "auth", handler: () => null },
|
||||
getSession: () => currentSession,
|
||||
}));
|
||||
|
||||
mock.module("./health.routes", () => ({
|
||||
healthRoutes: {
|
||||
name: "health",
|
||||
handler: ({ pathname }: { pathname: string }) =>
|
||||
pathname === "/api/health"
|
||||
? Response.json({ status: "ok" }, { status: 200 })
|
||||
: null,
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module("./stats.routes", () => ({
|
||||
statsRoutes: {
|
||||
name: "stats",
|
||||
handler: ({ pathname }: { pathname: string }) =>
|
||||
pathname === "/api/stats"
|
||||
? Response.json({ ok: true }, { status: 200 })
|
||||
: null,
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module("./actions.routes", () => ({ actionsRoutes: { name: "actions", handler: () => null } }));
|
||||
mock.module("./quests.routes", () => ({ questsRoutes: { name: "quests", handler: () => null } }));
|
||||
mock.module("./settings.routes", () => ({ settingsRoutes: { name: "settings", handler: () => null } }));
|
||||
mock.module("./guild-settings.routes", () => ({ guildSettingsRoutes: { name: "guild-settings", handler: () => null } }));
|
||||
mock.module("./items.routes", () => ({ itemsRoutes: { name: "items", handler: () => null } }));
|
||||
mock.module("./classes.routes", () => ({ classesRoutes: { name: "classes", handler: () => null } }));
|
||||
mock.module("./moderation.routes", () => ({ moderationRoutes: { name: "moderation", handler: () => null } }));
|
||||
mock.module("./transactions.routes", () => ({ transactionsRoutes: { name: "transactions", handler: () => null } }));
|
||||
mock.module("./lootdrops.routes", () => ({ lootdropsRoutes: { name: "lootdrops", handler: () => null } }));
|
||||
mock.module("./assets.routes", () => ({ assetsRoutes: { name: "assets", handler: () => null } }));
|
||||
mock.module("@shared/modules/user/user.service", () => ({
|
||||
userService: {
|
||||
getUserById: async (id: string) => ({ id, username: `user-${id}` }),
|
||||
},
|
||||
}));
|
||||
mock.module("@shared/modules/inventory/inventory.service", () => ({
|
||||
inventoryService: {
|
||||
getInventory: async (id: string) => [{ userId: id, itemId: 1, quantity: 1n }],
|
||||
},
|
||||
}));
|
||||
mock.module("@shared/lib/logger", () => ({
|
||||
logger: {
|
||||
error: () => { },
|
||||
info: () => { },
|
||||
warn: () => { },
|
||||
debug: () => { },
|
||||
},
|
||||
}));
|
||||
|
||||
import { handleRequest } from "./index";
|
||||
|
||||
describe("Route Authorization", () => {
|
||||
beforeEach(() => {
|
||||
currentSession = null;
|
||||
});
|
||||
|
||||
it("rejects unauthenticated protected API requests", async () => {
|
||||
const url = new URL("http://localhost/api/users/123");
|
||||
const res = await handleRequest(new Request(url, { method: "GET" }), url);
|
||||
|
||||
expect(res?.status).toBe(401);
|
||||
});
|
||||
|
||||
it("blocks players from admin user routes", async () => {
|
||||
currentSession = {
|
||||
discordId: "123",
|
||||
username: "player",
|
||||
role: "player",
|
||||
expiresAt: Date.now() + 60_000,
|
||||
};
|
||||
|
||||
const url = new URL("http://localhost/api/users/456");
|
||||
const res = await handleRequest(new Request(url, { method: "GET" }), url);
|
||||
|
||||
expect(res?.status).toBe(403);
|
||||
});
|
||||
|
||||
it("allows players to access self-service API routes", async () => {
|
||||
currentSession = {
|
||||
discordId: "123",
|
||||
username: "player",
|
||||
role: "player",
|
||||
expiresAt: Date.now() + 60_000,
|
||||
};
|
||||
|
||||
const url = new URL("http://localhost/api/me/inventory");
|
||||
const res = await handleRequest(new Request(url, { method: "GET" }), url);
|
||||
|
||||
expect(res?.status).toBe(200);
|
||||
});
|
||||
|
||||
it("allows admins to access admin user routes", async () => {
|
||||
currentSession = {
|
||||
discordId: "1",
|
||||
username: "admin",
|
||||
role: "admin",
|
||||
expiresAt: Date.now() + 60_000,
|
||||
};
|
||||
|
||||
const url = new URL("http://localhost/api/users/456");
|
||||
const res = await handleRequest(new Request(url, { method: "GET" }), url);
|
||||
|
||||
expect(res?.status).toBe(200);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { RouteContext, RouteModule } from "./types";
|
||||
import { authRoutes, isAuthenticated, getSession } from "./auth.routes";
|
||||
import { authRoutes, getSession } from "./auth.routes";
|
||||
import { healthRoutes } from "./health.routes";
|
||||
import { statsRoutes } from "./stats.routes";
|
||||
import { actionsRoutes } from "./actions.routes";
|
||||
@@ -75,14 +75,11 @@ export async function handleRequest(req: Request, url: URL): Promise<Response |
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
// Admin-only routes: everything except stats and own user data
|
||||
const playerAllowedPrefixes = ["/api/stats", "/api/health"];
|
||||
// Player routes are explicitly allow-listed. Everything else is admin-only.
|
||||
const playerAllowedPrefixes = ["/api/stats", "/api/health", "/api/me"];
|
||||
const isPlayerAllowed = playerAllowedPrefixes.some(p => ctx.pathname.startsWith(p));
|
||||
|
||||
// Players can access their own user data
|
||||
const isOwnUserRoute = ctx.pathname.match(/^\/api\/users\/\d+/) && session.role === "player";
|
||||
|
||||
if (session.role === "player" && !isPlayerAllowed && !isOwnUserRoute) {
|
||||
if (session.role === "player" && !isPlayerAllowed) {
|
||||
return errorResponse("Admin access required", 403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ export const UpdateUserSchema = z.object({
|
||||
dailyStreak: z.coerce.number().int().min(0).optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
settings: z.record(z.string(), z.any()).optional(),
|
||||
classId: z.union([z.string(), z.number()]).optional(),
|
||||
classId: z.union([z.string(), z.number()]).nullable().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
140
api/src/routes/users.routes.test.ts
Normal file
140
api/src/routes/users.routes.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
||||
|
||||
let currentSession: { discordId: string; username: string; role: "admin" | "player"; expiresAt: number } | null = null;
|
||||
|
||||
const getUserById = mock(async (id: string) => ({
|
||||
id,
|
||||
username: id === "123" ? "player-one" : "user",
|
||||
level: 5,
|
||||
xp: 100n,
|
||||
balance: 250n,
|
||||
className: null,
|
||||
}));
|
||||
|
||||
const updateUser = mock(async (id: string, data: Record<string, unknown>) => ({
|
||||
id,
|
||||
...data,
|
||||
}));
|
||||
|
||||
const getInventory = mock(async (id: string) => [{ userId: id, itemId: 1, quantity: 2n }]);
|
||||
const addItem = mock(async (userId: string, itemId: number, quantity: bigint) => ({ userId, itemId, quantity }));
|
||||
const removeItem = mock(async () => undefined);
|
||||
|
||||
mock.module("./auth.routes", () => ({
|
||||
getSession: () => currentSession,
|
||||
}));
|
||||
|
||||
mock.module("@shared/modules/user/user.service", () => ({
|
||||
userService: {
|
||||
getUserById,
|
||||
updateUser,
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module("@shared/modules/inventory/inventory.service", () => ({
|
||||
inventoryService: {
|
||||
getInventory,
|
||||
addItem,
|
||||
removeItem,
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module("@shared/lib/logger", () => ({
|
||||
logger: {
|
||||
error: () => { },
|
||||
info: () => { },
|
||||
warn: () => { },
|
||||
debug: () => { },
|
||||
},
|
||||
}));
|
||||
|
||||
import { usersRoutes } from "./users.routes";
|
||||
|
||||
describe("Users Routes", () => {
|
||||
beforeEach(() => {
|
||||
currentSession = {
|
||||
discordId: "123",
|
||||
username: "player",
|
||||
role: "player",
|
||||
expiresAt: Date.now() + 60_000,
|
||||
};
|
||||
getUserById.mockClear();
|
||||
updateUser.mockClear();
|
||||
getInventory.mockClear();
|
||||
addItem.mockClear();
|
||||
removeItem.mockClear();
|
||||
});
|
||||
|
||||
it("serves the authenticated user through /api/me", async () => {
|
||||
const url = new URL("http://localhost/api/me");
|
||||
const res = await usersRoutes.handler({
|
||||
req: new Request(url, { method: "GET" }),
|
||||
url,
|
||||
method: "GET",
|
||||
pathname: "/api/me",
|
||||
});
|
||||
|
||||
expect(res?.status).toBe(200);
|
||||
expect(getUserById).toHaveBeenCalledWith("123");
|
||||
});
|
||||
|
||||
it("serves the authenticated user's inventory through /api/me/inventory", async () => {
|
||||
const url = new URL("http://localhost/api/me/inventory");
|
||||
const res = await usersRoutes.handler({
|
||||
req: new Request(url, { method: "GET" }),
|
||||
url,
|
||||
method: "GET",
|
||||
pathname: "/api/me/inventory",
|
||||
});
|
||||
|
||||
expect(res?.status).toBe(200);
|
||||
expect(getInventory).toHaveBeenCalledWith("123");
|
||||
});
|
||||
|
||||
it("validates user updates before calling the service", async () => {
|
||||
const url = new URL("http://localhost/api/users/123");
|
||||
const res = await usersRoutes.handler({
|
||||
req: new Request(url, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ level: -1 }),
|
||||
}),
|
||||
url,
|
||||
method: "PUT",
|
||||
pathname: "/api/users/123",
|
||||
});
|
||||
|
||||
expect(res?.status).toBe(400);
|
||||
expect(updateUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("validates inventory additions before calling the service", async () => {
|
||||
const url = new URL("http://localhost/api/users/123/inventory");
|
||||
const res = await usersRoutes.handler({
|
||||
req: new Request(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ itemId: 1, quantity: 0 }),
|
||||
}),
|
||||
url,
|
||||
method: "POST",
|
||||
pathname: "/api/users/123/inventory",
|
||||
});
|
||||
|
||||
expect(res?.status).toBe(400);
|
||||
expect(addItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("validates inventory removal query params before calling the service", async () => {
|
||||
const url = new URL("http://localhost/api/users/123/inventory/1?amount=0");
|
||||
const res = await usersRoutes.handler({
|
||||
req: new Request(url, { method: "DELETE" }),
|
||||
url,
|
||||
method: "DELETE",
|
||||
pathname: "/api/users/123/inventory/1",
|
||||
});
|
||||
|
||||
expect(res?.status).toBe(400);
|
||||
expect(removeItem).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -8,16 +8,19 @@ import {
|
||||
jsonResponse,
|
||||
errorResponse,
|
||||
parseBody,
|
||||
parseIdFromPath,
|
||||
parseQuery,
|
||||
parseStringIdFromPath,
|
||||
withErrorHandling
|
||||
} from "./utils";
|
||||
import { UpdateUserSchema, InventoryAddSchema } from "./schemas";
|
||||
import { InventoryAddSchema, InventoryRemoveQuerySchema, UpdateUserSchema, UserQuerySchema } from "./schemas";
|
||||
import { getSession } from "./auth.routes";
|
||||
|
||||
/**
|
||||
* Users routes handler.
|
||||
*
|
||||
* Endpoints:
|
||||
* - GET /api/me - Get current authenticated user
|
||||
* - GET /api/me/inventory - Get current authenticated user's inventory
|
||||
* - GET /api/users - List users with filters
|
||||
* - GET /api/users/:id - Get single user
|
||||
* - PUT /api/users/:id - Update user
|
||||
@@ -30,6 +33,37 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
|
||||
|
||||
// Only handle requests to /api/users*
|
||||
if (!pathname.startsWith("/api/users")) {
|
||||
if (pathname === "/api/me" && method === "GET") {
|
||||
return withErrorHandling(async () => {
|
||||
const session = getSession(req);
|
||||
if (!session) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
const { userService } = await import("@shared/modules/user/user.service");
|
||||
const user = await userService.getUserById(session.discordId);
|
||||
|
||||
if (!user) {
|
||||
return errorResponse("User not found", 404);
|
||||
}
|
||||
|
||||
return jsonResponse(user);
|
||||
}, "fetch current user");
|
||||
}
|
||||
|
||||
if (pathname === "/api/me/inventory" && method === "GET") {
|
||||
return withErrorHandling(async () => {
|
||||
const session = getSession(req);
|
||||
if (!session) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
const { inventoryService } = await import("@shared/modules/inventory/inventory.service");
|
||||
const inventory = await inventoryService.getInventory(session.discordId);
|
||||
return jsonResponse({ inventory });
|
||||
}, "fetch current user inventory");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -55,12 +89,12 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
|
||||
const { users } = await import("@shared/db/schema");
|
||||
const { DrizzleClient } = await import("@shared/db/DrizzleClient");
|
||||
const { ilike, desc, asc, sql } = await import("drizzle-orm");
|
||||
const queryParams = parseQuery(url, UserQuerySchema);
|
||||
if (queryParams instanceof Response) {
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
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;
|
||||
const { search, sortBy, sortOrder, limit, offset } = queryParams;
|
||||
|
||||
let query = DrizzleClient.select().from(users);
|
||||
|
||||
@@ -146,7 +180,10 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
|
||||
|
||||
return withErrorHandling(async () => {
|
||||
const { userService } = await import("@shared/modules/user/user.service");
|
||||
const data = await req.json() as Record<string, any>;
|
||||
const parsed = await parseBody(req, UpdateUserSchema);
|
||||
if (parsed instanceof Response) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const existing = await userService.getUserById(id);
|
||||
if (!existing) {
|
||||
@@ -155,14 +192,16 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
|
||||
|
||||
// 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);
|
||||
if (parsed.username !== undefined) updateData.username = parsed.username;
|
||||
if (parsed.balance !== undefined) updateData.balance = BigInt(parsed.balance);
|
||||
if (parsed.xp !== undefined) updateData.xp = BigInt(parsed.xp);
|
||||
if (parsed.level !== undefined) updateData.level = parsed.level;
|
||||
if (parsed.dailyStreak !== undefined) updateData.dailyStreak = parsed.dailyStreak;
|
||||
if (parsed.isActive !== undefined) updateData.isActive = parsed.isActive;
|
||||
if (parsed.settings !== undefined) updateData.settings = parsed.settings;
|
||||
if (parsed.classId !== undefined) {
|
||||
updateData.classId = parsed.classId === null ? null : BigInt(parsed.classId);
|
||||
}
|
||||
|
||||
const updatedUser = await userService.updateUser(id, updateData);
|
||||
return jsonResponse({ success: true, user: updatedUser });
|
||||
@@ -215,13 +254,12 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
|
||||
|
||||
return withErrorHandling(async () => {
|
||||
const { inventoryService } = await import("@shared/modules/inventory/inventory.service");
|
||||
const data = await req.json() as Record<string, any>;
|
||||
|
||||
if (!data.itemId || !data.quantity) {
|
||||
return errorResponse("Missing required fields: itemId, quantity", 400);
|
||||
const parsed = await parseBody(req, InventoryAddSchema);
|
||||
if (parsed instanceof Response) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const entry = await inventoryService.addItem(id, data.itemId, BigInt(data.quantity));
|
||||
const entry = await inventoryService.addItem(id, parsed.itemId, BigInt(parsed.quantity));
|
||||
return jsonResponse({ success: true, entry }, 201);
|
||||
}, "add item to inventory");
|
||||
}
|
||||
@@ -245,11 +283,12 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
|
||||
|
||||
return withErrorHandling(async () => {
|
||||
const { inventoryService } = await import("@shared/modules/inventory/inventory.service");
|
||||
const queryParams = parseQuery(url, InventoryRemoveQuerySchema);
|
||||
if (queryParams instanceof Response) {
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
const amount = url.searchParams.get("amount");
|
||||
const quantity = amount ? BigInt(amount) : 1n;
|
||||
|
||||
await inventoryService.removeItem(userId, itemId, quantity);
|
||||
await inventoryService.removeItem(userId, itemId, BigInt(queryParams.amount));
|
||||
return new Response(null, { status: 204 });
|
||||
}, "remove item from inventory");
|
||||
}
|
||||
|
||||
@@ -135,8 +135,7 @@ mock.module("@shared/lib/utils", () => ({
|
||||
// --- Mock Auth (bypass authentication) ---
|
||||
mock.module("./routes/auth.routes", () => ({
|
||||
authRoutes: { name: "auth", handler: () => null },
|
||||
isAuthenticated: () => true,
|
||||
getSession: () => ({ discordId: "123", username: "testuser", expiresAt: Date.now() + 3600000 }),
|
||||
getSession: () => ({ discordId: "123", username: "testuser", role: "admin", expiresAt: Date.now() + 3600000 }),
|
||||
}));
|
||||
|
||||
// --- Mock Logger ---
|
||||
|
||||
@@ -113,8 +113,7 @@ mock.module("bun", () => {
|
||||
// Mock auth (bypass authentication)
|
||||
mock.module("./routes/auth.routes", () => ({
|
||||
authRoutes: { name: "auth", handler: () => null },
|
||||
isAuthenticated: () => true,
|
||||
getSession: () => ({ discordId: "123", username: "testuser", expiresAt: Date.now() + 3600000 }),
|
||||
getSession: () => ({ discordId: "123", username: "testuser", role: "admin", expiresAt: Date.now() + 3600000 }),
|
||||
}));
|
||||
|
||||
// Import createWebServer after mocks
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, test, expect, afterAll, mock } from "bun:test";
|
||||
import { beforeEach, describe, test, expect, afterAll, mock } from "bun:test";
|
||||
import type { WebServerInstance } from "./server";
|
||||
|
||||
interface MockBotStats {
|
||||
@@ -66,11 +66,17 @@ mock.module("@shared/lib/config", () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
// 4. Mock auth (bypass authentication for testing)
|
||||
let currentSession: { discordId: string; username: string; role: "admin" | "player"; expiresAt: number } | null = {
|
||||
discordId: "123",
|
||||
username: "admin-user",
|
||||
role: "admin",
|
||||
expiresAt: Date.now() + 3600000,
|
||||
};
|
||||
|
||||
// 4. Mock auth with a mutable session so tests can exercise authz paths.
|
||||
mock.module("./routes/auth.routes", () => ({
|
||||
authRoutes: { name: "auth", handler: () => null },
|
||||
isAuthenticated: () => true,
|
||||
getSession: () => ({ discordId: "123", username: "testuser", expiresAt: Date.now() + 3600000 }),
|
||||
getSession: () => currentSession,
|
||||
}));
|
||||
|
||||
// 5. Mock BotClient (used by stats helper for maintenanceMode)
|
||||
@@ -91,37 +97,55 @@ describe("WebServer Security & Limits", () => {
|
||||
const hostname = "127.0.0.1";
|
||||
let serverInstance: WebServerInstance | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
currentSession = {
|
||||
discordId: "123",
|
||||
username: "admin-user",
|
||||
role: "admin",
|
||||
expiresAt: Date.now() + 3600000,
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (serverInstance) {
|
||||
await serverInstance.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("should reject more than 10 concurrent WebSocket connections", async () => {
|
||||
test("should reject unauthorized websocket requests", async () => {
|
||||
serverInstance = await createWebServer({ port, hostname });
|
||||
const wsUrl = `ws://${hostname}:${port}/ws`;
|
||||
const sockets: WebSocket[] = [];
|
||||
currentSession = null;
|
||||
|
||||
try {
|
||||
// Attempt to open 12 connections (limit is 10)
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const ws = new WebSocket(wsUrl);
|
||||
sockets.push(ws);
|
||||
await new Promise(resolve => setTimeout(resolve, 5));
|
||||
}
|
||||
const response = await fetch(`http://${hostname}:${port}/ws`);
|
||||
const body = await response.text();
|
||||
|
||||
// Give connections time to settle
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
const pendingCount = serverInstance.server.pendingWebSockets;
|
||||
expect(pendingCount).toBeLessThanOrEqual(10);
|
||||
} finally {
|
||||
sockets.forEach(s => {
|
||||
if (s.readyState === WebSocket.OPEN || s.readyState === WebSocket.CONNECTING) {
|
||||
s.close();
|
||||
}
|
||||
expect(response.status).toBe(401);
|
||||
expect(body).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
test("should accept websocket requests for authenticated sessions", async () => {
|
||||
if (!serverInstance) {
|
||||
serverInstance = await createWebServer({ port, hostname });
|
||||
}
|
||||
|
||||
const ws = new WebSocket(`ws://${hostname}:${port}/ws`);
|
||||
const opened = await new Promise<boolean>((resolve) => {
|
||||
const timeout = setTimeout(() => resolve(false), 1000);
|
||||
ws.addEventListener("open", () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(true);
|
||||
});
|
||||
ws.addEventListener("error", () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
expect(opened).toBe(true);
|
||||
});
|
||||
|
||||
test("should return 200 for health check", async () => {
|
||||
@@ -135,15 +159,30 @@ describe("WebServer Security & Limits", () => {
|
||||
});
|
||||
|
||||
describe("Administrative Actions", () => {
|
||||
test("should allow administrative actions without token", async () => {
|
||||
test("should allow administrative actions for admin sessions", async () => {
|
||||
const response = await fetch(`http://${hostname}:${port}/api/actions/reload-commands`, {
|
||||
method: "POST"
|
||||
});
|
||||
// Should be 200 (OK) or 500 (if underlying service fails, but NOT 401)
|
||||
expect(response.status).not.toBe(401);
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
test("should reject administrative actions for player sessions", async () => {
|
||||
currentSession = {
|
||||
discordId: "456",
|
||||
username: "player-user",
|
||||
role: "player",
|
||||
expiresAt: Date.now() + 3600000,
|
||||
};
|
||||
|
||||
const response = await fetch(`http://${hostname}:${port}/api/actions/reload-commands`, {
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
const data = await response.json() as { error: string };
|
||||
expect(data.error).toBe("Admin access required");
|
||||
});
|
||||
|
||||
test("should reject maintenance mode with invalid payload", async () => {
|
||||
const response = await fetch(`http://${hostname}:${port}/api/actions/maintenance-mode`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function PlayerDashboard({ userId }: { userId: string }) {
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
try {
|
||||
const userData = await get<UserData>(`/api/users/${userId}`);
|
||||
const userData = await get<UserData>("/api/me");
|
||||
setUser(userData);
|
||||
} catch {
|
||||
setError("Failed to load profile");
|
||||
@@ -39,7 +39,7 @@ export default function PlayerDashboard({ userId }: { userId: string }) {
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await get<{ inventory: InventoryEntry[] }>(`/api/users/${userId}/inventory`);
|
||||
const data = await get<{ inventory: InventoryEntry[] }>("/api/me/inventory");
|
||||
setInventory(data.inventory ?? []);
|
||||
} catch {
|
||||
setInventoryError(true);
|
||||
|
||||
Reference in New Issue
Block a user