This commit is contained in:
2026-02-11 23:54:42 +01:00 Unverified
commit ace6f5a022
47 changed files with 5315 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# backend/app/routes/__init__.py
+48
View File
@@ -0,0 +1,48 @@
# backend/app/routes/auth.py
from datetime import timedelta
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from ..schemas import Token
from ..core.auth import create_access_token, get_current_user
from ..core.config import (
ACCESS_TOKEN_EXPIRE_MINUTES,
ADMIN_HASH,
ADMIN_USER,
verify_password,
)
router = APIRouter(prefix="/auth", tags=["Auth"])
@router.post("/token", response_model=Token)
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
):
if form_data.username != ADMIN_USER:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not verify_password(form_data.password, ADMIN_HASH):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": form_data.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@router.get("/check")
async def check_auth(user: str = Depends(get_current_user)):
return {"is_admin": True, "user": user}
@@ -0,0 +1,6 @@
# backend/app/routes/tournaments/__init__.py
from fastapi import APIRouter
router = APIRouter(prefix="/tournaments", tags=["Tournaments"])
from . import report, courts, matches, teams, tournaments
+61
View File
@@ -0,0 +1,61 @@
# backend/app/routes/tournaments/courts.py
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from ... import crud, schemas
from ...constants import SUCCESS
from ...core.websocket_manager import send_ws_update
from ...core.auth import get_current_user
from ...database import get_db
from . import router
@router.get("/{id}/courts", response_model=list[schemas.CourtSchema])
def get_courts(id: str, db: Session = Depends(get_db)):
return crud.get_courts(db, id)
@router.post("/{id}/courts", response_model=schemas.CourtSchema)
async def create_court(
id: str,
court: schemas.CourtCreate,
db: Session = Depends(get_db),
user: str = Depends(get_current_user),
):
new_court = crud.create_court(db, id, court)
if not new_court:
raise HTTPException(404, "Tournament not found")
await send_ws_update(id)
return new_court
@router.patch("/{id}/courts", response_model=schemas.TournamentDetail)
async def update_courts(
id: str,
courts: list[str],
db: Session = Depends(get_db),
user: str = Depends(get_current_user),
):
t = crud.update_tournament_courts(db, id, courts)
if not t:
raise HTTPException(404, "Not found")
await send_ws_update(id)
return t
@router.delete("/{id}/courts/{court_id}")
async def delete_court(
id: str,
court_id: int,
db: Session = Depends(get_db),
user: str = Depends(get_current_user),
):
success = crud.delete_court(db, id, court_id)
if not success:
raise HTTPException(404, "Court or Tournament not found")
await send_ws_update(id)
return SUCCESS
+24
View File
@@ -0,0 +1,24 @@
# backend/app/routes/tournaments/matches.py
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from ... import crud, schemas
from ...database import get_db
from . import router
@router.get("/{id}/matches", response_model=list[schemas.MatchOut])
def get_tournament_matches(id: str, db: Session = Depends(get_db)):
matches = crud.get_tournament_matches(db, id)
return matches
@router.get("/{id}/matches/{match_id}", response_model=schemas.MatchOut)
def get_match_details(id: str, match_id: str, db: Session = Depends(get_db)):
match = crud.get_match(db, id, match_id)
if not match:
raise HTTPException(404, "Match not found")
return match
+145
View File
@@ -0,0 +1,145 @@
# backend/app/routes/tournaments/report.py
from typing import List, Optional
from fastapi import Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import flag_modified
from ... import crud, logic, models, schemas
from ...constants import SUCCESS, MatchStatus
from ...core.auth import get_optional_user
from ...core.websocket_manager import send_ws_update
from ...database import get_db
from . import router
# --- Helper: Centralize Auth Logic ---
def _check_auth(t: models.Tournament, user: Optional[str], code: Optional[str]):
is_admin = user is not None
code_matches = code is not None and str(code).strip() == str(t.code).strip()
if not is_admin and not code_matches:
raise HTTPException(403, "Invalid tournament code or admin privileges required")
@router.post("/{id}/matches/{match_id}/score")
async def report_score(
id: str,
match_id: str,
report: schemas.ScoreReport,
db: Session = Depends(get_db),
user: Optional[str] = Depends(get_optional_user),
):
t = crud.get_tournament(db, id)
if not t:
raise HTTPException(404, "Tournament not found")
_check_auth(t, user, report.code)
match = crud.get_match(db, id, match_id)
if not match:
raise HTTPException(404, "Match not found")
if not report.sets:
raise HTTPException(400, "No sets submitted")
_apply_score(match, report.sets)
flag_modified(match, "sets")
logic.refresh_bracket(t)
logic.update_schedule(t)
db.commit()
await send_ws_update(id)
return SUCCESS
@router.patch("/{id}/matches/{match_id}/score")
async def edit_score(
id: str,
match_id: str,
report: schemas.ScoreReport,
db: Session = Depends(get_db),
user: Optional[str] = Depends(get_optional_user),
):
"""
Allows correcting a score without resetting the match status logic entirely,
or just re-applying the new sets.
"""
t = crud.get_tournament(db, id)
if not t:
raise HTTPException(404, "Tournament not found")
_check_auth(t, user, report.code)
match = crud.get_match(db, id, match_id)
if not match:
raise HTTPException(404, "Match not found")
if report.sets:
_apply_score(match, report.sets)
flag_modified(match, "sets")
logic.refresh_bracket(t)
logic.update_schedule(t)
db.commit()
await send_ws_update(id)
return SUCCESS
@router.delete("/{id}/matches/{match_id}/score")
async def clear_score(
id: str,
match_id: str,
code: Optional[str] = Query(None),
db: Session = Depends(get_db),
user: Optional[str] = Depends(get_optional_user),
):
t = crud.get_tournament(db, id)
if not t:
raise HTTPException(404, "Tournament not found")
_check_auth(t, user, code)
match = crud.get_match(db, id, match_id)
if not match:
raise HTTPException(404, "Match not found")
match.winner = None
match.status = MatchStatus.PENDING.value
match.sets = []
flag_modified(match, "sets")
logic.refresh_bracket(t)
logic.update_schedule(t)
db.commit()
await send_ws_update(id)
return SUCCESS
def _apply_score(match: models.Match, sets: List[schemas.SetScore]):
"""
Calculates winner based on sets and updates the match object.
Does NOT commit to DB.
"""
p1_wins = sum(1 for s in sets if s.p1 > s.p2)
p2_wins = sum(1 for s in sets if s.p2 > s.p1)
if p1_wins > p2_wins:
match.winner = match.p1_name
elif p2_wins > p1_wins:
match.winner = match.p2_name
else:
p1_points = sum(s.p1 for s in sets)
p2_points = sum(s.p2 for s in sets)
if p1_points == p2_points:
raise HTTPException(400, "Absolute tie: Sets and Points are equal.")
match.winner = match.p1_name if p1_points > p2_points else match.p2_name
match.status = MatchStatus.FINISHED.value
match.sets = [s.model_dump() for s in sets]
+61
View File
@@ -0,0 +1,61 @@
# backend/app/routes/tournaments/teams.py
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from ... import crud, schemas
from ...constants import SUCCESS
from ...core.websocket_manager import send_ws_update
from ...core.auth import get_current_user
from ...database import get_db
from . import router
@router.get("/{id}/teams", response_model=list[schemas.TeamSchema])
def get_teams(id: str, db: Session = Depends(get_db)):
return crud.get_teams(db, id)
@router.post("/{id}/teams", response_model=schemas.TeamSchema)
async def create_team(
id: str,
team: schemas.TeamCreate,
db: Session = Depends(get_db),
user: str = Depends(get_current_user),
):
new_team = crud.create_team(db, id, team)
if not new_team:
raise HTTPException(404, "Tournament not found")
await send_ws_update(id)
return new_team
@router.patch("/{id}/teams", response_model=list[schemas.TeamSchema])
async def update_teams(
id: str,
teams: list[str],
db: Session = Depends(get_db),
user: str = Depends(get_current_user),
):
t = crud.update_tournament_teams(db, id, teams)
if not t:
raise HTTPException(404, "Not found")
await send_ws_update(id)
return t.teams
@router.delete("/{id}/teams/{team_id}")
async def delete_team(
id: str,
team_id: int,
db: Session = Depends(get_db),
user: str = Depends(get_current_user),
):
success = crud.delete_team(db, id, team_id)
if not success:
raise HTTPException(404, "Team or Tournament not found")
await send_ws_update(id)
return SUCCESS
@@ -0,0 +1,62 @@
# backend/app/routes/tournaments/tournaments.py
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from ... import crud, schemas
from ...constants import SUCCESS
from ...core.websocket_manager import send_ws_update
from ...database import get_db
from ...core.auth import get_current_user
from . import router
@router.post("", response_model=schemas.TournamentOut)
async def create_tournament(
data: schemas.TournamentCreate,
db: Session = Depends(get_db),
user: str = Depends(get_current_user),
):
new_t = crud.create_tournament(db, data)
await send_ws_update(new_t.id)
return new_t
@router.get("", response_model=list[schemas.TournamentOut])
def list_tournaments(db: Session = Depends(get_db)):
return crud.get_tournaments(db)
@router.get("/{id}", response_model=schemas.TournamentDetail)
def get_tournament(id: str, db: Session = Depends(get_db)):
t = crud.get_tournament(db, id)
if not t:
raise HTTPException(404, "Tournament not found")
return t
@router.patch("/{id}", response_model=schemas.TournamentUpdateResponse)
async def update_settings(
id: str,
data: schemas.TournamentUpdate,
db: Session = Depends(get_db),
user: str = Depends(get_current_user),
):
t = crud.update_tournament_details(db, id, data)
if not t:
raise HTTPException(404, "Not found")
await send_ws_update(id)
return t
@router.delete("/{id}")
async def delete_tournament(
id: str, db: Session = Depends(get_db), user: str = Depends(get_current_user)
):
success = crud.delete_tournament(db, id)
if not success:
raise HTTPException(404, "Tournament not found")
await send_ws_update(id)
return SUCCESS
+16
View File
@@ -0,0 +1,16 @@
# backend/app/routes/websocket.py
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from ..core.websocket_manager import manager
router = APIRouter(prefix="/ws", tags=["Websocket"])
@router.websocket("/")
async def websocket_endpoint(websocket: WebSocket):
await manager.connect(websocket)
try:
while True:
await websocket.receive_text()
except WebSocketDisconnect:
manager.disconnect(websocket)