diff --git a/microservices/CharacterApi/Controllers/CharactersController.cs b/microservices/CharacterApi/Controllers/CharactersController.cs index bc04546..c48b879 100644 --- a/microservices/CharacterApi/Controllers/CharactersController.cs +++ b/microservices/CharacterApi/Controllers/CharactersController.cs @@ -64,9 +64,23 @@ public class CharactersController : ControllerBase return Unauthorized(); var allowAnyOwner = User.IsInRole("SUPER"); - var character = await _characters.GetForOwnerByIdAsync(id, userId, allowAnyOwner); + 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}", diff --git a/microservices/CharacterApi/Services/CharacterStore.cs b/microservices/CharacterApi/Services/CharacterStore.cs index f5cfd1a..f16e334 100644 --- a/microservices/CharacterApi/Services/CharacterStore.cs +++ b/microservices/CharacterApi/Services/CharacterStore.cs @@ -7,7 +7,8 @@ namespace CharacterApi.Services; public class CharacterStore { private readonly IMongoCollection _col; - private readonly IMongoCollection _locations; + private readonly IMongoCollection _locations; + private const string CoordIndexName = "coord_x_1_coord_y_1"; public sealed record VisibleLocationResult(List Locations, int GeneratedCount); @@ -18,7 +19,8 @@ public class CharacterStore var client = new MongoClient(cs); var db = client.GetDatabase(dbName); _col = db.GetCollection("Characters"); - _locations = db.GetCollection("Locations"); + _locations = db.GetCollection("Locations"); + EnsureLocationCoordIndexes(); var ownerIndex = Builders.IndexKeys.Ascending(c => c.OwnerUserId); _col.Indexes.CreateOne(new CreateIndexModel(ownerIndex)); @@ -29,19 +31,8 @@ public class CharacterStore public Task> GetForOwnerAsync(string ownerUserId) => _col.Find(c => c.OwnerUserId == ownerUserId).ToListAsync(); - public async Task GetForOwnerByIdAsync(string id, string ownerUserId, bool allowAnyOwner) - { - var filter = Builders.Filter.Eq(c => c.Id, id); - if (!allowAnyOwner) - { - filter = Builders.Filter.And( - filter, - Builders.Filter.Eq(c => c.OwnerUserId, ownerUserId) - ); - } - - return await _col.Find(filter).FirstOrDefaultAsync(); - } + public async Task GetByIdAsync(string id) => + await _col.Find(c => c.Id == id).FirstOrDefaultAsync(); public Task> GetVisibleLocationsAsync(Character character) { @@ -55,7 +46,7 @@ public class CharacterStore return new VisibleLocationResult(locations, generatedCount); } - private Task> GetVisibleLocationsInternalAsync(Character character, bool ensureGenerated) + private async Task> GetVisibleLocationsInternalAsync(Character character, bool ensureGenerated) { var radius = character.VisionRadius > 0 ? character.VisionRadius : 3; var minX = character.Coord.X - radius; @@ -63,14 +54,15 @@ public class CharacterStore var minY = character.Coord.Y - radius; var maxY = character.Coord.Y + radius; - var filter = Builders.Filter.And( - Builders.Filter.Gte(l => l.Coord.X, minX), - Builders.Filter.Lte(l => l.Coord.X, maxX), - Builders.Filter.Gte(l => l.Coord.Y, minY), - Builders.Filter.Lte(l => l.Coord.Y, maxY) + var filter = Builders.Filter.And( + Builders.Filter.Gte("coord.x", minX), + Builders.Filter.Lte("coord.x", maxX), + Builders.Filter.Gte("coord.y", minY), + Builders.Filter.Lte("coord.y", maxY) ); - return _locations.Find(filter).ToListAsync(); + var documents = await _locations.Find(filter).ToListAsync(); + return documents.Select(MapVisibleLocation).ToList(); } private async Task EnsureVisibleLocationsExistAsync(Character character) @@ -82,15 +74,19 @@ public class CharacterStore { for (var y = character.Coord.Y - radius; y <= character.Coord.Y + radius; y++) { - var filter = Builders.Filter.And( - Builders.Filter.Eq(l => l.Coord.X, x), - Builders.Filter.Eq(l => l.Coord.Y, y) + var filter = Builders.Filter.And( + Builders.Filter.Eq("coord.x", x), + Builders.Filter.Eq("coord.y", y) ); - var update = Builders.Update - .SetOnInsert(l => l.Name, DefaultLocationName(x, y)) - .SetOnInsert(l => l.Coord, new LocationCoord { X = x, Y = y }) - .SetOnInsert(l => l.Id, ObjectId.GenerateNewId().ToString()) + var update = Builders.Update + .SetOnInsert("_id", ObjectId.GenerateNewId()) + .SetOnInsert("name", DefaultLocationName(x, y)) + .SetOnInsert("coord", new BsonDocument + { + { "x", x }, + { "y", y } + }) .SetOnInsert("createdUtc", DateTime.UtcNow); try @@ -116,6 +112,67 @@ public class CharacterStore return $"Location {x},{y}"; } + private static VisibleLocation MapVisibleLocation(BsonDocument document) + { + var coord = document.GetValue("coord", new BsonDocument()).AsBsonDocument; + var idValue = document.GetValue("_id", BsonNull.Value); + string? id = null; + if (!idValue.IsBsonNull) + { + id = idValue.BsonType == BsonType.ObjectId + ? idValue.AsObjectId.ToString() + : idValue.ToString(); + } + + return new VisibleLocation + { + Id = id, + Name = document.GetValue("name", "").AsString, + Coord = new LocationCoord + { + X = coord.GetValue("x", 0).ToInt32(), + Y = coord.GetValue("y", 0).ToInt32() + } + }; + } + + private void EnsureLocationCoordIndexes() + { + var indexes = _locations.Indexes.List().ToList(); + foreach (var index in indexes) + { + var name = index.GetValue("name", "").AsString; + if (name == "_id_") + continue; + + var keyDoc = index.GetValue("key", new BsonDocument()).AsBsonDocument; + if (IsLegacyCoordIndex(keyDoc) || IsUnexpectedCoordIndex(keyDoc)) + _locations.Indexes.DropOne(name); + } + + var coordIndex = new BsonDocument + { + { "coord.x", 1 }, + { "coord.y", 1 } + }; + var coordIndexOptions = new CreateIndexOptions { Unique = true, Name = CoordIndexName }; + _locations.Indexes.CreateOne(new CreateIndexModel(coordIndex, coordIndexOptions)); + } + + private static bool IsLegacyCoordIndex(BsonDocument keyDoc) => + keyDoc.ElementCount == 2 && + keyDoc.TryGetValue("Coord.X", out var xValue) && + xValue.IsInt32 && xValue.AsInt32 == 1 && + keyDoc.TryGetValue("Coord.Y", out var yValue) && + yValue.IsInt32 && yValue.AsInt32 == 1; + + private static bool IsUnexpectedCoordIndex(BsonDocument keyDoc) + { + var hasLower = keyDoc.Contains("coord.x") || keyDoc.Contains("coord.y"); + var hasUpper = keyDoc.Contains("Coord.X") || keyDoc.Contains("Coord.Y"); + return hasUpper || (hasLower && !(keyDoc.Contains("coord.x") && keyDoc.Contains("coord.y"))); + } + public async Task DeleteForOwnerAsync(string id, string ownerUserId, bool allowAnyOwner) { var filter = Builders.Filter.Eq(c => c.Id, id); diff --git a/microservices/LocationsApi/Services/LocationStore.cs b/microservices/LocationsApi/Services/LocationStore.cs index 27e753f..1d589a1 100644 --- a/microservices/LocationsApi/Services/LocationStore.cs +++ b/microservices/LocationsApi/Services/LocationStore.cs @@ -4,28 +4,27 @@ using MongoDB.Driver; namespace LocationsApi.Services; -public class LocationStore -{ - private readonly IMongoCollection _col; - - public LocationStore(IConfiguration cfg) - { - var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017"; - var dbName = cfg["MongoDB:DatabaseName"] ?? "GameDb"; +public class LocationStore +{ + private readonly IMongoCollection _col; + private readonly IMongoCollection _rawCol; + private const string CoordIndexName = "coord_x_1_coord_y_1"; + + public LocationStore(IConfiguration cfg) + { + var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017"; + var dbName = cfg["MongoDB:DatabaseName"] ?? "GameDb"; var client = new MongoClient(cs); - var db = client.GetDatabase(dbName); - var collectionName = "Locations"; - EnsureLocationSchema(db, collectionName); - _col = db.GetCollection(collectionName); - - var coordIndex = Builders.IndexKeys - .Ascending(l => l.Coord.X) - .Ascending(l => l.Coord.Y); - var coordIndexOptions = new CreateIndexOptions { Unique = true }; - _col.Indexes.CreateOne(new CreateIndexModel(coordIndex, coordIndexOptions)); - - EnsureOriginLocation(); - } + var db = client.GetDatabase(dbName); + var collectionName = "Locations"; + EnsureLocationSchema(db, collectionName); + _col = db.GetCollection(collectionName); + _rawCol = db.GetCollection(collectionName); + + EnsureCoordIndexes(); + + EnsureOriginLocation(); + } private static void EnsureLocationSchema(IMongoDatabase db, string collectionName) { @@ -95,18 +94,55 @@ public class LocationStore return result.DeletedCount > 0; } - public async Task UpdateNameAsync(string id, string name) - { - var filter = Builders.Filter.Eq(l => l.Id, id); - var update = Builders.Update.Set(l => l.Name, name); - var result = await _col.UpdateOneAsync(filter, update); - return result.ModifiedCount > 0; - } - - private void EnsureOriginLocation() - { - var filter = Builders.Filter.And( - Builders.Filter.Eq(l => l.Coord.X, 0), + public async Task UpdateNameAsync(string id, string name) + { + var filter = Builders.Filter.Eq(l => l.Id, id); + var update = Builders.Update.Set(l => l.Name, name); + var result = await _col.UpdateOneAsync(filter, update); + return result.ModifiedCount > 0; + } + + private void EnsureCoordIndexes() + { + var indexes = _rawCol.Indexes.List().ToList(); + foreach (var index in indexes) + { + var name = index.GetValue("name", "").AsString; + if (name == "_id_") + continue; + + var keyDoc = index.GetValue("key", new BsonDocument()).AsBsonDocument; + if (IsLegacyCoordIndex(keyDoc) || IsUnexpectedCoordIndex(keyDoc)) + _rawCol.Indexes.DropOne(name); + } + + var coordIndex = new BsonDocument + { + { "coord.x", 1 }, + { "coord.y", 1 } + }; + var coordIndexOptions = new CreateIndexOptions { Unique = true, Name = CoordIndexName }; + _rawCol.Indexes.CreateOne(new CreateIndexModel(coordIndex, coordIndexOptions)); + } + + private static bool IsLegacyCoordIndex(BsonDocument keyDoc) => + keyDoc.ElementCount == 2 && + keyDoc.TryGetValue("Coord.X", out var xValue) && + xValue.IsInt32 && xValue.AsInt32 == 1 && + keyDoc.TryGetValue("Coord.Y", out var yValue) && + yValue.IsInt32 && yValue.AsInt32 == 1; + + private static bool IsUnexpectedCoordIndex(BsonDocument keyDoc) + { + var hasLower = keyDoc.Contains("coord.x") || keyDoc.Contains("coord.y"); + var hasUpper = keyDoc.Contains("Coord.X") || keyDoc.Contains("Coord.Y"); + return hasUpper || (hasLower && !(keyDoc.Contains("coord.x") && keyDoc.Contains("coord.y"))); + } + + private void EnsureOriginLocation() + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(l => l.Coord.X, 0), Builders.Filter.Eq(l => l.Coord.Y, 0) ); var existing = _col.Find(filter).FirstOrDefault();