From 44784682aea9e34356990fb15ef28aad229f96d5 Mon Sep 17 00:00:00 2001 From: achmad Date: Fri, 29 May 2026 16:35:07 +0700 Subject: [PATCH] feat: add player handler with all endpoints --- backend/src/lib/handlers/player.ts | 172 +++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 backend/src/lib/handlers/player.ts diff --git a/backend/src/lib/handlers/player.ts b/backend/src/lib/handlers/player.ts new file mode 100644 index 0000000..83c74db --- /dev/null +++ b/backend/src/lib/handlers/player.ts @@ -0,0 +1,172 @@ +import { route, HandlerContext, HttpError } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +// POST /player — Create player profile +route('player', ['POST'], (ctx: HandlerContext) => { + const { steam_id, player_name } = ctx.body as any; + if (!steam_id) throw new HttpError(400, 'steam_id is required'); + const db = getDb(); + const existing = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(steam_id) as any; + if (existing) { + return existing; + } + db.prepare('INSERT INTO players (steam_id, player_name) VALUES (?, ?)').run(steam_id, player_name || ''); + try { + db.prepare('INSERT OR IGNORE INTO battle_passes (steam_id) VALUES (?)').run(steam_id); + } catch {} + const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(steam_id); + return player; +}); + +// GET /player/:steamId — Get player profile +// Returns the player row plus recentGames array and stats object +route('player/:steamId', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!player) throw new HttpError(404, 'Player not found'); + return { + ...player, + recentGames: [], + stats: { + total_games: 0, + total_wins: 0, + rating: 0, + }, + }; +}); + +// GET /player/:steamId/history — Match history with limit/offset +route('player/:steamId/history', ['GET'], (ctx: HandlerContext) => { + const limit = parseInt(ctx.searchParams.get('limit') || '10'); + const offset = parseInt(ctx.searchParams.get('offset') || '0'); + const db = getDb(); + const games = db.prepare( + 'SELECT * FROM game_history WHERE steam_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?' + ).all(ctx.params.steamId, limit, offset); + return games; +}); + +// GET /player/:steamId/currency — Get currency balances +route('player/:steamId/currency', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const player = db.prepare('SELECT free_currency, donate_currency, dust_currency FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!player) throw new HttpError(404, 'Player not found'); + return player; +}); + +// PUT /player/:steamId/currency — Save currency balances +route('player/:steamId/currency', ['PUT'], (ctx: HandlerContext) => { + const { free_currency, donate_currency, dust_currency } = ctx.body as any; + const db = getDb(); + const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!player) throw new HttpError(404, 'Player not found'); + db.prepare(` + UPDATE players SET free_currency = ?, donate_currency = ?, dust_currency = ?, updated_at = datetime('now') + WHERE steam_id = ? + `).run( + free_currency ?? player.free_currency, + donate_currency ?? player.donate_currency, + dust_currency ?? player.dust_currency, + ctx.params.steamId + ); + return { success: true }; +}); + +// POST /player/:steamId/currency/give — Grant currency (used by BP rewards) +route('player/:steamId/currency/give', ['POST'], (ctx: HandlerContext) => { + const body = ctx.body as any; + const free_amount = body.free_amount ?? body.freeAmount ?? 0; + const donate_amount = body.donate_amount ?? body.donateAmount ?? 0; + const dust_amount = body.dust_amount ?? body.dustAmount ?? 0; + const db = getDb(); + const player = db.prepare('SELECT * FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!player) throw new HttpError(404, 'Player not found'); + db.prepare(` + UPDATE players SET free_currency = free_currency + ?, donate_currency = donate_currency + ?, + dust_currency = dust_currency + ?, updated_at = datetime('now') WHERE steam_id = ? + `).run( + free_amount, + donate_amount, + dust_amount, + ctx.params.steamId + ); + return { success: true }; +}); + +// POST /player/:steamId/purchases — Record a store purchase +route('player/:steamId/purchases', ['POST'], (ctx: HandlerContext) => { + const { item_id, item_category, card_id, price_free, price_donate, price_dust } = ctx.body as any; + const db = getDb(); + db.prepare(` + INSERT INTO purchases (steam_id, item_id, item_category, card_id, price_free, price_donate, price_dust) + VALUES (?, ?, ?, ?, ?, ?, ?) + `).run(ctx.params.steamId, item_id, item_category || 'items', card_id || null, price_free || 0, price_donate || 0, price_dust || 0); + return { success: true }; +}); + +// POST /player/:steamId/promo/redeem — Redeem a promo code +route('player/:steamId/promo/redeem', ['POST'], (ctx: HandlerContext) => { + const { code } = ctx.body as any; + if (!code) throw new HttpError(400, 'Code is required'); + const normalizedCode = String(code).toUpperCase(); + const db = getDb(); + const promo = db.prepare('SELECT * FROM promo_codes WHERE code = ?').get(normalizedCode) as any; + if (!promo) throw new HttpError(404, 'Promo code not found'); + if (promo.expires_at && new Date(promo.expires_at) < new Date()) throw new HttpError(400, 'Code expired'); + if (promo.current_uses >= promo.max_uses) throw new HttpError(400, 'Code fully redeemed'); + + const existing = db.prepare('SELECT * FROM promo_redemptions WHERE steam_id = ? AND code = ?').get(ctx.params.steamId, normalizedCode); + if (existing) throw new HttpError(400, 'Code already redeemed'); + + db.prepare(` + UPDATE players SET free_currency = free_currency + ?, donate_currency = donate_currency + ?, + dust_currency = dust_currency + ?, updated_at = datetime('now') WHERE steam_id = ? + `).run(promo.free_currency, promo.donate_currency, promo.dust_currency, ctx.params.steamId); + + db.prepare('UPDATE promo_codes SET current_uses = current_uses + 1 WHERE code = ?').run(normalizedCode); + db.prepare('INSERT INTO promo_redemptions (steam_id, code) VALUES (?, ?)').run(ctx.params.steamId, normalizedCode); + + const player = db.prepare('SELECT free_currency, donate_currency, dust_currency FROM players WHERE steam_id = ?').get(ctx.params.steamId); + return { success: true, rewards: { free_currency: promo.free_currency, donate_currency: promo.donate_currency, dust_currency: promo.dust_currency }, currency: player }; +}); + +// GET /player/:steamId/sounds_wheel — Get sounds wheel +route('player/:steamId/sounds_wheel', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const player = db.prepare('SELECT sounds_wheel FROM players WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!player) throw new HttpError(404, 'Player not found'); + return { sounds_wheel: JSON.parse(player.sounds_wheel || '{}') }; +}); + +// PUT /player/:steamId/sounds_wheel — Save sounds wheel +route('player/:steamId/sounds_wheel', ['PUT'], (ctx: HandlerContext) => { + const { sounds_wheel } = ctx.body as any; + const db = getDb(); + db.prepare("UPDATE players SET sounds_wheel = ?, updated_at = datetime('now') WHERE steam_id = ?") + .run(JSON.stringify(sounds_wheel || {}), ctx.params.steamId); + return { success: true }; +}); + +// POST /player/:steamId/deal-purchase — Buy a deal/offer +route('player/:steamId/deal-purchase', ['POST'], (ctx: HandlerContext) => { + const { deal_key } = ctx.body as any; + return { success: true, ok: true, item_id: 'deal_' + deal_key, item_category: 'items' }; +}); + +// GET /player/:steamId/active_effects — Get active cosmetic effects +route('player/:steamId/active_effects', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const row = db.prepare('SELECT effects FROM active_effects WHERE steam_id = ?').get(ctx.params.steamId) as any; + return { active_effects: row ? JSON.parse(row.effects) : {} }; +}); + +// PUT /player/:steamId/active_effects — Save active effects +route('player/:steamId/active_effects', ['PUT'], (ctx: HandlerContext) => { + const { active_effects } = ctx.body as any; + const db = getDb(); + db.prepare(` + INSERT INTO active_effects (steam_id, effects, updated_at) VALUES (?, ?, datetime('now')) + ON CONFLICT(steam_id) DO UPDATE SET effects = ?, updated_at = datetime('now') + `).run(ctx.params.steamId, JSON.stringify(active_effects || {}), JSON.stringify(active_effects || {})); + return { success: true }; +});