paddock-api / server.js
Shaurya2020's picture
Wire the five modules together
3db1459 verified
Raw History Blame Contribute Delete
4.42 kB
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;