Files
Dhananjaya99 bffc9b1686 first commit
2026-09-28 11:44:26 +05:30

154 lines
6.8 KiB
C#

using Fitness_core_BE.Auth;
using Fitness_core_BE.DTOs.Agent;
using Fitness_core_BE.Exceptions;
using Fitness_core_BE.Interfaces.Services;
using Fitness_core_BE.Models.IOs;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace Fitness_core_BE.Controllers
{
/// <summary>
/// Endpoints consumed by the on-prem desktop agent. The agent talks to the Hikvision
/// device on the gym LAN and to us over outbound HTTPS using its X-Api-Key.
/// All endpoints except enrollment require the "AgentApiKey" policy; the agent's client
/// is always resolved server-side from the authenticated key, never from the request body.
/// </summary>
[Route("api/agent")]
[ApiController]
[Produces("application/json")]
[EnableCors("AllowAll")]
public class AgentController : ControllerBase
{
private readonly IAgentService _agentService;
private readonly ITimeBankCardService _timeBankCardService;
private readonly ILogger<AgentController> _logger;
public AgentController(IAgentService agentService, ITimeBankCardService timeBankCardService, ILogger<AgentController> logger)
{
_agentService = agentService;
_timeBankCardService = timeBankCardService;
_logger = logger;
}
// Bootstrap: no api-key yet. Exchanges a pairing code for a new agent + api key.
[HttpPost("enroll")]
[AllowAnonymous]
public Task<IActionResult> Enroll([FromBody] EnrollRequest request)
=> Execute(() => _agentService.EnrollAsync(request));
[HttpPost("devices")]
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
public Task<IActionResult> RegisterDevice([FromBody] RegisterDeviceRequest request)
=> Execute(() => _agentService.RegisterDeviceAsync(GetClientId(), GetAgentId(), request));
[HttpPost("device-inventory")]
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
public Task<IActionResult> ReconcileDeviceInventory([FromBody] DeviceInventoryRequest request)
=> Execute(() => _agentService.ReconcileDeviceInventoryAsync(
GetClientId(), GetAgentId(), request));
[HttpPost("attendance")]
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
public Task<IActionResult> Attendance([FromBody] AgentAttendanceRequest request)
{
_logger.LogInformation(
"Attendance endpoint hit: device {DeviceId}, {EventCount} events in body.",
request?.DeviceId, request?.Events?.Count ?? 0);
return Execute(() => _agentService.IngestAttendanceAsync(
GetClientId(), GetAgentId(), request));
}
[HttpGet("commands")]
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
public Task<IActionResult> GetCommands()
=> Execute(() => _agentService.GetPendingCommandsAsync(GetClientId(), GetAgentId()));
[HttpPost("commands/{id}/result")]
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
public Task<IActionResult> SubmitCommandResult(Guid id, [FromBody] CommandResultRequest request)
=> Execute(() => _agentService.SubmitCommandResultAsync(
GetClientId(), GetAgentId(), id, request));
// Called by an RFID-reader-equipped agent station on a card swipe -- same
// "agent resolves the club from its own API key" pattern as attendance/commands, so a
// reader never needs staff credentials of its own. facilityId identifies which area the
// reader is stationed at. First swipe on a card taps it in (starts the clock); the next
// swipe taps it out and bills the elapsed time -- see TimeBankCardService.Swipe.
[HttpPost("time-bank-cards/redeem")]
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
public Task<IActionResult> RedeemTimeBankCard([FromBody] AgentSwipeTimeBankCardRequest request)
=> Execute(() => _timeBankCardService.Swipe(new
{
rfidTagId = request.RfidTagId,
facilityId = request.FacilityId
}));
[HttpPost("heartbeat")]
[Authorize(Policy = AgentApiKeyDefaults.PolicyName)]
public async Task<IActionResult> Heartbeat()
{
// Accept an empty body from older agents during a rolling deployment.
var request = Request.HasJsonContentType()
? await Request.ReadFromJsonAsync<AgentHeartbeatRequest>()
: null;
return await Execute(() => _agentService.HeartbeatAsync(
GetClientId(), GetAgentId(), request ?? new AgentHeartbeatRequest()));
}
// ---- helpers ----
private Guid GetClientId()
{
var value = User.FindFirst(AgentApiKeyDefaults.ClientIdClaim)?.Value;
if (string.IsNullOrWhiteSpace(value) || !Guid.TryParse(value, out var clientId))
throw new UnauthorizedException("Agent is not authenticated.");
return clientId;
}
private Guid GetAgentId()
{
var value = User.FindFirst(AgentApiKeyDefaults.AgentIdClaim)?.Value;
if (string.IsNullOrWhiteSpace(value) || !Guid.TryParse(value, out var agentId))
throw new UnauthorizedException("Agent is not authenticated.");
return agentId;
}
// Mirrors the function-handler exception->ApiResponse mapping used by admin flows.
private async Task<IActionResult> Execute(Func<Task<object>> action)
{
try
{
var data = await action();
return StatusCode(200, new ApiResponse<object>
{
StatusCode = 200,
Message = "success",
Data = data
});
}
catch (UnauthorizedException ex)
{
return StatusCode(401, new ApiResponse<object> { StatusCode = 401, Message = ex.Message, Data = null! });
}
catch (NotFoundException ex)
{
return StatusCode(404, new ApiResponse<object> { StatusCode = 404, Message = ex.Message, Data = null! });
}
catch (BadRequestException ex)
{
return StatusCode(400, new ApiResponse<object> { StatusCode = 400, Message = ex.Message, Data = null! });
}
catch (ForbiddenException ex)
{
return StatusCode(403, new ApiResponse<object> { StatusCode = 403, Message = ex.Message, Data = null! });
}
catch (Exception ex)
{
return StatusCode(500, new ApiResponse<object> { StatusCode = 500, Message = $"An error occurred: {ex.Message}", Data = null! });
}
}
}
}