Premiere verison route admin operationnelle
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
backend/sql/add_scores_columns.sql
Normal file
4
backend/sql/add_scores_columns.sql
Normal 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
13
backend/sql/matches.sql
Normal 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()
|
||||
);
|
||||
31
backend/sql/migrate_matches_add_cols.sql
Normal file
31
backend/sql/migrate_matches_add_cols.sql
Normal 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);
|
||||
6
backend/sql/seed_matches.sql
Normal file
6
backend/sql/seed_matches.sql
Normal 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;
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* Minimal PG pool using env vars:
|
||||
* PGHOST, PGUSER, PGPASSWORD, PGDATABASE, PGPORT
|
||||
*/
|
||||
const { Pool } = require('pg');
|
||||
|
||||
const pool = new Pool({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user