Tracking character movement
This commit is contained in:
@@ -8,14 +8,16 @@ namespace CharacterApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class CharactersController : ControllerBase
|
||||
{
|
||||
private readonly CharacterStore _characters;
|
||||
|
||||
public CharactersController(CharacterStore characters)
|
||||
{
|
||||
_characters = characters;
|
||||
}
|
||||
public class CharactersController : ControllerBase
|
||||
{
|
||||
private readonly CharacterStore _characters;
|
||||
private readonly LocationsClient _locations;
|
||||
|
||||
public CharactersController(CharacterStore characters, LocationsClient locations)
|
||||
{
|
||||
_characters = characters;
|
||||
_locations = locations;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
@@ -54,17 +56,52 @@ public class CharactersController : ControllerBase
|
||||
|
||||
[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();
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
return Ok("Deleted");
|
||||
}
|
||||
|
||||
[HttpPut("{id}/move")]
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
public async Task<IActionResult> Move(string id, [FromBody] MoveCharacterRequest req, CancellationToken ct)
|
||||
{
|
||||
if (req?.Coord is null)
|
||||
return BadRequest("Coord required");
|
||||
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
return Unauthorized();
|
||||
|
||||
var allowAnyOwner = User.IsInRole("SUPER");
|
||||
var existing = await _characters.GetForOwnerByIdAsync(id, userId, allowAnyOwner);
|
||||
if (existing is null)
|
||||
return NotFound();
|
||||
|
||||
var presence = await _locations.UpdatePresenceAsync(id, req.Coord, ct);
|
||||
if (!presence.Ok)
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(presence.Body)
|
||||
? "Location presence update failed"
|
||||
: presence.Body;
|
||||
return StatusCode((int)presence.Status, message);
|
||||
}
|
||||
|
||||
var updated = await _characters.UpdateCoordAsync(id, userId, allowAnyOwner, req.Coord);
|
||||
if (!updated)
|
||||
{
|
||||
await _locations.UpdatePresenceAsync(id, existing.Coord, ct);
|
||||
return StatusCode(500, "Failed to update character coord");
|
||||
}
|
||||
|
||||
return Ok("Moved");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,21 @@ This service expects JSON request bodies for character creation and stores
|
||||
character documents in MongoDB.
|
||||
|
||||
Inbound JSON documents
|
||||
- CreateCharacterRequest (`POST /api/characters`)
|
||||
```json
|
||||
{
|
||||
"name": "string"
|
||||
}
|
||||
```
|
||||
- CreateCharacterRequest (`POST /api/characters`)
|
||||
```json
|
||||
{
|
||||
"name": "string"
|
||||
}
|
||||
```
|
||||
- MoveCharacterRequest (`PUT /api/characters/{id}/move`)
|
||||
```json
|
||||
{
|
||||
"coord": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Stored documents (MongoDB)
|
||||
- Character
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace CharacterApi.Models;
|
||||
|
||||
public class MoveCharacterRequest
|
||||
{
|
||||
public Coord? Coord { get; set; }
|
||||
}
|
||||
@@ -5,10 +5,15 @@ using Microsoft.OpenApi.Models;
|
||||
using System.Text;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// DI
|
||||
builder.Services.AddSingleton<CharacterStore>();
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// DI
|
||||
builder.Services.AddSingleton<CharacterStore>();
|
||||
builder.Services.AddHttpClient<LocationsClient>(client =>
|
||||
{
|
||||
var baseUrl = builder.Configuration["LocationsApi:BaseUrl"] ?? "http://localhost:5002";
|
||||
client.BaseAddress = new Uri(baseUrl);
|
||||
});
|
||||
|
||||
// Swagger + JWT auth in Swagger
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
@@ -7,3 +7,4 @@ See `DOCUMENTS.md` for request payloads and stored document shapes.
|
||||
- `POST /api/characters` Create a character.
|
||||
- `GET /api/characters` List characters for the current user.
|
||||
- `DELETE /api/characters/{id}` Delete a character owned by the current user.
|
||||
- `PUT /api/characters/{id}/move` Move a character to a new coord.
|
||||
|
||||
@@ -21,14 +21,44 @@ public class CharacterStore
|
||||
|
||||
public Task CreateAsync(Character character) => _col.InsertOneAsync(character);
|
||||
|
||||
public Task<List<Character>> GetForOwnerAsync(string ownerUserId) =>
|
||||
_col.Find(c => c.OwnerUserId == ownerUserId).ToListAsync();
|
||||
|
||||
public async Task<bool> DeleteForOwnerAsync(string id, string ownerUserId, bool allowAnyOwner)
|
||||
{
|
||||
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
||||
if (!allowAnyOwner)
|
||||
{
|
||||
public Task<List<Character>> GetForOwnerAsync(string ownerUserId) =>
|
||||
_col.Find(c => c.OwnerUserId == ownerUserId).ToListAsync();
|
||||
|
||||
public async Task<Character?> GetForOwnerByIdAsync(string id, string ownerUserId, bool allowAnyOwner)
|
||||
{
|
||||
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
||||
if (!allowAnyOwner)
|
||||
{
|
||||
filter = Builders<Character>.Filter.And(
|
||||
filter,
|
||||
Builders<Character>.Filter.Eq(c => c.OwnerUserId, ownerUserId)
|
||||
);
|
||||
}
|
||||
|
||||
return await _col.Find(filter).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateCoordAsync(string id, string ownerUserId, bool allowAnyOwner, Coord coord)
|
||||
{
|
||||
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
||||
if (!allowAnyOwner)
|
||||
{
|
||||
filter = Builders<Character>.Filter.And(
|
||||
filter,
|
||||
Builders<Character>.Filter.Eq(c => c.OwnerUserId, ownerUserId)
|
||||
);
|
||||
}
|
||||
|
||||
var update = Builders<Character>.Update.Set(c => c.Coord, coord);
|
||||
var result = await _col.UpdateOneAsync(filter, update);
|
||||
return result.ModifiedCount > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteForOwnerAsync(string id, string ownerUserId, bool allowAnyOwner)
|
||||
{
|
||||
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
||||
if (!allowAnyOwner)
|
||||
{
|
||||
filter = Builders<Character>.Filter.And(
|
||||
filter,
|
||||
Builders<Character>.Filter.Eq(c => c.OwnerUserId, ownerUserId)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using CharacterApi.Models;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace CharacterApi.Services;
|
||||
|
||||
public class LocationsClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly string _internalKey;
|
||||
|
||||
public LocationsClient(HttpClient http, IConfiguration cfg)
|
||||
{
|
||||
_http = http;
|
||||
_internalKey = cfg["LocationsApi:InternalKey"] ?? string.Empty;
|
||||
}
|
||||
|
||||
public async Task<(bool Ok, HttpStatusCode Status, string? Body)> UpdatePresenceAsync(
|
||||
string characterId,
|
||||
Coord coord,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "api/locations/presence")
|
||||
{
|
||||
Content = JsonContent.Create(new { characterId, coord })
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_internalKey))
|
||||
request.Headers.TryAddWithoutValidation("X-Internal-Key", _internalKey);
|
||||
|
||||
using var response = await _http.SendAsync(request, ct);
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
return (response.IsSuccessStatusCode, response.StatusCode, body);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5001" } } },
|
||||
"MongoDB": { "ConnectionString": "mongodb://192.168.86.50:27017", "DatabaseName": "promiscuity" },
|
||||
"Jwt": { "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!", "Issuer": "promiscuity", "Audience": "promiscuity-auth-api" },
|
||||
"Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5001" } } },
|
||||
"MongoDB": { "ConnectionString": "mongodb://192.168.86.50:27017", "DatabaseName": "promiscuity" },
|
||||
"LocationsApi": { "BaseUrl": "http://localhost:5002", "InternalKey": "dev-internal-key" },
|
||||
"Jwt": { "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!", "Issuer": "promiscuity", "Audience": "promiscuity-auth-api" },
|
||||
"Logging": { "LogLevel": { "Default": "Information" } }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5001" } } },
|
||||
"MongoDB": { "ConnectionString": "mongodb://192.168.86.50:27017", "DatabaseName": "promiscuity" },
|
||||
"Jwt": { "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!", "Issuer": "promiscuity", "Audience": "promiscuity-auth-api" },
|
||||
"Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5001" } } },
|
||||
"MongoDB": { "ConnectionString": "mongodb://192.168.86.50:27017", "DatabaseName": "promiscuity" },
|
||||
"LocationsApi": { "BaseUrl": "http://localhost:5002", "InternalKey": "dev-internal-key" },
|
||||
"Jwt": { "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!", "Issuer": "promiscuity", "Audience": "promiscuity-auth-api" },
|
||||
"Logging": { "LogLevel": { "Default": "Information" } },
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user