step 8.2: done
This commit is contained in:
@@ -10,6 +10,7 @@ const { routes: inventoryRoutes } = require('./routes/inventory');
|
||||
const { routes: shopRoutes } = require('./routes/shop');
|
||||
const { routes: expeditionRoutes } = require('./routes/expeditions');
|
||||
const { routes: leaderboardRoutes } = require('./routes/leaderboard');
|
||||
const { routes: heroProfileRoutes } = require('./routes/heroProfile');
|
||||
const { AppError } = require('./utils/errors');
|
||||
|
||||
function createApp() {
|
||||
@@ -76,6 +77,9 @@ function createApp() {
|
||||
// Leaderboard route (require session)
|
||||
app.get('/api/leaderboard', requireAuth, leaderboardRoutes.getLeaderboard);
|
||||
|
||||
// Hero profile route (public, no session required)
|
||||
app.get('/api/heroes/:name', heroProfileRoutes.getHeroProfile);
|
||||
|
||||
// Central error handler
|
||||
app.use((err, _req, res, _next) => {
|
||||
// Log stack traces server-side only
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Hero profile routes: GET /api/heroes/:name (public).
|
||||
const zod = require('zod');
|
||||
const { AppError } = require('../utils/errors');
|
||||
const { getHeroProfile } = require('../services/heroProfileService');
|
||||
|
||||
function wrap(fn) {
|
||||
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/heroes/:name — public hero profile.
|
||||
*/
|
||||
async function handleHeroProfile(req, res) {
|
||||
const parsed = zod.strictObject({
|
||||
name: zod.string().min(1).trim(),
|
||||
}).safeParse(req.params);
|
||||
if (!parsed.success) {
|
||||
throw new AppError('VALIDATION_ERROR', 'Request validation failed.', 400);
|
||||
}
|
||||
|
||||
const profile = getHeroProfile(parsed.data.name);
|
||||
if (!profile) {
|
||||
throw new AppError('NOT_FOUND', 'Hero not found.', 404);
|
||||
}
|
||||
|
||||
res.json({ ok: true, data: profile });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
routes: {
|
||||
getHeroProfile: wrap(handleHeroProfile),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
// Hero profile service: public hero lookup (no private data).
|
||||
const { getDb } = require('../db');
|
||||
const { AppError } = require('../utils/errors');
|
||||
|
||||
// Prepared statements cached per DB handle (WeakMap).
|
||||
const stmtCache = new WeakMap();
|
||||
|
||||
function getStmts(db) {
|
||||
let s = stmtCache.get(db);
|
||||
if (!s) {
|
||||
s = {
|
||||
// Find character by name (case-insensitive).
|
||||
findByName: db.prepare(
|
||||
`SELECT c.id, c.name, c.class, c.level, c.xp, c.gold,
|
||||
c.str, c.agi, c.int, c.vit, c.kills, c.deaths
|
||||
FROM characters c
|
||||
WHERE c.name = ? COLLATE NOCASE`
|
||||
),
|
||||
|
||||
// Get equipped items for a character.
|
||||
equippedItems: db.prepare(
|
||||
`SELECT it.name, i.equipped_slot
|
||||
FROM inventory i
|
||||
JOIN items it ON it.id = i.item_id
|
||||
WHERE i.character_id = ? AND i.equipped_slot IS NOT NULL`
|
||||
),
|
||||
};
|
||||
stmtCache.set(db, s);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a public hero profile by name.
|
||||
* @param {string} heroName - hero name (case-insensitive)
|
||||
* @returns {object} { name, class, level, xp, gold, str, agi, int, vit, kills, deaths, equipment: { weapon, armor, trinket } }
|
||||
*/
|
||||
function getHeroProfile(heroName) {
|
||||
const db = getDb();
|
||||
const s = getStmts(db);
|
||||
|
||||
const character = s.findByName.get(heroName);
|
||||
if (!character) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rows = s.equippedItems.all(character.id);
|
||||
|
||||
const equipment = { weapon: null, armor: null, trinket: null };
|
||||
for (const row of rows) {
|
||||
if (row.equipped_slot in equipment) {
|
||||
equipment[row.equipped_slot] = row.name;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: character.name,
|
||||
class: character.class,
|
||||
level: character.level,
|
||||
xp: character.xp,
|
||||
gold: character.gold,
|
||||
str: character.str,
|
||||
agi: character.agi,
|
||||
int: character.int,
|
||||
vit: character.vit,
|
||||
kills: character.kills,
|
||||
deaths: character.deaths,
|
||||
equipment,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { getHeroProfile };
|
||||
Reference in New Issue
Block a user