Mise à jour : dernières modifs
This commit is contained in:
12
backend/Dockerfile
Normal file
12
backend/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* .npmrc* ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
EXPOSE 8080
|
||||
CMD ["node", "server.js"]
|
||||
1200
backend/package-lock.json
generated
Normal file
1200
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
backend/package.json
Normal file
18
backend/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "supersunday-api",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"pg": "^8.11.5"
|
||||
}
|
||||
}
|
||||
277
backend/server.js
Normal file
277
backend/server.js
Normal file
@@ -0,0 +1,277 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import morgan from 'morgan';
|
||||
import dotenv from 'dotenv';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import pkg from 'pg';
|
||||
|
||||
dotenv.config();
|
||||
const { Pool } = pkg;
|
||||
|
||||
const PORT = process.env.PORT || 8080;
|
||||
const DATABASE_URL = process.env.DATABASE_URL || 'postgres://supersunday:supersunday@db:5432/supersunday';
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret';
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@supersunday.local';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
|
||||
|
||||
const app = express();
|
||||
app.use(helmet());
|
||||
app.use(cors({ origin: process.env.CORS_ORIGIN?.split(',') || '*' }));
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
app.use(morgan('tiny'));
|
||||
|
||||
const pool = new Pool({ connectionString: DATABASE_URL });
|
||||
async function q(text, params=[]) { const { rows } = await pool.query(text, params); return rows; }
|
||||
|
||||
function auth(req, res, next) {
|
||||
const header = req.headers.authorization || '';
|
||||
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
|
||||
if (!token) return res.status(401).json({ error: 'missing token' });
|
||||
try { req.user = jwt.verify(token, JWT_SECRET); next(); }
|
||||
catch { res.status(401).json({ error: 'invalid token' }); }
|
||||
}
|
||||
|
||||
/* Health + Auth */
|
||||
app.get('/api/health', async (req, res) => {
|
||||
try { const { rows } = await pool.query('SELECT 1 as ok'); res.json({ status: 'ok', db: rows[0].ok === 1 }); }
|
||||
catch (e) { res.status(500).json({ status: 'error', error: String(e) }); }
|
||||
});
|
||||
app.post('/api/auth/login', (req, res) => {
|
||||
const { email, password } = req.body || {};
|
||||
if (email === ADMIN_EMAIL && password === ADMIN_PASSWORD) {
|
||||
const token = jwt.sign({ email, role: 'admin' }, JWT_SECRET, { expiresIn: '12h' });
|
||||
return res.json({ token });
|
||||
}
|
||||
res.status(401).json({ error: 'invalid credentials' });
|
||||
});
|
||||
|
||||
/* Players */
|
||||
app.get('/api/players', async (req, res) => {
|
||||
res.json(await q('SELECT * FROM players ORDER BY id DESC'));
|
||||
});
|
||||
app.get('/api/players/:id', async (req, res) => {
|
||||
const rows = await q('SELECT * FROM players WHERE id=$1', [+req.params.id]);
|
||||
if (!rows[0]) return res.status(404).json({ error: 'not found' });
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.post('/api/players', auth, async (req, res) => {
|
||||
const { first_name, last_name, email=null, ranking=0 } = req.body||{};
|
||||
if (!first_name || !last_name) return res.status(400).json({ error: 'first_name & last_name required' });
|
||||
const rows = await q('INSERT INTO players(first_name,last_name,email,ranking) VALUES ($1,$2,$3,$4) RETURNING *',
|
||||
[first_name, last_name, email, ranking]);
|
||||
res.status(201).json(rows[0]);
|
||||
});
|
||||
app.put('/api/players/:id', auth, async (req, res) => {
|
||||
const { first_name, last_name, email, ranking } = req.body||{};
|
||||
const rows = await q('UPDATE players SET first_name=$1,last_name=$2,email=$3,ranking=$4 WHERE id=$5 RETURNING *',
|
||||
[first_name, last_name, email, ranking, +req.params.id]);
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.delete('/api/players/:id', auth, async (req, res) => {
|
||||
await q('DELETE FROM players WHERE id=$1', [+req.params.id]); res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* Tournaments */
|
||||
app.get('/api/tournaments', async (req, res) => {
|
||||
res.json(await q('SELECT * FROM tournaments ORDER BY start_date DESC, id DESC'));
|
||||
});
|
||||
app.get('/api/tournaments/upcoming', async (req, res) => {
|
||||
res.json(await q('SELECT * FROM tournaments WHERE start_date >= CURRENT_DATE ORDER BY start_date ASC LIMIT 10'));
|
||||
});
|
||||
app.get('/api/tournaments/:id', async (req, res) => {
|
||||
const id = +req.params.id;
|
||||
const [t] = await q('SELECT * FROM tournaments WHERE id=$1', [id]);
|
||||
if (!t) return res.status(404).json({ error: 'tournament not found' });
|
||||
const [{ enrollment_count }] = await q('SELECT COUNT(*)::int AS enrollment_count FROM enrollments WHERE tournament_id=$1', [id]);
|
||||
const [{ team_count }] = await q('SELECT COUNT(*)::int AS team_count FROM teams WHERE tournament_id=$1', [id]);
|
||||
const [{ match_count }] = await q('SELECT COUNT(*)::int AS match_count FROM matches WHERE tournament_id=$1', [id]);
|
||||
const nextRows = await q(`SELECT * FROM matches WHERE tournament_id=$1 AND status IN ('scheduled','playing') AND scheduled_at IS NOT NULL ORDER BY scheduled_at ASC LIMIT 1`, [id]);
|
||||
res.json({ ...t, enrollment_count, team_count, match_count, next_match: nextRows[0] || null });
|
||||
});
|
||||
app.get('/api/tournaments/:id/summary', async (req, res) => {
|
||||
const id = +req.params.id;
|
||||
const [t] = await q('SELECT * FROM tournaments WHERE id=$1', [id]);
|
||||
if (!t) return res.status(404).json({ error: 'tournament not found' });
|
||||
const enroll = await q(`
|
||||
SELECT p.id, p.first_name, p.last_name, p.ranking
|
||||
FROM enrollments e JOIN players p ON p.id=e.player_id
|
||||
WHERE e.tournament_id=$1 ORDER BY p.ranking ASC NULLS LAST, p.last_name ASC
|
||||
`, [id]);
|
||||
const matches = await q('SELECT * FROM matches WHERE tournament_id=$1 ORDER BY scheduled_at NULLS LAST, id ASC', [id]);
|
||||
res.json({ tournament: t, enrollments: enroll, matches });
|
||||
});
|
||||
app.post('/api/tournaments', auth, async (req, res) => {
|
||||
const { name, location=null, start_date, end_date } = req.body||{};
|
||||
if (!name || !start_date || !end_date) return res.status(400).json({ error: 'name, start_date, end_date required' });
|
||||
const rows = await q('INSERT INTO tournaments(name,location,start_date,end_date) VALUES ($1,$2,$3,$4) RETURNING *',
|
||||
[name, location, start_date, end_date]);
|
||||
res.status(201).json(rows[0]);
|
||||
});
|
||||
app.put('/api/tournaments/:id', auth, async (req, res) => {
|
||||
const { name, location, start_date, end_date } = req.body||{};
|
||||
const rows = await q('UPDATE tournaments SET name=$1,location=$2,start_date=$3,end_date=$4 WHERE id=$5 RETURNING *',
|
||||
[name, location, start_date, end_date, +req.params.id]);
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.delete('/api/tournaments/:id', auth, async (req, res) => {
|
||||
await q('DELETE FROM tournaments WHERE id=$1', [+req.params.id]); res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* Enrollments */
|
||||
app.get('/api/enrollments', async (req, res) => {
|
||||
const tid = req.query.tournament_id ? +req.query.tournament_id : null;
|
||||
if (tid) {
|
||||
return res.json(await q(`
|
||||
SELECT e.player_id, p.first_name, p.last_name, p.ranking
|
||||
FROM enrollments e JOIN players p ON p.id=e.player_id
|
||||
WHERE e.tournament_id=$1 ORDER BY p.ranking ASC, p.last_name ASC
|
||||
`, [tid]));
|
||||
}
|
||||
res.json(await q('SELECT * FROM enrollments ORDER BY tournament_id, player_id'));
|
||||
});
|
||||
app.post('/api/enrollments', auth, async (req, res) => {
|
||||
const { player_id, tournament_id } = req.body||{};
|
||||
if (!player_id || !tournament_id) return res.status(400).json({ error: 'player_id & tournament_id required' });
|
||||
await q('INSERT INTO enrollments(player_id,tournament_id) VALUES($1,$2) ON CONFLICT DO NOTHING', [player_id, tournament_id]);
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
app.post('/api/enrollments/bulk', auth, async (req, res) => {
|
||||
const { player_ids = [], tournament_id } = req.body||{};
|
||||
if (!tournament_id || !Array.isArray(player_ids)) return res.status(400).json({ error: 'player_ids[] & tournament_id required' });
|
||||
for (const pid of player_ids) { await q('INSERT INTO enrollments(player_id,tournament_id) VALUES($1,$2) ON CONFLICT DO NOTHING', [pid, tournament_id]); }
|
||||
res.json({ ok: true, count: player_ids.length });
|
||||
});
|
||||
app.delete('/api/enrollments', auth, async (req, res) => {
|
||||
const { player_id, tournament_id } = req.body||{};
|
||||
await q('DELETE FROM enrollments WHERE player_id=$1 AND tournament_id=$2', [player_id, tournament_id]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* Teams */
|
||||
app.get('/api/teams', async (req, res) => {
|
||||
const tid = req.query.tournament_id ? +req.query.tournament_id : null;
|
||||
if (tid) {
|
||||
return res.json(await q(`
|
||||
SELECT t.*,
|
||||
p1.first_name AS p1_first, p1.last_name AS p1_last,
|
||||
p2.first_name AS p2_first, p2.last_name AS p2_last
|
||||
FROM teams t
|
||||
JOIN players p1 ON p1.id=t.player1_id
|
||||
JOIN players p2 ON p2.id=t.player2_id
|
||||
WHERE t.tournament_id=$1 ORDER BY t.id DESC
|
||||
`, [tid]));
|
||||
}
|
||||
res.json(await q('SELECT * FROM teams ORDER BY id DESC'));
|
||||
});
|
||||
app.get('/api/teams/:id', async (req, res) => {
|
||||
const rows = await q('SELECT * FROM teams WHERE id=$1', [+req.params.id]);
|
||||
if (!rows[0]) return res.status(404).json({ error: 'not found' });
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.post('/api/teams', auth, async (req, res) => {
|
||||
const { player1_id, player2_id, tournament_id=null } = req.body||{};
|
||||
if (!player1_id || !player2_id) return res.status(400).json({ error: 'player1_id & player2_id required' });
|
||||
const rows = await q('INSERT INTO teams(player1_id,player2_id,tournament_id) VALUES ($1,$2,$3) RETURNING *',
|
||||
[Math.min(player1_id, player2_id), Math.max(player1_id, player2_id), tournament_id]);
|
||||
res.status(201).json(rows[0]);
|
||||
});
|
||||
app.post('/api/teams/auto', auth, async (req, res) => {
|
||||
const { tournament_id } = req.body||{};
|
||||
if (!tournament_id) return res.status(400).json({ error: 'tournament_id required' });
|
||||
const players = await q(`
|
||||
SELECT p.* FROM enrollments e JOIN players p ON p.id=e.player_id
|
||||
WHERE e.tournament_id=$1 ORDER BY p.ranking ASC NULLS LAST
|
||||
`, [tournament_id]);
|
||||
const created = [];
|
||||
for (let i=0;i<players.length;i+=2) {
|
||||
if (i+1 >= players.length) break;
|
||||
const a = players[i].id, b = players[i+1].id;
|
||||
const { rows } = await pool.query(
|
||||
'INSERT INTO teams(player1_id,player2_id,tournament_id) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING RETURNING *',
|
||||
[Math.min(a,b), Math.max(a,b), tournament_id]
|
||||
);
|
||||
if (rows[0]) created.push(rows[0]);
|
||||
}
|
||||
res.json({ created_count: created.length, created });
|
||||
});
|
||||
app.put('/api/teams/:id', auth, async (req, res) => {
|
||||
const { player1_id, player2_id, tournament_id } = req.body||{};
|
||||
const rows = await q('UPDATE teams SET player1_id=$1,player2_id=$2,tournament_id=$3 WHERE id=$4 RETURNING *',
|
||||
[Math.min(player1_id, player2_id), Math.max(player1_id, player2_id), tournament_id, +req.params.id]);
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.delete('/api/teams/:id', auth, async (req, res) => {
|
||||
await q('DELETE FROM teams WHERE id=$1', [+req.params.id]); res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* Matches */
|
||||
app.get('/api/matches', async (req, res) => {
|
||||
const tid = req.query.tournament_id ? +req.query.tournament_id : null;
|
||||
if (tid) return res.json(await q('SELECT * FROM matches WHERE tournament_id=$1 ORDER BY scheduled_at NULLS LAST, id DESC', [tid]));
|
||||
res.json(await q('SELECT * FROM matches ORDER BY scheduled_at NULLS LAST, id DESC'));
|
||||
});
|
||||
app.get('/api/matches/:id', async (req, res) => {
|
||||
const rows = await q('SELECT * FROM matches WHERE id=$1', [+req.params.id]);
|
||||
if (!rows[0]) return res.status(404).json({ error: 'not found' });
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.post('/api/matches', auth, async (req, res) => {
|
||||
const { tournament_id, court=null, round=null, scheduled_at=null, team_a_id=null, team_b_id=null } = req.body||{};
|
||||
if (!tournament_id) return res.status(400).json({ error: 'tournament_id required' });
|
||||
const rows = await q('INSERT INTO matches(tournament_id,court,round,scheduled_at,team_a_id,team_b_id) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *',
|
||||
[tournament_id, court, round, scheduled_at, team_a_id, team_b_id]);
|
||||
res.status(201).json(rows[0]);
|
||||
});
|
||||
app.put('/api/matches/:id', auth, async (req, res) => {
|
||||
const { court, round, scheduled_at, team_a_id, team_b_id, status } = req.body||{};
|
||||
const rows = await q('UPDATE matches SET court=$1, round=$2, scheduled_at=$3, team_a_id=$4, team_b_id=$5, status=$6 WHERE id=$7 RETURNING *',
|
||||
[court, round, scheduled_at, team_a_id, team_b_id, status, +req.params.id]);
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.post('/api/matches/:id/score', auth, async (req, res) => {
|
||||
const { score_a, score_b, status='finished' } = req.body||{};
|
||||
const rows = await q('UPDATE matches SET score_a=$1, score_b=$2, status=$3 WHERE id=$4 RETURNING *',
|
||||
[score_a, score_b, status, +req.params.id]);
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.post('/api/matches/generate', auth, async (req, res) => {
|
||||
const { tournament_id, courts=['Court 1'], start_at=null, interval_minutes=20 } = req.body||{};
|
||||
if (!tournament_id) return res.status(400).json({ error: 'tournament_id required' });
|
||||
const teams = await q('SELECT id FROM teams WHERE tournament_id=$1 ORDER BY id ASC', [tournament_id]);
|
||||
if (teams.length < 2) return res.status(400).json({ error: 'not enough teams' });
|
||||
const now = start_at ? new Date(start_at) : new Date(Date.now() + 5*60*1000);
|
||||
let t = new Date(now); let courtIdx = 0; const created = [];
|
||||
for (let i=0;i<teams.length;i+=2) {
|
||||
if (i+1 >= teams.length) break;
|
||||
const a = teams[i].id, b = teams[i+1].id;
|
||||
const court = courts[courtIdx % courts.length]; courtIdx++;
|
||||
const { rows } = await pool.query(
|
||||
'INSERT INTO matches(tournament_id, court, round, scheduled_at, team_a_id, team_b_id, status) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *',
|
||||
[tournament_id, court, 'Americano', t.toISOString(), a, b, 'scheduled']
|
||||
);
|
||||
created.push(rows[0]); t = new Date(t.getTime() + interval_minutes*60*1000);
|
||||
}
|
||||
res.json({ created });
|
||||
});
|
||||
|
||||
/* Leaderboard (simple): sums wins/losses by team in a tournament */
|
||||
app.get('/api/leaderboard', async (req, res) => {
|
||||
const tid = req.query.tournament_id ? +req.query.tournament_id : null;
|
||||
if (!tid) return res.status(400).json({ error: 'tournament_id required' });
|
||||
const rows = await q(`
|
||||
SELECT team_id, SUM(w) AS wins, SUM(l) AS losses
|
||||
FROM (
|
||||
SELECT team_a_id AS team_id, CASE WHEN score_a>score_b THEN 1 ELSE 0 END AS w, CASE WHEN score_a<score_b THEN 1 ELSE 0 END AS l
|
||||
FROM matches WHERE tournament_id=$1 AND status='finished' AND team_a_id IS NOT NULL AND team_b_id IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT team_b_id AS team_id, CASE WHEN score_b>score_a THEN 1 ELSE 0 END AS w, CASE WHEN score_b<score_a THEN 1 ELSE 0 END AS l
|
||||
FROM matches WHERE tournament_id=$1 AND status='finished' AND team_a_id IS NOT NULL AND team_b_id IS NOT NULL
|
||||
) x
|
||||
GROUP BY team_id
|
||||
ORDER BY wins DESC, losses ASC
|
||||
`, [tid]);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
app.listen(PORT, () => console.log(`API listening on :${PORT}`));
|
||||
Reference in New Issue
Block a user