Zeeshaun 038981d7b1
All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 46s
Deploy Promiscuity Character API / deploy (push) Successful in 57s
Deploy Promiscuity Inventory API / deploy (push) Successful in 59s
Deploy Promiscuity Locations API / deploy (push) Successful in 58s
k8s smoke test / test (push) Successful in 8s
Speeding up visible location call
2026-03-20 09:50:17 -05:00

344 lines
14 KiB
C#

using InventoryApi.Models;
using InventoryApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace InventoryApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class InventoryController : ControllerBase
{
private readonly InventoryStore _inventory;
public InventoryController(InventoryStore inventory)
{
_inventory = inventory;
}
[HttpPost("internal/by-owner/{ownerType}")]
public async Task<IActionResult> GetByOwnersInternal(string ownerType, [FromBody] InternalOwnerInventoryBatchRequest req)
{
var configuredKey = (HttpContext.RequestServices.GetRequiredService<IConfiguration>()["InternalApi:Key"]
?? HttpContext.RequestServices.GetRequiredService<IConfiguration>()["Jwt:Key"]
?? string.Empty).Trim();
var requestKey = (Request.Headers["X-Internal-Api-Key"].FirstOrDefault() ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(configuredKey) || !string.Equals(configuredKey, requestKey, StringComparison.Ordinal))
return Unauthorized();
var normalizedOwnerType = ownerType.Trim().ToLowerInvariant();
if (normalizedOwnerType is not ("character" or "location"))
return BadRequest("Unsupported ownerType");
var groupedItems = await _inventory.GetByOwnersAsync(normalizedOwnerType, req.OwnerIds);
var ownerIds = req.OwnerIds
.Where(id => !string.IsNullOrWhiteSpace(id))
.Select(id => id.Trim())
.Distinct(StringComparer.Ordinal)
.ToList();
var response = ownerIds.Select(ownerId => new OwnerInventorySummaryResponse
{
OwnerType = normalizedOwnerType,
OwnerId = ownerId,
Items = groupedItems.GetValueOrDefault(ownerId, []).Select(InventoryItemResponse.FromModel).ToList()
}).ToList();
return Ok(response);
}
[HttpGet("item-definitions")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> ListItemDefinitions()
{
var definitions = await _inventory.ListItemDefinitionsAsync();
return Ok(definitions.Select(ItemDefinitionResponse.FromModel).ToList());
}
[HttpGet("item-definitions/{itemKey}")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> GetItemDefinition(string itemKey)
{
var definition = await _inventory.GetItemDefinitionAsync(itemKey);
if (definition is null)
return NotFound();
return Ok(ItemDefinitionResponse.FromModel(definition));
}
[HttpPost("item-definitions")]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> CreateItemDefinition([FromBody] CreateItemDefinitionRequest req)
{
if (string.IsNullOrWhiteSpace(req.ItemKey))
return BadRequest("itemKey required");
if (string.IsNullOrWhiteSpace(req.DisplayName))
return BadRequest("displayName required");
if (req.MaxStackSize <= 0)
return BadRequest("maxStackSize must be greater than 0");
if (!req.Stackable && req.MaxStackSize != 1)
return BadRequest("Non-stackable items must have maxStackSize of 1");
var created = await _inventory.CreateItemDefinitionAsync(req);
if (created is null)
return Conflict("Item definition already exists");
return Ok(ItemDefinitionResponse.FromModel(created));
}
[HttpPut("item-definitions/{itemKey}")]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> UpdateItemDefinition(string itemKey, [FromBody] UpdateItemDefinitionRequest req)
{
if (string.IsNullOrWhiteSpace(req.DisplayName))
return BadRequest("displayName required");
if (req.MaxStackSize <= 0)
return BadRequest("maxStackSize must be greater than 0");
if (!req.Stackable && req.MaxStackSize != 1)
return BadRequest("Non-stackable items must have maxStackSize of 1");
var updated = await _inventory.UpdateItemDefinitionAsync(itemKey, req);
if (updated is null)
return NotFound();
return Ok(ItemDefinitionResponse.FromModel(updated));
}
[HttpGet("by-owner/{ownerType}/{ownerId}")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> GetByOwner(string ownerType, string ownerId)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var access = await _inventory.ResolveOwnerAsync(ownerType, ownerId, userId, User.IsInRole("SUPER"));
if (!access.IsSupported)
return BadRequest("Unsupported ownerType");
if (!access.Exists)
return NotFound();
if (!access.IsAuthorized)
return Forbid();
var items = await _inventory.GetByOwnerAsync(access.OwnerType, access.OwnerId);
return Ok(new InventoryOwnerResponse
{
OwnerType = access.OwnerType,
OwnerId = access.OwnerId,
Items = items.Select(InventoryItemResponse.FromModel).ToList()
});
}
[HttpPost("by-owner/{ownerType}/{ownerId}/grant")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> Grant(string ownerType, string ownerId, [FromBody] GrantInventoryItemRequest req)
{
if (string.IsNullOrWhiteSpace(req.ItemKey))
return BadRequest("itemKey required");
if (req.Quantity <= 0)
return BadRequest("quantity must be greater than 0");
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var access = await _inventory.ResolveOwnerAsync(ownerType, ownerId, userId, User.IsInRole("SUPER"));
if (!access.IsSupported)
return BadRequest("Unsupported ownerType");
if (!access.Exists)
return NotFound();
if (!access.IsAuthorized)
return Forbid();
var definition = await _inventory.GetItemDefinitionAsync(req.ItemKey);
if (definition is null)
return BadRequest("Unknown itemKey");
var grant = await _inventory.GrantAsync(access, req, definition);
return Ok(new InventoryOwnerResponse
{
OwnerType = access.OwnerType,
OwnerId = access.OwnerId,
RequestedQuantity = grant.RequestedQuantity,
GrantedQuantity = grant.GrantedQuantity,
OverflowQuantity = grant.OverflowQuantity,
Items = grant.Items.Select(InventoryItemResponse.FromModel).ToList()
});
}
[HttpPost("by-owner/{ownerType}/{ownerId}/move")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> Move(string ownerType, string ownerId, [FromBody] MoveInventoryItemRequest req)
{
if (string.IsNullOrWhiteSpace(req.ItemId))
return BadRequest("itemId required");
if (req.ToSlot < 0)
return BadRequest("toSlot must be >= 0");
if (req.Quantity is <= 0)
return BadRequest("quantity must be greater than 0");
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var access = await _inventory.ResolveOwnerAsync(ownerType, ownerId, userId, User.IsInRole("SUPER"));
if (!access.IsSupported)
return BadRequest("Unsupported ownerType");
if (!access.Exists)
return NotFound();
if (!access.IsAuthorized)
return Forbid();
var result = await _inventory.MoveAsync(access, req);
return result.Status switch
{
InventoryMutationStatus.ItemNotFound => NotFound(),
InventoryMutationStatus.Invalid => BadRequest("Invalid move"),
InventoryMutationStatus.Conflict => Conflict("Target slot is not available"),
_ => Ok(new InventoryOwnerResponse
{
OwnerType = access.OwnerType,
OwnerId = access.OwnerId,
Items = result.Items.Select(InventoryItemResponse.FromModel).ToList()
})
};
}
[HttpPost("transfer")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> Transfer([FromBody] TransferInventoryItemRequest req)
{
if (string.IsNullOrWhiteSpace(req.ItemId))
return BadRequest("itemId required");
if (string.IsNullOrWhiteSpace(req.FromOwnerType) || string.IsNullOrWhiteSpace(req.FromOwnerId))
return BadRequest("from owner required");
if (string.IsNullOrWhiteSpace(req.ToOwnerType) || string.IsNullOrWhiteSpace(req.ToOwnerId))
return BadRequest("to owner required");
if (req.ToSlot is < 0)
return BadRequest("toSlot must be >= 0");
if (req.Quantity is <= 0)
return BadRequest("quantity must be greater than 0");
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var fromAccess = await _inventory.ResolveOwnerAsync(req.FromOwnerType, req.FromOwnerId, userId, User.IsInRole("SUPER"));
if (!fromAccess.IsSupported)
return BadRequest("Unsupported fromOwnerType");
if (!fromAccess.Exists)
return NotFound("Source owner not found");
if (!fromAccess.IsAuthorized)
return Forbid();
var toAccess = await _inventory.ResolveOwnerAsync(req.ToOwnerType, req.ToOwnerId, userId, User.IsInRole("SUPER"));
if (!toAccess.IsSupported)
return BadRequest("Unsupported toOwnerType");
if (!toAccess.Exists)
return NotFound("Target owner not found");
if (!toAccess.IsAuthorized)
return Forbid();
var result = await _inventory.TransferAsync(fromAccess, toAccess, req);
return result.Status switch
{
InventoryMutationStatus.ItemNotFound => NotFound(),
InventoryMutationStatus.Invalid => BadRequest("Invalid transfer"),
InventoryMutationStatus.Conflict => Conflict("Target slot is not available"),
_ => Ok(new TransferInventoryResponse
{
MovedItemId = req.ItemId,
FromOwnerType = fromAccess.OwnerType,
FromOwnerId = fromAccess.OwnerId,
ToOwnerType = toAccess.OwnerType,
ToOwnerId = toAccess.OwnerId,
FromItems = result.Items.Select(InventoryItemResponse.FromModel).Where(x => x.OwnerType == fromAccess.OwnerType && x.OwnerId == fromAccess.OwnerId).ToList(),
ToItems = result.Items.Select(InventoryItemResponse.FromModel).Where(x => x.OwnerType == toAccess.OwnerType && x.OwnerId == toAccess.OwnerId).ToList()
})
};
}
[HttpPost("items/{itemId}/consume")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> Consume(string itemId, [FromBody] ConsumeInventoryItemRequest req)
{
if (req.Quantity <= 0)
return BadRequest("quantity must be greater than 0");
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var result = await _inventory.ConsumeAsync(itemId, req.Quantity, userId, User.IsInRole("SUPER"));
return result.Status switch
{
InventoryMutationStatus.ItemNotFound => NotFound(),
InventoryMutationStatus.Invalid => BadRequest("Invalid consume request"),
InventoryMutationStatus.Conflict => Conflict(),
_ => Ok(new InventoryOwnerResponse
{
OwnerType = result.OwnerType,
OwnerId = result.OwnerId,
Items = result.Items.Select(InventoryItemResponse.FromModel).ToList()
})
};
}
[HttpPost("items/{itemId}/equip")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> Equip(string itemId, [FromBody] EquipInventoryItemRequest req)
{
if (string.IsNullOrWhiteSpace(req.OwnerId))
return BadRequest("ownerId required");
if (string.IsNullOrWhiteSpace(req.EquipmentSlot))
return BadRequest("equipmentSlot required");
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var result = await _inventory.EquipAsync(itemId, req.OwnerId, req.EquipmentSlot, userId, User.IsInRole("SUPER"));
return result.Status switch
{
InventoryMutationStatus.ItemNotFound => NotFound(),
InventoryMutationStatus.Invalid => BadRequest("Invalid equip request"),
InventoryMutationStatus.Conflict => Conflict("Equipment slot is not available"),
_ => Ok(new InventoryOwnerResponse
{
OwnerType = result.OwnerType,
OwnerId = result.OwnerId,
Items = result.Items.Select(InventoryItemResponse.FromModel).ToList()
})
};
}
[HttpPost("items/{itemId}/unequip")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> Unequip(string itemId, [FromBody] UnequipInventoryItemRequest req)
{
if (string.IsNullOrWhiteSpace(req.OwnerId))
return BadRequest("ownerId required");
if (req.PreferredSlot is < 0)
return BadRequest("preferredSlot must be >= 0");
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
var result = await _inventory.UnequipAsync(itemId, req.OwnerId, req.PreferredSlot, userId, User.IsInRole("SUPER"));
return result.Status switch
{
InventoryMutationStatus.ItemNotFound => NotFound(),
InventoryMutationStatus.Invalid => BadRequest("Invalid unequip request"),
InventoryMutationStatus.Conflict => Conflict("Inventory slot is not available"),
_ => Ok(new InventoryOwnerResponse
{
OwnerType = result.OwnerType,
OwnerId = result.OwnerId,
Items = result.Items.Select(InventoryItemResponse.FromModel).ToList()
})
};
}
}