All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 45s
Deploy Promiscuity Character API / deploy (push) Successful in 57s
Deploy Promiscuity Inventory API / deploy (push) Successful in 45s
Deploy Promiscuity Locations API / deploy (push) Successful in 58s
k8s smoke test / test (push) Successful in 8s
208 lines
7.3 KiB
C#
208 lines
7.3 KiB
C#
using CharacterApi.Models;
|
|
using CharacterApi.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using System.Net.Http.Headers;
|
|
using System.Security.Claims;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace CharacterApi.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class CharactersController : ControllerBase
|
|
{
|
|
private readonly CharacterStore _characters;
|
|
private readonly IHttpClientFactory _httpClientFactory;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ILogger<CharactersController> _logger;
|
|
|
|
public CharactersController(CharacterStore characters, IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<CharactersController> logger)
|
|
{
|
|
_characters = characters;
|
|
_httpClientFactory = httpClientFactory;
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
}
|
|
|
|
[HttpPost]
|
|
[Authorize(Roles = "USER,SUPER")]
|
|
public async Task<IActionResult> Create([FromBody] CreateCharacterRequest req)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(req.Name))
|
|
return BadRequest("Name required");
|
|
|
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
if (string.IsNullOrWhiteSpace(userId))
|
|
return Unauthorized();
|
|
|
|
var character = new Character
|
|
{
|
|
OwnerUserId = userId,
|
|
Name = req.Name.Trim(),
|
|
Coord = new Coord { X = 0, Y = 0 },
|
|
VisionRadius = 3,
|
|
CreatedUtc = DateTime.UtcNow
|
|
};
|
|
|
|
await _characters.CreateAsync(character);
|
|
return Ok(character);
|
|
}
|
|
|
|
[HttpGet]
|
|
[Authorize(Roles = "USER,SUPER")]
|
|
public async Task<IActionResult> ListMine()
|
|
{
|
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
if (string.IsNullOrWhiteSpace(userId))
|
|
return Unauthorized();
|
|
|
|
var characters = await _characters.GetForOwnerAsync(userId);
|
|
return Ok(characters);
|
|
}
|
|
|
|
[HttpGet("{id}/visible-locations")]
|
|
[Authorize(Roles = "USER,SUPER")]
|
|
public async Task<IActionResult> VisibleLocations(string id)
|
|
{
|
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
if (string.IsNullOrWhiteSpace(userId))
|
|
return Unauthorized();
|
|
|
|
var allowAnyOwner = User.IsInRole("SUPER");
|
|
var character = await _characters.GetByIdAsync(id);
|
|
if (character is null)
|
|
{
|
|
_logger.LogWarning("Visible locations request failed: character {CharacterId} was not found.", id);
|
|
return NotFound();
|
|
}
|
|
|
|
if (!allowAnyOwner && character.OwnerUserId != userId)
|
|
{
|
|
_logger.LogWarning(
|
|
"Visible locations request denied: character {CharacterId} belongs to owner {OwnerUserId}, request user was {UserId}",
|
|
id,
|
|
character.OwnerUserId,
|
|
userId
|
|
);
|
|
return Forbid();
|
|
}
|
|
|
|
_logger.LogInformation(
|
|
"Visible locations requested for character {CharacterId} at ({X},{Y}) radius {VisionRadius} by user {UserId}",
|
|
character.Id,
|
|
character.Coord.X,
|
|
character.Coord.Y,
|
|
character.VisionRadius,
|
|
userId
|
|
);
|
|
|
|
var locationsBaseUrl = (_configuration["Services:LocationsApiBaseUrl"] ?? "http://localhost:5002").TrimEnd('/');
|
|
var internalApiKey = (_configuration["InternalApi:Key"] ?? _configuration["Jwt:Key"] ?? string.Empty).Trim();
|
|
var body = JsonSerializer.Serialize(new
|
|
{
|
|
x = character.Coord.X,
|
|
y = character.Coord.Y,
|
|
radius = character.VisionRadius > 0 ? character.VisionRadius : 3
|
|
});
|
|
|
|
var client = _httpClientFactory.CreateClient();
|
|
using var request = new HttpRequestMessage(HttpMethod.Post, $"{locationsBaseUrl}/api/locations/internal/visible-window");
|
|
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
|
request.Headers.Add("X-Internal-Api-Key", internalApiKey);
|
|
|
|
HttpResponseMessage response;
|
|
try
|
|
{
|
|
response = await client.SendAsync(request);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to reach LocationsApi while resolving visible locations for character {CharacterId}", character.Id);
|
|
return StatusCode(StatusCodes.Status502BadGateway, new
|
|
{
|
|
type = "https://httpstatuses.com/502",
|
|
title = "Bad Gateway",
|
|
status = 502,
|
|
detail = $"Failed to reach LocationsApi at {locationsBaseUrl}.",
|
|
traceId = HttpContext.TraceIdentifier
|
|
});
|
|
}
|
|
|
|
using (response)
|
|
{
|
|
var responseBody = await response.Content.ReadAsStringAsync();
|
|
if (!response.IsSuccessStatusCode)
|
|
return StatusCode((int)response.StatusCode, responseBody);
|
|
|
|
var generation = JsonSerializer.Deserialize<VisibleLocationWindowResponse>(
|
|
responseBody,
|
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
|
if (generation is null)
|
|
{
|
|
return StatusCode(StatusCodes.Status502BadGateway, new
|
|
{
|
|
type = "https://httpstatuses.com/502",
|
|
title = "Bad Gateway",
|
|
status = 502,
|
|
detail = "LocationsApi returned an unreadable visible locations payload.",
|
|
traceId = HttpContext.TraceIdentifier
|
|
});
|
|
}
|
|
|
|
var locations = generation.Locations;
|
|
|
|
_logger.LogInformation(
|
|
"Visible locations resolved for character {CharacterId}: generated {GeneratedCount}, returned {ReturnedCount}",
|
|
character.Id,
|
|
generation.GeneratedCount,
|
|
locations.Count
|
|
);
|
|
|
|
return Ok(locations);
|
|
}
|
|
}
|
|
|
|
[HttpPut("{id}/coord")]
|
|
[Authorize(Roles = "USER,SUPER")]
|
|
public async Task<IActionResult> UpdateCoord(string id, [FromBody] UpdateCharacterCoordRequest req)
|
|
{
|
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
if (string.IsNullOrWhiteSpace(userId))
|
|
return Unauthorized();
|
|
if (req.Coord is null)
|
|
return BadRequest("Coord required");
|
|
|
|
var allowAnyOwner = User.IsInRole("SUPER");
|
|
var character = await _characters.GetByIdAsync(id);
|
|
if (character is null)
|
|
return NotFound();
|
|
if (!allowAnyOwner && character.OwnerUserId != userId)
|
|
return Forbid();
|
|
|
|
character.Coord = req.Coord;
|
|
var updated = await _characters.UpdateCoordAsync(id, req.Coord);
|
|
if (!updated)
|
|
return NotFound();
|
|
|
|
return Ok(character);
|
|
}
|
|
|
|
[HttpDelete("{id}")]
|
|
[Authorize(Roles = "USER,SUPER")]
|
|
public async Task<IActionResult> Delete(string id)
|
|
{
|
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
if (string.IsNullOrWhiteSpace(userId))
|
|
return Unauthorized();
|
|
|
|
var allowAnyOwner = User.IsInRole("SUPER");
|
|
var deleted = await _characters.DeleteForOwnerAsync(id, userId, allowAnyOwner);
|
|
if (!deleted)
|
|
return NotFound();
|
|
|
|
return Ok("Deleted");
|
|
}
|
|
}
|