From be2ba473ff946ee286bd37fb4b1f1d135da2ca5c Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 20 Dec 2025 10:34:05 -0500 Subject: [PATCH] Add health check endpoint and missing REST API endpoints. --- README.md | 45 ++++++++++++++++---- TODO.md | 14 ++++++- src/index.js | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 162 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1ab5481..3c64fce 100644 --- a/README.md +++ b/README.md @@ -44,15 +44,44 @@ If the wallet RPC service or configuration is unavailable the backend automatica ## API Surface -- `POST /api/payment/create` – Request a payment for the active mode. -- `GET /api/payment/status/:paymentId` – Poll payment status. -- `POST /api/payment/callback` – Wallet RPC webhook to confirm payments. -- `GET /api/game/modes` – Current game modes and pricing. -- `GET /api/user/credits/:userId` – Remaining credit balance. -- `GET /api/user/mode/:userId` – User game mode. -- `POST /api/user/address/:userId` – Store payout address after confirmation. +### Health & Status -Only the payment routes require wallet RPC access; all other gameplay functions work without it. +- `GET /health` – Server health check with uptime, memory usage, game counts, and wallet status. +- `GET /api/game-modes` – Current game modes and pricing configuration. + +### User Endpoints + +- `GET /api/user/:socketId/credits` – Remaining credit balance for a user. +- `GET /api/user/:socketId/mode` – User payment mode preferences and enabled features. +- `POST /api/user/:socketId/address` – Store payout address (body: `{ "address": "" }`). + +### Payment Endpoints (Socket.IO Preferred) + +The following REST endpoints exist but return `501 Not Implemented`. Payment flows are handled via Socket.IO events for real-time feedback: + +- `POST /api/payment/create` – Use socket event `request_payment` instead. +- `GET /api/payment/status/:paymentId` – Use socket event `payment_status` instead. +- `POST /api/payment/callback` – Reserved for future webhook integration. + +### Socket.IO Events (Primary Payment Flow) + +| Client → Server | Description | +|-----------------|-------------| +| `request_payment` | Request a payment address for game entry | +| `confirm_address` | Confirm a detected payout address | +| `cancel_address` | Cancel pending address confirmation | +| `join_queue` | Join the game queue after payment | + +| Server → Client | Description | +|-----------------|-------------| +| `payment_created` | Payment address and QR code ready | +| `payment_confirmed` | Payment received and confirmed | +| `address_detected` | Payout address detected in chat | +| `address_confirmed` | Payout address saved | +| `game_start` | Game session beginning | +| `credits_update` | Credit balance changed | + +All gameplay functions work without wallet RPC access; only paid modes require it. ## Installation diff --git a/TODO.md b/TODO.md index 5c5d929..f0e5bb1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,7 @@ # Wownerogue Development Todo ## High Priority -- [ ] **Verify Server Startup**: Ensure `npm run dev` starts cleanly without errors after recent refactors. +- [x] **Verify Server Startup**: Server starts cleanly with `npm run dev` ✅ - [ ] **Test Payment Flow**: Verify the full payment lifecycle (Request -> QR -> Payment -> Confirmation -> Game Start). - [ ] **Test Payouts**: Confirm payouts are triggered correctly upon winning in paid modes. - [ ] **Address Persistence**: Verify that payout addresses persist across server restarts via session tokens. @@ -13,5 +13,15 @@ - [ ] **Frontend Cleanup**: Remove any remaining dead code or unused assets from the `html/` directory. ## Documentation -- [ ] **API Documentation**: Expand API documentation in `README.md` with request/response examples. +- [x] **API Documentation**: README updated with accurate API surface including health check and Socket.IO events ✅ - [ ] **Deployment Guide**: Create a detailed deployment guide including reverse proxy setup (Nginx/Caddy). + +## Pre-Beta Checklist +- [x] All 21 tests pass ✅ +- [x] Health check endpoint available at `/health` ✅ +- [x] REST endpoints implemented: `/api/user/:socketId/credits`, `/api/user/:socketId/mode`, `/api/user/:socketId/address` ✅ +- [ ] **TLS/HTTPS**: Configure reverse proxy (Nginx/Caddy) with SSL certificates before exposing to real users. +- [ ] **Database Backups**: Implement automated backup strategy for PostgreSQL. +- [ ] **Manual Integration Test**: Run full payment flow with real `wownero-wallet-rpc` (not mocked). +- [ ] **Rate Limit Tuning**: Review rate limits for production traffic patterns. +- [ ] **Log Rotation**: Ensure logs don't fill disk in production. diff --git a/src/index.js b/src/index.js index 9b3c822..9ef5300 100644 --- a/src/index.js +++ b/src/index.js @@ -92,10 +92,121 @@ app.get('/api/payment/status/:paymentId', asyncHandler(async (req, res) => { throw restNotImplemented(); })); -app.get('/api/user/:userId/credits', asyncHandler(async (req, res) => { - throw restNotImplemented(); +app.get('/api/user/:socketId/credits', asyncHandler(async (req, res) => { + const { socketId } = req.params; + if (!socketId) { + throw new ValidationError('Missing socketId', { + safeMessage: 'socketId parameter is required.' + }); + } + + try { + const user = await gameModeManager.getOrCreateUser(socketId); + res.json({ + socketId, + credits: user.credits || 0, + totalCreditsPurchased: user.total_credits_purchased || 0 + }); + } catch (error) { + throw new AppError('Failed to retrieve credits', { + statusCode: 500, + safeMessage: 'Unable to retrieve credit balance.', + cause: error + }); + } })); +app.get('/api/user/:socketId/mode', asyncHandler(async (req, res) => { + const { socketId } = req.params; + if (!socketId) { + throw new ValidationError('Missing socketId', { + safeMessage: 'socketId parameter is required.' + }); + } + + try { + const user = await gameModeManager.getOrCreateUser(socketId); + res.json({ + socketId, + preferredPaymentMode: user.preferred_payment_mode || 'direct', + hasPayoutAddress: !!user.payout_address, + paymentsEnabled: gameModeManager.paymentsEnabled, + directModeEnabled: gameModeManager.directModeEnabled, + creditsModeEnabled: gameModeManager.creditsModeEnabled + }); + } catch (error) { + throw new AppError('Failed to retrieve user mode', { + statusCode: 500, + safeMessage: 'Unable to retrieve user mode.', + cause: error + }); + } +})); + +app.post('/api/user/:socketId/address', asyncHandler(async (req, res) => { + const { socketId } = req.params; + const { address } = req.body || {}; + + if (!socketId) { + throw new ValidationError('Missing socketId', { + safeMessage: 'socketId parameter is required.' + }); + } + + if (!address || typeof address !== 'string') { + throw new ValidationError('Missing or invalid address', { + safeMessage: 'A valid payout address is required.' + }); + } + + // Basic address validation (XMR/WOW addresses) + const ADDRESS_REGEX = /((?:4|8)[1-9A-HJ-NP-Za-km-z]{90,110}|(?:Wo|WO|ww|WW)[0-9A-Za-z]{88,112}|W[0-9A-Za-z]{90,112})/; + if (!ADDRESS_REGEX.test(address.trim())) { + throw new ValidationError('Invalid address format', { + safeMessage: 'The provided address does not appear to be a valid XMR/WOW address.' + }); + } + + try { + const success = await gameModeManager.setUserPayoutAddress(socketId, address.trim()); + if (!success) { + throw new AppError('Failed to save address', { + statusCode: 500, + safeMessage: 'Unable to save payout address.' + }); + } + res.json({ + success: true, + message: 'Payout address saved successfully.' + }); + } catch (error) { + if (error instanceof AppError || error instanceof ValidationError) { + throw error; + } + throw new AppError('Failed to save address', { + statusCode: 500, + safeMessage: 'Unable to save payout address.', + cause: error + }); + } +})); + +// Health check endpoint +app.get('/health', (req, res) => { + const health = { + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + memoryUsage: process.memoryUsage(), + gameMode: gameModeManager.gameMode, + paymentsEnabled: gameModeManager.paymentsEnabled, + walletHealthy: walletRPCService.isHealthy, + activeGames: activeGames.size, + debugMode: debugManager.getDebugStatus().debugMode + }; + res.json(health); +}); + app.get('/api/game-modes', (req, res) => { const config = paymentConfigManager.getConfig(); const decimals = Number(config.currency?.decimals ?? 12);