const express = require('express'); const cors = require('cors'); const { db, authenticate, ApiError, scope } = require('./src/core.js'); const app = express(); app.use(cors()); app.use(express.json()); /* ── public ──────────────────────────────────────────────────────────────── */ app.get('/', (_req, res) => res.json({ service: 'paddock-api', docs: 'https://huggingface.co/spaces/Shaurya2020/paddock-api', console: 'https://Shaurya2020-paddock-console.hf.space', endpoints: { health: 'GET /api/health', login: 'POST /api/auth/login {email, password}', me: 'GET /api/me', race_events: 'GET|POST /api/race-events · GET|PUT|DELETE /api/race-events/:id · PATCH /api/race-events/:id/status', odds: 'GET|POST /api/race-events/:id/odds · GET /api/race-events/:id/odds/history', betting: 'POST|GET /api/bets · GET /api/bets/:id · PATCH /api/bets/:id/cancel', settlement: 'GET /api/settlements · GET /api/settlements/:id · POST /api/settlements/:id/{result,settle,void}', content: 'GET|POST /api/teams · PUT|DELETE /api/teams/:id · same for /api/jockeys' }, demo_logins: [ { email: 'ops@paddock.io', password: 'super', role: 1, sees: 'every operator' }, { email: 'admin@meridian.in', password: 'meridian', role: 2, sees: 'own rows only' }, { email: 'admin@kingsgate.in', password: 'kingsgate', role: 2, sees: 'own rows only' } ] })); app.get('/api/health', (_req, res) => { const t = db.prepare( `SELECT count(*) n FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name <> '_migrations'` ).get().n; res.json({ ok: true, tables: t, uptime_s: Math.round(process.uptime()) }); }); /** Demo sign-in. Returns the token the other endpoints expect. */ app.post('/api/auth/login', (req, res) => { const crypto = require('node:crypto'); const { email, password } = req.body || {}; const hash = crypto.createHash('sha256').update((password || '') + '::paddock').digest('hex'); const a = db.prepare('SELECT * FROM admins WHERE email = ? AND password_hash = ?').get(email, hash); if (!a) return res.status(401).json({ error: { message: 'Those credentials do not match an admin.' } }); res.json({ data: { token: `admin:${a.id}`, admin: { id: a.id, name: a.name, role: a.role, label: a.label } } }); }); /* ── everything below requires a token ───────────────────────────────────── */ app.use('/api', authenticate); app.get('/api/me', (req, res) => { const counts = {}; for (const t of ['race_events', 'bets', 'teams', 'jockeys']) { const s = scope(req.admin, 'x'); counts[t] = { visible: db.prepare(`SELECT count(*) n FROM ${t} x WHERE ${s.sql}`).get(...s.params).n, total: db.prepare(`SELECT count(*) n FROM ${t}`).get().n }; } res.json({ data: { admin: req.admin, scope: req.admin.role === 1 ? 'all operators' : 'own rows only', counts } }); }); const raceEvents = require('./src/raceEventsApi.js'); const odds = require('./src/oddsApi.js'); const betting = require('./src/bettingApi.js'); const settlements = require('./src/settlementsApi.js'); const content = require('./src/contentApi.js'); app.use('/api/race-events/:id/odds', odds); // mounted first so it wins over /race-events/:id app.use('/api/race-events', raceEvents); app.use('/api/bets', betting); app.use('/api/settlements', settlements); app.use('/api', content); // /api/teams and /api/jockeys /* ── errors ──────────────────────────────────────────────────────────────── */ app.use((_req, res) => res.status(404).json({ error: { message: 'No such endpoint.' } })); app.use((err, _req, res, _next) => { const status = err instanceof ApiError ? err.status : 500; if (status === 500) console.error(err); res.status(status).json({ error: { message: err.message, ...(err.detail ? { detail: err.detail } : {}) } }); }); const PORT = process.env.PORT || 3000; if (require.main === module) app.listen(PORT, '0.0.0.0', () => console.log(`paddock-api listening on ${PORT}`)); module.exports = app;