Initial commit
This commit is contained in:
36
BookieApi/src/Bookie.Api/Controllers/AuthController.cs
Normal file
36
BookieApi/src/Bookie.Api/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Bookie.Application.DTOs.Auth;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IAuthService _auth;
|
||||
|
||||
public AuthController(IAuthService auth) => _auth = auth;
|
||||
|
||||
/// <summary>Exchange username/password for a JWT.</summary>
|
||||
[HttpPost("login")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(AuthResponseDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public ActionResult<AuthResponseDto> Login([FromBody] LoginRequestDto request)
|
||||
{
|
||||
var result = _auth.Login(request);
|
||||
return result is null ? Unauthorized(new { message = "Invalid credentials." }) : Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>Returns the current user's identity from the token.</summary>
|
||||
[HttpGet("me")]
|
||||
[Authorize]
|
||||
public ActionResult<object> Me()
|
||||
=> Ok(new
|
||||
{
|
||||
username = User.Identity?.Name,
|
||||
role = User.FindFirst(System.Security.Claims.ClaimTypes.Role)?.Value
|
||||
});
|
||||
}
|
||||
54
BookieApi/src/Bookie.Api/Controllers/CrudControllerBase.cs
Normal file
54
BookieApi/src/Bookie.Api/Controllers/CrudControllerBase.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Bookie.Application.DTOs.Common;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Shared CRUD endpoints. Reads require any authenticated user; writes require the admin role.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public abstract class CrudControllerBase<TRead, TCreate, TUpdate> : ControllerBase
|
||||
{
|
||||
protected readonly ICrudService<TRead, TCreate, TUpdate, int> Service;
|
||||
|
||||
protected CrudControllerBase(ICrudService<TRead, TCreate, TUpdate, int> service) => Service = service;
|
||||
|
||||
/// <summary>Paged list with optional search + sorting.</summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<TRead>>> GetPaged([FromQuery] PagedQuery query, CancellationToken ct)
|
||||
=> Ok(await Service.GetPagedAsync(query, ct));
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
public async Task<ActionResult<TRead>> GetById(int id, CancellationToken ct)
|
||||
{
|
||||
var item = await Service.GetByIdAsync(id, ct);
|
||||
return item is null ? NotFound() : Ok(item);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<ActionResult<TRead>> Create([FromBody] TCreate dto, CancellationToken ct)
|
||||
{
|
||||
var created = await Service.CreateAsync(dto, ct);
|
||||
return StatusCode(StatusCodes.Status201Created, created);
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<ActionResult<TRead>> Update(int id, [FromBody] TUpdate dto, CancellationToken ct)
|
||||
{
|
||||
var updated = await Service.UpdateAsync(id, dto, ct);
|
||||
return updated is null ? NotFound() : Ok(updated);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(int id, CancellationToken ct)
|
||||
{
|
||||
var ok = await Service.DeleteAsync(id, ct);
|
||||
return ok ? NoContent() : NotFound();
|
||||
}
|
||||
}
|
||||
21
BookieApi/src/Bookie.Api/Controllers/DashboardController.cs
Normal file
21
BookieApi/src/Bookie.Api/Controllers/DashboardController.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Bookie.Application.DTOs.Dashboard;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/dashboard")]
|
||||
[Authorize]
|
||||
public class DashboardController : ControllerBase
|
||||
{
|
||||
private readonly IDashboardService _dashboard;
|
||||
|
||||
public DashboardController(IDashboardService dashboard) => _dashboard = dashboard;
|
||||
|
||||
[HttpGet("summary")]
|
||||
[ProducesResponseType(typeof(DashboardSummaryDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<DashboardSummaryDto>> GetSummary(CancellationToken ct)
|
||||
=> Ok(await _dashboard.GetSummaryAsync(ct));
|
||||
}
|
||||
11
BookieApi/src/Bookie.Api/Controllers/LeaguesController.cs
Normal file
11
BookieApi/src/Bookie.Api/Controllers/LeaguesController.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Bookie.Application.DTOs.Leagues;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[Route("api/leagues")]
|
||||
public class LeaguesController : CrudControllerBase<LeagueDto, LeagueCreateDto, LeagueUpdateDto>
|
||||
{
|
||||
public LeaguesController(ILeagueService service) : base(service) { }
|
||||
}
|
||||
36
BookieApi/src/Bookie.Api/Controllers/LookupsController.cs
Normal file
36
BookieApi/src/Bookie.Api/Controllers/LookupsController.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Bookie.Application.DTOs.Common;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/lookups")]
|
||||
[Authorize]
|
||||
public class LookupsController : ControllerBase
|
||||
{
|
||||
private readonly ILookupService _lookups;
|
||||
|
||||
public LookupsController(ILookupService lookups) => _lookups = lookups;
|
||||
|
||||
[HttpGet("leagues")]
|
||||
public async Task<ActionResult<IReadOnlyList<LookupDto>>> Leagues(CancellationToken ct)
|
||||
=> Ok(await _lookups.LeaguesAsync(ct));
|
||||
|
||||
[HttpGet("seasons")]
|
||||
public async Task<ActionResult<IReadOnlyList<LookupDto>>> Seasons([FromQuery] int? leagueId, CancellationToken ct)
|
||||
=> Ok(await _lookups.SeasonsAsync(leagueId, ct));
|
||||
|
||||
[HttpGet("teams")]
|
||||
public async Task<ActionResult<IReadOnlyList<LookupDto>>> Teams([FromQuery] int? leagueId, [FromQuery] int? seasonId, CancellationToken ct)
|
||||
=> Ok(await _lookups.TeamsAsync(leagueId, seasonId, ct));
|
||||
|
||||
[HttpGet("players")]
|
||||
public async Task<ActionResult<IReadOnlyList<LookupDto>>> Players(CancellationToken ct)
|
||||
=> Ok(await _lookups.PlayersAsync(ct));
|
||||
|
||||
[HttpGet("matchdays")]
|
||||
public async Task<ActionResult<IReadOnlyList<LookupDto>>> Matchdays(CancellationToken ct)
|
||||
=> Ok(await _lookups.MatchdaysAsync(ct));
|
||||
}
|
||||
11
BookieApi/src/Bookie.Api/Controllers/MatchesController.cs
Normal file
11
BookieApi/src/Bookie.Api/Controllers/MatchesController.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Bookie.Application.DTOs.Matches;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[Route("api/matches")]
|
||||
public class MatchesController : CrudControllerBase<MatchDto, MatchCreateDto, MatchUpdateDto>
|
||||
{
|
||||
public MatchesController(IMatchService service) : base(service) { }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Bookie.Application.DTOs.Contracts;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[Route("api/player-contracts")]
|
||||
public class PlayerContractsController : CrudControllerBase<PlayerContractDto, PlayerContractCreateDto, PlayerContractUpdateDto>
|
||||
{
|
||||
private readonly IPlayerContractService _contracts;
|
||||
|
||||
public PlayerContractsController(IPlayerContractService service) : base(service) => _contracts = service;
|
||||
|
||||
/// <summary>All contracts for a given player.</summary>
|
||||
[HttpGet("by-player/{playerId:int}")]
|
||||
public async Task<ActionResult<IReadOnlyList<PlayerContractDto>>> GetByPlayer(int playerId, CancellationToken ct)
|
||||
=> Ok(await _contracts.GetByPlayerAsync(playerId, ct));
|
||||
}
|
||||
11
BookieApi/src/Bookie.Api/Controllers/PlayersController.cs
Normal file
11
BookieApi/src/Bookie.Api/Controllers/PlayersController.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Bookie.Application.DTOs.Players;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[Route("api/players")]
|
||||
public class PlayersController : CrudControllerBase<PlayerDto, PlayerCreateDto, PlayerUpdateDto>
|
||||
{
|
||||
public PlayersController(IPlayerService service) : base(service) { }
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Bookie.Application.DTOs.Predictions;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/predictions")]
|
||||
[Authorize]
|
||||
public class PredictionsController : ControllerBase
|
||||
{
|
||||
private readonly IPredictionService _service;
|
||||
private readonly ILogger<PredictionsController> _logger;
|
||||
|
||||
public PredictionsController(IPredictionService service, ILogger<PredictionsController> logger)
|
||||
{
|
||||
_service = service;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Statistical projection (goals, cards, fouls, corners, shots on target) for a single
|
||||
/// fixture, produced by the OpenAI-backed Football Match Statistics Predictor.
|
||||
/// </summary>
|
||||
[HttpPost("match/{matchId:int}")]
|
||||
[ProducesResponseType(typeof(MatchPredictionDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status502BadGateway)]
|
||||
public async Task<ActionResult<MatchPredictionDto>> PredictMatch(int matchId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var prediction = await _service.PredictMatchAsync(matchId, ct);
|
||||
return prediction is null ? NotFound() : Ok(prediction);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// Configuration or upstream OpenAI failures — surface a clean message to the client.
|
||||
_logger.LogError(ex, "Prediction failed for match {MatchId}", matchId);
|
||||
return Problem(title: "Prediction unavailable", detail: ex.Message, statusCode: StatusCodes.Status502BadGateway);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch projection for several fixtures in a single model call. The predictor prompt is
|
||||
/// batch-oriented, so this produces consistent, cross-checked estimates across the selection.
|
||||
/// </summary>
|
||||
[HttpPost("matches")]
|
||||
[ProducesResponseType(typeof(IEnumerable<MatchPredictionDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status502BadGateway)]
|
||||
public async Task<ActionResult<IEnumerable<MatchPredictionDto>>> PredictMatches(
|
||||
[FromBody] PredictMatchesRequest request, CancellationToken ct)
|
||||
{
|
||||
if (request?.MatchIds is null || request.MatchIds.Count == 0)
|
||||
return BadRequest("Provide at least one match id.");
|
||||
if (request.MatchIds.Count > 20)
|
||||
return BadRequest("A maximum of 20 matches can be predicted per request.");
|
||||
|
||||
try
|
||||
{
|
||||
var predictions = await _service.PredictMatchesAsync(request.MatchIds, ct);
|
||||
return Ok(predictions);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Batch prediction failed for {Count} matches", request.MatchIds.Count);
|
||||
return Problem(title: "Prediction unavailable", detail: ex.Message, statusCode: StatusCodes.Status502BadGateway);
|
||||
}
|
||||
}
|
||||
|
||||
public class PredictMatchesRequest
|
||||
{
|
||||
public List<int> MatchIds { get; set; } = new();
|
||||
}
|
||||
}
|
||||
57
BookieApi/src/Bookie.Api/Controllers/ReportsController.cs
Normal file
57
BookieApi/src/Bookie.Api/Controllers/ReportsController.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Bookie.Application.DTOs.Reports;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/reports")]
|
||||
[Authorize]
|
||||
public class ReportsController : ControllerBase
|
||||
{
|
||||
private readonly IPreMatchReportService _service;
|
||||
|
||||
public ReportsController(IPreMatchReportService service) => _service = service;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-Match Report: all matches on the given date grouped by league/country/season,
|
||||
/// with per-team historical stats computed only from earlier matches in the same season.
|
||||
/// </summary>
|
||||
/// <param name="date">Target day in YYYY-MM-DD. Defaults to today.</param>
|
||||
/// <param name="seasonsBack">
|
||||
/// How many previous seasons of the same league to fold into the stats.
|
||||
/// 0 (default) = current season only; 1 = current + previous, etc.
|
||||
/// </param>
|
||||
[HttpGet("pre-match")]
|
||||
[ProducesResponseType(typeof(PreMatchReportDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PreMatchReportDto>> GetPreMatch(
|
||||
[FromQuery] DateOnly? date, [FromQuery] int seasonsBack = 0, CancellationToken ct = default)
|
||||
{
|
||||
var target = date ?? DateOnly.FromDateTime(DateTime.Today);
|
||||
var report = await _service.GetPreMatchReportAsync(target, seasonsBack, ct);
|
||||
return Ok(report);
|
||||
}
|
||||
|
||||
/// <summary>Full statistics for a single match (team stats, goals, cards, penalties).</summary>
|
||||
[HttpGet("match/{matchId:int}")]
|
||||
[ProducesResponseType(typeof(MatchDetailsDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<MatchDetailsDto>> GetMatchDetails(int matchId, CancellationToken ct)
|
||||
{
|
||||
var details = await _service.GetMatchDetailsAsync(matchId, ct);
|
||||
return details is null ? NotFound() : Ok(details);
|
||||
}
|
||||
|
||||
/// <summary>Historical head-to-head record between two teams (previous meetings before a given match).</summary>
|
||||
[HttpGet("head-to-head")]
|
||||
[ProducesResponseType(typeof(HeadToHeadDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<HeadToHeadDto>> GetHeadToHead(
|
||||
[FromQuery] int teamAId, [FromQuery] int teamBId,
|
||||
[FromQuery] int? beforeMatchId, CancellationToken ct)
|
||||
{
|
||||
var h2h = await _service.GetHeadToHeadAsync(teamAId, teamBId, beforeMatchId, ct);
|
||||
return h2h is null ? NotFound() : Ok(h2h);
|
||||
}
|
||||
}
|
||||
18
BookieApi/src/Bookie.Api/Controllers/SeasonsController.cs
Normal file
18
BookieApi/src/Bookie.Api/Controllers/SeasonsController.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Bookie.Application.DTOs.Seasons;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[Route("api/seasons")]
|
||||
public class SeasonsController : CrudControllerBase<SeasonDto, SeasonCreateDto, SeasonUpdateDto>
|
||||
{
|
||||
private readonly ISeasonService _seasons;
|
||||
|
||||
public SeasonsController(ISeasonService service) : base(service) => _seasons = service;
|
||||
|
||||
/// <summary>All seasons for a given league (for dropdowns).</summary>
|
||||
[HttpGet("by-league/{leagueId:int}")]
|
||||
public async Task<ActionResult<IReadOnlyList<SeasonDto>>> GetByLeague(int leagueId, CancellationToken ct)
|
||||
=> Ok(await _seasons.GetByLeagueAsync(leagueId, ct));
|
||||
}
|
||||
35
BookieApi/src/Bookie.Api/Controllers/StatsController.cs
Normal file
35
BookieApi/src/Bookie.Api/Controllers/StatsController.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Bookie.Application.DTOs.Stats;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class StatsController : ControllerBase
|
||||
{
|
||||
private readonly IStatsService _service;
|
||||
|
||||
public StatsController(IStatsService service) => _service = service;
|
||||
|
||||
/// <summary>Aggregated career statistics for a single player (goals, assists, cards, per-season, recent goals).</summary>
|
||||
[HttpGet("api/players/{playerId:int}/stats")]
|
||||
[ProducesResponseType(typeof(PlayerStatsDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PlayerStatsDto>> GetPlayerStats(int playerId, CancellationToken ct)
|
||||
{
|
||||
var stats = await _service.GetPlayerStatsAsync(playerId, ct);
|
||||
return stats is null ? NotFound() : Ok(stats);
|
||||
}
|
||||
|
||||
/// <summary>Aggregated statistics for a single team (record, averages, form, top scorers, per-season, recent matches).</summary>
|
||||
[HttpGet("api/teams/{teamId:int}/stats")]
|
||||
[ProducesResponseType(typeof(TeamStatsDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<TeamStatsDto>> GetTeamStats(int teamId, CancellationToken ct)
|
||||
{
|
||||
var stats = await _service.GetTeamStatsAsync(teamId, ct);
|
||||
return stats is null ? NotFound() : Ok(stats);
|
||||
}
|
||||
}
|
||||
18
BookieApi/src/Bookie.Api/Controllers/TeamsController.cs
Normal file
18
BookieApi/src/Bookie.Api/Controllers/TeamsController.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Bookie.Application.DTOs.Teams;
|
||||
using Bookie.Application.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bookie.Api.Controllers;
|
||||
|
||||
[Route("api/teams")]
|
||||
public class TeamsController : CrudControllerBase<TeamDto, TeamCreateDto, TeamUpdateDto>
|
||||
{
|
||||
private readonly ITeamService _teams;
|
||||
|
||||
public TeamsController(ITeamService service) : base(service) => _teams = service;
|
||||
|
||||
/// <summary>Unpaged list of all teams (for dropdowns).</summary>
|
||||
[HttpGet("all")]
|
||||
public async Task<ActionResult<IReadOnlyList<TeamDto>>> GetAll(CancellationToken ct)
|
||||
=> Ok(await _teams.GetAllAsync(ct));
|
||||
}
|
||||
Reference in New Issue
Block a user