🚀 Patch auto
This commit is contained in:
27
src/db.js
Normal file
27
src/db.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import pg from 'pg';
|
||||
const { Pool } = pg;
|
||||
|
||||
const {
|
||||
PGHOST = 'db',
|
||||
PGPORT = 5432,
|
||||
PGUSER = 'postgres',
|
||||
PGPASSWORD = 'postgres',
|
||||
PGDATABASE = 'supersunday',
|
||||
} = process.env;
|
||||
|
||||
const pool = new Pool({ host: PGHOST, port: PGPORT, user: PGUSER, password: PGPASSWORD, database: PGDATABASE });
|
||||
|
||||
export async function waitForDb(retries = 30, delayMs = 2000) {
|
||||
for (let i = 1; i <= retries; i++) {
|
||||
try {
|
||||
await pool.query('select 1');
|
||||
return;
|
||||
} catch (e) {
|
||||
console.log(`[DB] attempt ${i}/${retries} failed: ${e.message}`);
|
||||
await new Promise(r => setTimeout(r, delayMs));
|
||||
}
|
||||
}
|
||||
throw new Error('DB not reachable after retries');
|
||||
}
|
||||
|
||||
export default pool;
|
||||
27
src/index.js
Normal file
27
src/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import morgan from 'morgan';
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import authRoutes from './routes/auth.js';
|
||||
import tournamentsRoutes from './routes/tournaments.js';
|
||||
import matchesRoutes from './routes/matches.js';
|
||||
|
||||
const app = express();
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(morgan('dev'));
|
||||
|
||||
app.get('/api/health', (req,res)=> res.json({ ok:true }));
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/tournaments', tournamentsRoutes);
|
||||
app.use('/api/matches', matchesRoutes);
|
||||
|
||||
const port = process.env.PORT || 4000;
|
||||
app.listen(port, ()=>{
|
||||
console.log(`SuperSunday backend listening on :${port}`);
|
||||
});
|
||||
14
src/middleware/auth.js
Normal file
14
src/middleware/auth.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export function requireAuth(req, res, next){
|
||||
const hdr = req.headers.authorization || '';
|
||||
const token = hdr.startsWith('Bearer ') ? hdr.slice(7) : null;
|
||||
if(!token) return res.status(401).json({error:'Missing token'});
|
||||
try{
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET || 'devsecret');
|
||||
req.user = payload;
|
||||
next();
|
||||
}catch(e){
|
||||
res.status(401).json({error:'Invalid token'});
|
||||
}
|
||||
}
|
||||
18
src/routes/auth.js
Normal file
18
src/routes/auth.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Router } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
const { email, password } = req.body || {};
|
||||
if(!email || !password) return res.status(400).json({error:'email/password required'});
|
||||
const adminEmail = process.env.ADMIN_EMAIL || 'admin@supersunday.local';
|
||||
const adminPass = process.env.ADMIN_PASSWORD || 'admin1234';
|
||||
if(email === adminEmail && password === adminPass){
|
||||
const token = jwt.sign({ sub: email, role: 'admin' }, process.env.JWT_SECRET || 'devsecret', { expiresIn: '12h' });
|
||||
return res.json({ token });
|
||||
}
|
||||
return res.status(401).json({error:'Invalid credentials'});
|
||||
});
|
||||
|
||||
export default router;
|
||||
15
src/routes/matches.js
Normal file
15
src/routes/matches.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Router } from 'express';
|
||||
import { query } from '../db.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/:id/score', requireAuth, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { score_a, score_b } = req.body || {};
|
||||
if(score_a==null || score_b==null) return res.status(400).json({error:'score_a and score_b required'});
|
||||
await query(`UPDATE matches SET score_a=$1, score_b=$2, finished=TRUE WHERE id=$3`, [score_a, score_b, id]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
87
src/routes/tournaments.js
Normal file
87
src/routes/tournaments.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Router } from 'express';
|
||||
import { query } from '../db.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Public
|
||||
router.get('/', async (req, res) => {
|
||||
const r = await query(`SELECT id, name, location, start_date, end_date FROM tournaments ORDER BY start_date DESC NULLS LAST, id DESC LIMIT 50`);
|
||||
res.json(r.rows);
|
||||
});
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const r = await query(`SELECT id, name, location, start_date, end_date, description FROM tournaments WHERE id=$1`, [id]);
|
||||
if(!r.rowCount) return res.status(404).json({error:'Not found'});
|
||||
res.json(r.rows[0]);
|
||||
});
|
||||
|
||||
router.get('/:id/participants', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const r = await query(`SELECT p.id, p.name, p.rating FROM participants p WHERE p.tournament_id=$1 ORDER BY p.name`, [id]);
|
||||
res.json(r.rows);
|
||||
});
|
||||
|
||||
router.get('/:id/matches', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const r = await query(`SELECT id, court, start_time, team_a, team_b, score_a, score_b, finished FROM matches WHERE tournament_id=$1 ORDER BY start_time, id`, [id]);
|
||||
res.json(r.rows);
|
||||
});
|
||||
|
||||
// Admin
|
||||
router.post('/', requireAuth, async (req, res) => {
|
||||
const { name, location, start_date, end_date, description } = req.body || {};
|
||||
if(!name) return res.status(400).json({error:'name required'});
|
||||
const r = await query(
|
||||
`INSERT INTO tournaments(name, location, start_date, end_date, description)
|
||||
VALUES ($1,$2,$3,$4,$5) RETURNING id`,
|
||||
[name, location||null, start_date||null, end_date||null, description||null]
|
||||
);
|
||||
res.status(201).json({ id: r.rows[0].id });
|
||||
});
|
||||
|
||||
router.post('/:id/participants', requireAuth, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { name, rating } = req.body || {};
|
||||
if(!name) return res.status(400).json({error:'name required'});
|
||||
const r = await query(
|
||||
`INSERT INTO participants(tournament_id, name, rating) VALUES ($1,$2,$3) RETURNING id`,
|
||||
[id, name, rating||null]
|
||||
);
|
||||
res.status(201).json({ id: r.rows[0].id });
|
||||
});
|
||||
|
||||
router.post('/:id/generate-americano', requireAuth, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { courts = [], start = null, slotMinutes = 20 } = req.body || {};
|
||||
// Minimal placeholder generator: pairs consecutive participants; schedule sequentially per court
|
||||
const p = await query(`SELECT id, name FROM participants WHERE tournament_id=$1 ORDER BY id`, [id]);
|
||||
if(p.rows.length < 4) return res.status(400).json({error:'Need at least 4 participants'});
|
||||
const pairs = [];
|
||||
for(let i=0;i<p.rows.length;i+=2){
|
||||
if(i+1<p.rows.length) pairs.push([p.rows[i], p.rows[i+1]]);
|
||||
}
|
||||
const courtsArr = Array.isArray(courts) && courts.length ? courts : ['Court 1', 'Court 2'];
|
||||
const startTime = start ? new Date(start) : new Date(Date.now()+10*60*1000);
|
||||
const matches = [];
|
||||
let t = new Date(startTime);
|
||||
let courtIdx = 0;
|
||||
for(const pair of pairs){
|
||||
const court = courtsArr[courtIdx % courtsArr.length];
|
||||
matches.push({ court, start_time: t.toISOString(), team_a: pair[0].name, team_b: pair[1].name });
|
||||
t = new Date(t.getTime() + slotMinutes*60*1000);
|
||||
courtIdx++;
|
||||
}
|
||||
const inserted = [];
|
||||
for(const m of matches){
|
||||
const r = await query(
|
||||
`INSERT INTO matches(tournament_id, court, start_time, team_a, team_b) VALUES ($1,$2,$3,$4,$5) RETURNING id`,
|
||||
[id, m.court, m.start_time, m.team_a, m.team_b]
|
||||
);
|
||||
inserted.push(r.rows[0].id);
|
||||
}
|
||||
res.json({ created: inserted.length, match_ids: inserted });
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user