Zeeshaun 52a76625a3
All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 45s
Deploy Promiscuity Character API / deploy (push) Successful in 45s
Deploy Promiscuity Inventory API / deploy (push) Successful in 45s
Deploy Promiscuity Locations API / deploy (push) Successful in 1m0s
k8s smoke test / test (push) Successful in 8s
Location error handling
2026-03-19 16:54:06 -05:00

198 lines
7.0 KiB
C#

using LocationsApi.Models;
using LocationsApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
namespace LocationsApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class LocationsController : ControllerBase
{
private readonly LocationStore _locations;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
private readonly ILogger<LocationsController> _logger;
public LocationsController(LocationStore locations, IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<LocationsController> logger)
{
_locations = locations;
_httpClientFactory = httpClientFactory;
_configuration = configuration;
_logger = logger;
}
[HttpPost]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> Create([FromBody] CreateLocationRequest req)
{
if (string.IsNullOrWhiteSpace(req.Name))
return BadRequest("Name required");
if (req.Coord is null)
return BadRequest("Coord required");
var location = new Location
{
Name = req.Name.Trim(),
Coord = req.Coord,
CreatedUtc = DateTime.UtcNow
};
try
{
await _locations.CreateAsync(location);
}
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
return Conflict("Coord must be unique");
}
catch (MongoWriteException ex) when (ex.WriteError.Code == 121)
{
return BadRequest("Location document failed validation");
}
return Ok(location);
}
[HttpGet]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> ListMine()
{
var locations = await _locations.GetAllAsync();
return Ok(locations);
}
[HttpDelete("{id}")]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> Delete(string id)
{
var deleted = await _locations.DeleteAsync(id);
if (!deleted)
return NotFound();
return Ok("Deleted");
}
[HttpPut("{id}")]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> Update(string id, [FromBody] UpdateLocationRequest req)
{
if (string.IsNullOrWhiteSpace(req.Name))
return BadRequest("Name required");
if (req.Coord is not null)
return BadRequest("Coord cannot be updated");
var updated = await _locations.UpdateNameAsync(id, req.Name.Trim());
if (!updated)
return NotFound();
return Ok("Updated");
}
[HttpPost("{id}/interact")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> Interact(string id, [FromBody] InteractLocationObjectRequest req)
{
if (string.IsNullOrWhiteSpace(req.CharacterId))
return BadRequest("characterId required");
if (string.IsNullOrWhiteSpace(req.ObjectId))
return BadRequest("objectId required");
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var allowAnyOwner = User.IsInRole("SUPER");
var interact = await _locations.InteractWithObjectAsync(id, req.CharacterId, req.ObjectId, userId, allowAnyOwner);
if (interact.Status == InteractStatus.LocationNotFound)
return NotFound("Location not found");
if (interact.Status == InteractStatus.CharacterNotFound)
return NotFound("Character not found");
if (interact.Status == InteractStatus.Forbidden)
return Forbid();
if (interact.Status == InteractStatus.Invalid)
return BadRequest("Character is not at the target location");
if (interact.Status == InteractStatus.ObjectNotFound)
return NotFound("Location object not found");
if (interact.Status == InteractStatus.UnsupportedObjectType)
return BadRequest("Location object type is not supported");
if (interact.Status == InteractStatus.ObjectConsumed)
return Conflict("Location object is consumed");
var inventoryBaseUrl = (_configuration["Services:InventoryApiBaseUrl"] ?? "http://localhost:5003").TrimEnd('/');
var token = Request.Headers.Authorization.ToString();
var grantBody = JsonSerializer.Serialize(new
{
itemKey = interact.ItemKey,
quantity = interact.QuantityGranted
});
var client = _httpClientFactory.CreateClient();
using var request = new HttpRequestMessage(
HttpMethod.Post,
$"{inventoryBaseUrl}/api/inventory/by-owner/character/{req.CharacterId}/grant");
request.Content = new StringContent(grantBody, Encoding.UTF8, "application/json");
if (!string.IsNullOrWhiteSpace(token))
request.Headers.Authorization = AuthenticationHeaderValue.Parse(token);
HttpResponseMessage response;
try
{
response = await client.SendAsync(request);
}
catch (HttpRequestException ex)
{
if (interact.PreviousObject is not null)
await _locations.RestoreObjectInteractionAsync(id, interact.PreviousObject);
_logger.LogError(
ex,
"Failed to reach InventoryApi while processing location interaction for location {LocationId}, character {CharacterId}, object {ObjectId}",
id,
req.CharacterId,
req.ObjectId
);
return StatusCode(StatusCodes.Status502BadGateway, new
{
type = "https://httpstatuses.com/502",
title = "Bad Gateway",
status = 502,
detail = $"Failed to reach InventoryApi at {inventoryBaseUrl}.",
traceId = HttpContext.TraceIdentifier
});
}
using (response)
{
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
if (interact.PreviousObject is not null)
await _locations.RestoreObjectInteractionAsync(id, interact.PreviousObject);
return StatusCode((int)response.StatusCode, responseBody);
}
return Ok(new InteractLocationObjectResponse
{
LocationId = id,
CharacterId = req.CharacterId,
ObjectId = interact.ObjectId,
ObjectType = interact.ObjectType,
ItemKey = interact.ItemKey,
QuantityGranted = interact.QuantityGranted,
RemainingQuantity = interact.RemainingQuantity,
Consumed = interact.Consumed,
InventoryResponseJson = responseBody
});
}
}
}