Premiere verison route admin operationnelle

This commit is contained in:
karim hassan
2025-08-25 12:39:54 +00:00
parent eabd0aa50f
commit 38ea5c7da0
21 changed files with 692 additions and 53 deletions

View File

@@ -1,18 +1,13 @@
{
"name": "supersunday-backend",
"name": "supersunday-api",
"version": "1.0.0",
"type": "module",
"description": "Super Sunday API (CommonJS)",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js"
"start": "node src/index.js"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.6.1",
"express": "^4.21.2",
"helmet": "^7.2.0",
"jsonwebtoken": "^9.0.2",
"pg": "^8.11.5"
"pg": "^8.12.0"
}
}
}

View File

@@ -0,0 +1,4 @@
-- Adds score_a and score_b if not present
ALTER TABLE IF EXISTS matches
ADD COLUMN IF NOT EXISTS score_a INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS score_b INT NOT NULL DEFAULT 0;

13
backend/sql/matches.sql Normal file
View File

@@ -0,0 +1,13 @@
-- Ensure basic matches table exists
CREATE TABLE IF NOT EXISTS matches (
id SERIAL PRIMARY KEY,
tournament_id INT NOT NULL REFERENCES tournaments(id) ON DELETE CASCADE,
player_a_id INT REFERENCES participants(id),
player_b_id INT REFERENCES participants(id),
court TEXT,
start_time TIMESTAMP NULL,
score_a INT DEFAULT 0,
score_b INT DEFAULT 0,
finished BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT now()
);

View File

@@ -0,0 +1,31 @@
-- Migrate existing matches table to expected columns
-- Add columns if they don't exist
ALTER TABLE IF EXISTS matches
ADD COLUMN IF NOT EXISTS player_a_id INT NULL,
ADD COLUMN IF NOT EXISTS player_b_id INT NULL,
ADD COLUMN IF NOT EXISTS court TEXT NULL,
ADD COLUMN IF NOT EXISTS start_time TIMESTAMP NULL,
ADD COLUMN IF NOT EXISTS score_a INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS score_b INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS finished BOOLEAN NOT NULL DEFAULT FALSE;
-- Optionally add FKs if participants table exists
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name='participants') THEN
BEGIN
ALTER TABLE matches
ADD CONSTRAINT IF NOT EXISTS matches_player_a_fk FOREIGN KEY (player_a_id) REFERENCES participants(id) ON DELETE SET NULL,
ADD CONSTRAINT IF NOT EXISTS matches_player_b_fk FOREIGN KEY (player_b_id) REFERENCES participants(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN
-- ignore if constraints already exist
NULL;
END;
END IF;
END $$;
-- Ensure finished default is respected on existing rows
UPDATE matches SET finished = COALESCE(finished, FALSE);
UPDATE matches SET score_a = COALESCE(score_a, 0);
UPDATE matches SET score_b = COALESCE(score_b, 0);

View File

@@ -0,0 +1,6 @@
-- Example seeds (adjust IDs to your existing rows)
INSERT INTO matches (tournament_id, player_a_id, player_b_id, court, start_time)
VALUES
(1, NULL, NULL, 'Court 1', now() + interval '1 hour'),
(1, NULL, NULL, 'Court 2', now() + interval '2 hour')
ON CONFLICT DO NOTHING;

View File

@@ -1,7 +1,3 @@
/**
* Minimal PG pool using env vars:
* PGHOST, PGUSER, PGPASSWORD, PGDATABASE, PGPORT
*/
const { Pool } = require('pg');
const pool = new Pool({

View File

@@ -1,28 +1,25 @@
const express = require('express');
const app = express();
app.use(express.json());
// Health
app.get('/api/health', (req, res) => res.json({ ok: true, ts: Date.now() }));
// Routes
app.use('/api/tournaments', require('./routes/tournaments'));
app.use('/api/participants', require('./routes/participants'));
app.use('/api/matches', require('./routes/matches'));
// "by tournament" nicer path
// Aliases convenient paths:
app.get('/api/tournaments/:id/participants', (req, res, next) => {
req.url = '/by-tournament/' + req.params.id;
return require('./routes/participants')(req, res, next);
});
app.get('/api/tournaments/:id/matches', (req, res, next) => {
req.url = '/by-tournament/' + req.params.id;
return require('./routes/matches')(req, res, next);
});
// 404 for unknown API paths
app.use('/api', (req, res) => res.status(404).json({ error: 'not_found' }));
const PORT = Number(process.env.PORT || 4000);
app.listen(PORT, () => console.log(`API listening on :${PORT}`));
app.use('/api/participants', require('./routes/participants'));
app.get('/api/tournaments/:id/participants', (req,res,next)=>{
req.url = '/by-tournament/' + req.params.id;
return require('./routes/participants')(req,res,next);
});

View File

@@ -1,20 +1,138 @@
import { Router } from 'express';
import pool from '../db.js';
import { requireAuth } from '../middleware/auth.js';
const express = require('express');
const router = express.Router();
const { pool } = require('../db');
const router = Router();
/**
* Schema attendu (observé chez toi) :
* - matches: id, tournament_id, team_a_id, team_b_id, court?, start_time?, status text default 'scheduled', finished bool
* - (optionnel) score_a, score_b → si absents, on peut les créer via migration fournie
*/
// Score a match (admin)
router.post('/:id/score', requireAuth, async (req,res)=>{
const mid = Number(req.params.id);
const { score_a, score_b, done } = req.body || {};
if (!mid) return res.status(400).json({ error:'bad_match_id' });
const { rows } = await pool.query(
'update matches set score_a=$2, score_b=$3, done=coalesce($4, done) where id=$1 returning *',
[mid, score_a ?? 0, score_b ?? 0, done]
);
if (!rows[0]) return res.status(404).json({ error:'match_not_found' });
res.json(rows[0]);
// Création d'un match (équipes)
router.post('/', async (req, res) => {
const { tournament_id, team_a_id, team_b_id, court, start_time, status } = req.body || {};
const tid = Number(tournament_id);
if (!tid) return res.status(400).json({ error: 'missing_tournament_id' });
try {
const { rows } = await pool.query(
`INSERT INTO matches (tournament_id, team_a_id, team_b_id, court, start_time, status)
VALUES ($1, $2, $3, $4, $5, COALESCE($6,'scheduled'))
RETURNING *`,
[tid, team_a_id || null, team_b_id || null, court || null, start_time || null, status || null]
);
res.status(201).json(rows[0]);
} catch (e) {
console.error('POST /matches error:', e);
res.status(500).json({ error: 'server_error', detail: e.message });
}
});
export default router;
// Liste (option tournoi)
router.get('/', async (req, res) => {
const tid = Number(req.query.tid || 0);
try {
const { rows } = await pool.query(
tid
? `SELECT * FROM matches WHERE tournament_id = $1 ORDER BY id DESC`
: `SELECT * FROM matches ORDER BY id DESC`,
tid ? [tid] : []
);
res.json(rows);
} catch (e) {
console.error('GET /matches', e);
res.status(500).json({ error: 'server_error' });
}
});
// Alias: GET /api/tournaments/:id/matches
router.get('/by-tournament/:id', async (req, res) => {
const id = Number(req.params.id);
if (!id) return res.status(400).json({ error: 'bad_tournament_id' });
try {
const { rows } = await pool.query(`SELECT * FROM matches WHERE tournament_id = $1 ORDER BY id DESC`, [id]);
res.json(rows);
} catch (e) {
console.error('GET /matches/by-tournament/:id', e);
res.status(500).json({ error: 'server_error' });
}
});
// Score d'un match
// Si score_a/score_b n'existent pas encore, répond avec un message clair (et fournir migration)
router.post('/:id/score', async (req, res) => {
const id = Number(req.params.id);
if (!id) return res.status(400).json({ error: 'bad_id' });
const { score_a, score_b, finished } = req.body || {};
// Construire SET dynamiquement
const sets = [];
const vals = [];
let i = 1;
if (typeof score_a === 'number') { sets.push(`score_a = $${i++}`); vals.push(score_a); }
if (typeof score_b === 'number') { sets.push(`score_b = $${i++}`); vals.push(score_b); }
if (typeof finished === 'boolean') { sets.push(`finished = $${i++}`); vals.push(finished); }
if (sets.length === 0) return res.status(400).json({ error: 'no_fields' });
vals.push(id);
try {
const q = `UPDATE matches SET ${sets.join(', ')} WHERE id = $${i} RETURNING *`;
const { rows } = await pool.query(q, vals);
if (rows.length === 0) return res.status(404).json({ error: 'not_found' });
res.json(rows[0]);
} catch (e) {
// Si colonne inexistante → proposer la migration
if (e && e.code === '42703') {
return res.status(409).json({
error: 'missing_columns',
detail: 'Les colonnes score_a/score_b sont absentes. Exécutez la migration SQL fournie pour les ajouter.'
});
}
console.error('POST /matches/:id/score', e);
res.status(500).json({ error: 'server_error' });
}
});
// Update générique (équipes, court, start_time, status, finished)
router.put('/:id', async (req, res) => {
const id = Number(req.params.id);
if (!id) return res.status(400).json({ error: 'bad_id' });
const allowed = ['team_a_id','team_b_id','court','start_time','status','finished','tournament_id'];
const sets = [];
const vals = [];
let i = 1;
for (const k of allowed) {
if (Object.prototype.hasOwnProperty.call(req.body, k)) {
sets.push(`${k} = $${i++}`);
vals.push(req.body[k]);
}
}
if (sets.length === 0) return res.status(400).json({ error: 'no_fields' });
vals.push(id);
try {
const { rows } = await pool.query(`UPDATE matches SET ${sets.join(', ')} WHERE id = $${i} RETURNING *`, vals);
if (rows.length === 0) return res.status(404).json({ error: 'not_found' });
res.json(rows[0]);
} catch (e) {
console.error('PUT /matches/:id', e);
res.status(500).json({ error: 'server_error' });
}
});
// Suppression
router.delete('/:id', async (req, res) => {
const id = Number(req.params.id);
if (!id) return res.status(400).json({ error: 'bad_id' });
try {
const result = await pool.query(`DELETE FROM matches WHERE id = $1`, [id]);
if (result.rowCount === 0) return res.status(404).json({ error: 'not_found' });
res.json({ ok: true, id });
} catch (e) {
console.error('DELETE /matches/:id', e);
res.status(500).json({ error: 'server_error' });
}
});
module.exports = router;

View File

@@ -2,7 +2,6 @@ const express = require('express');
const router = express.Router();
const { pool } = require('../db');
// GET /api/tournaments/:id/participants
router.get('/by-tournament/:id', async (req, res) => {
const id = Number(req.params.id);
if (!id) return res.status(400).json({ error: 'bad_tournament_id' });
@@ -18,7 +17,6 @@ router.get('/by-tournament/:id', async (req, res) => {
}
});
// POST /api/participants { tournament_id, full_name }
router.post('/', async (req, res) => {
const { tournament_id, full_name } = req.body || {};
const tid = Number(tournament_id);

View File

@@ -2,7 +2,6 @@ const express = require('express');
const router = express.Router();
const { pool } = require('../db');
// GET /api/tournaments -> liste
router.get('/', async (req, res) => {
try {
const { rows } = await pool.query(
@@ -15,7 +14,6 @@ router.get('/', async (req, res) => {
}
});
// POST /api/tournaments -> créer
router.post('/', async (req, res) => {
const { name, location, start_date, end_date } = req.body || {};
if (!name || !start_date || !end_date) {
@@ -35,7 +33,6 @@ router.post('/', async (req, res) => {
}
});
// DELETE /api/tournaments/:id -> supprimer (avec nettoyage dépendances si pas de CASCADE)
router.delete('/:id', async (req, res) => {
const id = Number(req.params.id);
if (!id) return res.status(400).json({ error: 'bad_id' });
@@ -43,18 +40,11 @@ router.delete('/:id', async (req, res) => {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Si votre schéma n'a PAS de ON DELETE CASCADE, on nettoie à la main :
// matches → participants → (tournament row)
try { await client.query('DELETE FROM matches WHERE tournament_id = $1', [id]); } catch {}
try { await client.query('DELETE FROM participants WHERE tournament_id = $1', [id]); } catch {}
const result = await client.query('DELETE FROM tournaments WHERE id = $1', [id]);
await client.query('COMMIT');
if (result.rowCount === 0) {
return res.status(404).json({ error: 'not_found' });
}
if (result.rowCount === 0) return res.status(404).json({ error: 'not_found' });
res.json({ ok: true, id });
} catch (e) {
await client.query('ROLLBACK');