< Summary

Information
Class: Chronicis.Api.Controllers.MapsController
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Controllers/MapsController.cs
Line coverage
100%
Covered lines: 8
Uncovered lines: 0
Coverable lines: 8
Total lines: 860
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Controllers/MapsController.cs

#LineLine coverage
 1using Chronicis.Api.Infrastructure;
 2using Chronicis.Api.Services;
 3using Chronicis.Shared.DTOs.Maps;
 4using Microsoft.AspNetCore.Authorization;
 5using Microsoft.AspNetCore.Mvc;
 6
 7namespace Chronicis.Api.Controllers;
 8
 9/// <summary>
 10/// API endpoints for WorldMap management (create, list, get, basemap upload).
 11/// </summary>
 12[Route("world/{worldId:guid}/maps")]
 13[Authorize]
 14public class MapsController : ControllerBase
 15{
 16    private readonly IWorldMapService _worldMapService;
 17    private readonly ICurrentUserService _currentUserService;
 18    private readonly ILogger<MapsController> _logger;
 19
 10220    public MapsController(
 10221        IWorldMapService worldMapService,
 10222        ICurrentUserService currentUserService,
 10223        ILogger<MapsController> logger)
 24    {
 10225        _worldMapService = worldMapService;
 10226        _currentUserService = currentUserService;
 10227        _logger = logger;
 10228    }
 29
 30    /// <summary>
 31    /// POST /world/{worldId}/maps — Create a new map with default hidden layers.
 32    /// </summary>
 33    [HttpPost]
 34    public async Task<ActionResult<MapDto>> CreateMap(Guid worldId, [FromBody] MapCreateDto dto)
 35    {
 36        var user = await _currentUserService.GetRequiredUserAsync();
 37
 38        if (dto == null || string.IsNullOrWhiteSpace(dto.Name))
 39        {
 40            return BadRequest(new { error = "Name is required" });
 41        }
 42
 43        _logger.LogTraceSanitized("User {UserId} creating map in world {WorldId}", user.Id, worldId);
 44
 45        try
 46        {
 47            var result = await _worldMapService.CreateMapAsync(worldId, user.Id, dto);
 48            return Ok(result);
 49        }
 50        catch (UnauthorizedAccessException ex)
 51        {
 52            _logger.LogWarningSanitized(ex, "Unauthorized map creation");
 53            return StatusCode(403, new { error = ex.Message });
 54        }
 55    }
 56
 57    /// <summary>
 58    /// GET /world/{worldId}/maps/{mapId} — Get full map metadata.
 59    /// </summary>
 60    [HttpGet("{mapId:guid}")]
 61    public async Task<ActionResult<MapDto>> GetMap(Guid worldId, Guid mapId)
 62    {
 63        var user = await _currentUserService.GetRequiredUserAsync();
 64        _logger.LogTraceSanitized("User {UserId} getting map {MapId}", user.Id, mapId);
 65
 66        var map = await _worldMapService.GetMapAsync(mapId, user.Id);
 67
 68        if (map == null)
 69        {
 70            return NotFound(new { error = "Map not found or access denied" });
 71        }
 72
 73        return Ok(map);
 74    }
 75
 76    /// <summary>
 77    /// GET /world/{worldId}/maps/{mapId}/layers — List map layers ordered by sort order.
 78    /// </summary>
 79    [HttpGet("{mapId:guid}/layers")]
 80    public async Task<ActionResult<IEnumerable<MapLayerDto>>> ListLayers(Guid worldId, Guid mapId)
 81    {
 82        var user = await _currentUserService.GetRequiredUserAsync();
 83        _logger.LogTraceSanitized("User {UserId} listing layers for map {MapId}", user.Id, mapId);
 84
 85        try
 86        {
 87            var layers = await _worldMapService.ListLayersForMapAsync(worldId, mapId, user.Id);
 88            return Ok(layers);
 89        }
 90        catch (UnauthorizedAccessException ex)
 91        {
 92            _logger.LogWarningSanitized(ex, "Unauthorized layer list");
 93            return StatusCode(403, new { error = ex.Message });
 94        }
 95        catch (InvalidOperationException ex)
 96        {
 97            _logger.LogWarningSanitized(ex, "Layer list target not found");
 98            return NotFound(new { error = ex.Message });
 99        }
 100    }
 101
 102    /// <summary>
 103    /// POST /world/{worldId}/maps/{mapId}/layers — Create a custom map layer.
 104    /// </summary>
 105    [HttpPost("{mapId:guid}/layers")]
 106    public async Task<ActionResult<MapLayerDto>> CreateLayer(
 107        Guid worldId,
 108        Guid mapId,
 109        [FromBody] CreateLayerRequest request)
 110    {
 111        var user = await _currentUserService.GetRequiredUserAsync();
 112
 113        if (request == null || string.IsNullOrWhiteSpace(request.Name))
 114        {
 115            return BadRequest(new { error = "Layer name is required" });
 116        }
 117
 118        _logger.LogTraceSanitized("User {UserId} creating layer on map {MapId}", user.Id, mapId);
 119
 120        try
 121        {
 122            var created = await _worldMapService.CreateLayer(worldId, mapId, user.Id, request.Name, request.ParentLayerI
 123            return Ok(created);
 124        }
 125        catch (ArgumentException ex)
 126        {
 127            _logger.LogWarningSanitized(ex, "Invalid create layer request");
 128            return BadRequest(new { error = ex.Message });
 129        }
 130        catch (UnauthorizedAccessException ex)
 131        {
 132            _logger.LogWarningSanitized(ex, "Unauthorized create layer request");
 133            return Forbid();
 134        }
 135    }
 136
 137    /// <summary>
 138    /// PUT /world/{worldId}/maps/{mapId}/layers/{layerId}/rename — Rename a custom map layer.
 139    /// </summary>
 140    [HttpPut("{mapId:guid}/layers/{layerId:guid}/rename")]
 141    public async Task<IActionResult> RenameLayer(
 142        Guid worldId,
 143        Guid mapId,
 144        Guid layerId,
 145        [FromBody] RenameLayerRequest request)
 146    {
 147        var user = await _currentUserService.GetRequiredUserAsync();
 148
 149        if (request == null || string.IsNullOrWhiteSpace(request.Name))
 150        {
 151            return BadRequest(new { error = "Layer name is required" });
 152        }
 153
 154        _logger.LogTraceSanitized("User {UserId} renaming layer {LayerId} on map {MapId}", user.Id, layerId, mapId);
 155
 156        try
 157        {
 158            await _worldMapService.RenameLayer(worldId, mapId, user.Id, layerId, request.Name);
 159            return NoContent();
 160        }
 161        catch (ArgumentException ex)
 162        {
 163            _logger.LogWarningSanitized(ex, "Invalid rename layer request");
 164            return BadRequest(new { error = ex.Message });
 165        }
 166        catch (UnauthorizedAccessException ex)
 167        {
 168            _logger.LogWarningSanitized(ex, "Unauthorized rename layer request");
 169            return Forbid();
 170        }
 171    }
 172
 173    /// <summary>
 174    /// PUT /world/{worldId}/maps/{mapId}/layers/{layerId}/parent — Assign or clear layer parent.
 175    /// </summary>
 176    [HttpPut("{mapId:guid}/layers/{layerId:guid}/parent")]
 177    public async Task<IActionResult> SetLayerParent(
 178        Guid worldId,
 179        Guid mapId,
 180        Guid layerId,
 181        [FromBody] SetLayerParentRequest request)
 182    {
 183        var user = await _currentUserService.GetRequiredUserAsync();
 184
 185        if (request == null)
 186        {
 187            return BadRequest(new { error = "Invalid request body" });
 188        }
 189
 190        _logger.LogTraceSanitized("User {UserId} setting parent for layer {LayerId} on map {MapId}", user.Id, layerId, m
 191
 192        try
 193        {
 194            await _worldMapService.SetLayerParentAsync(worldId, mapId, user.Id, layerId, request.ParentLayerId);
 195            return NoContent();
 196        }
 197        catch (ArgumentException ex)
 198        {
 199            _logger.LogWarningSanitized(ex, "Invalid set layer parent request");
 200            return BadRequest(new { error = ex.Message });
 201        }
 202        catch (UnauthorizedAccessException ex)
 203        {
 204            _logger.LogWarningSanitized(ex, "Unauthorized set layer parent request");
 205            return Forbid();
 206        }
 207    }
 208
 209    /// <summary>
 210    /// DELETE /world/{worldId}/maps/{mapId}/layers/{layerId} — Delete a custom map layer.
 211    /// </summary>
 212    [HttpDelete("{mapId:guid}/layers/{layerId:guid}")]
 213    public async Task<IActionResult> DeleteLayer(Guid worldId, Guid mapId, Guid layerId)
 214    {
 215        var user = await _currentUserService.GetRequiredUserAsync();
 216        _logger.LogTraceSanitized("User {UserId} deleting layer {LayerId} on map {MapId}", user.Id, layerId, mapId);
 217
 218        try
 219        {
 220            await _worldMapService.DeleteLayer(worldId, mapId, user.Id, layerId);
 221            return NoContent();
 222        }
 223        catch (ArgumentException ex)
 224        {
 225            _logger.LogWarningSanitized(ex, "Invalid delete layer request");
 226            return BadRequest(new { error = ex.Message });
 227        }
 228        catch (UnauthorizedAccessException ex)
 229        {
 230            _logger.LogWarningSanitized(ex, "Unauthorized delete layer request");
 231            return Forbid();
 232        }
 233    }
 234
 235    /// <summary>
 236    /// PUT /world/{worldId}/maps/{mapId} — Update map metadata.
 237    /// </summary>
 238    [HttpPut("{mapId:guid}")]
 239    public async Task<ActionResult<MapDto>> UpdateMap(Guid worldId, Guid mapId, [FromBody] MapUpdateDto dto)
 240    {
 241        var user = await _currentUserService.GetRequiredUserAsync();
 242
 243        if (dto == null || string.IsNullOrWhiteSpace(dto.Name))
 244        {
 245            return BadRequest(new { error = "Name is required" });
 246        }
 247
 248        _logger.LogTraceSanitized("User {UserId} updating map {MapId}", user.Id, mapId);
 249
 250        try
 251        {
 252            var updated = await _worldMapService.UpdateMapAsync(worldId, mapId, user.Id, dto);
 253            return Ok(updated);
 254        }
 255        catch (UnauthorizedAccessException ex)
 256        {
 257            _logger.LogWarningSanitized(ex, "Unauthorized map update");
 258            return StatusCode(403, new { error = ex.Message });
 259        }
 260        catch (InvalidOperationException ex)
 261        {
 262            _logger.LogWarningSanitized(ex, "Map update target not found");
 263            return NotFound(new { error = ex.Message });
 264        }
 265    }
 266
 267    /// <summary>
 268    /// PUT /world/{worldId}/maps/{mapId}/layers/{layerId} — Update layer visibility.
 269    /// </summary>
 270    [HttpPut("{mapId:guid}/layers/{layerId:guid}")]
 271    public async Task<IActionResult> UpdateLayerVisibility(
 272        Guid worldId,
 273        Guid mapId,
 274        Guid layerId,
 275        [FromBody] UpdateLayerVisibilityRequest request)
 276    {
 277        var user = await _currentUserService.GetRequiredUserAsync();
 278
 279        if (request == null)
 280        {
 281            return BadRequest(new { error = "Invalid request body" });
 282        }
 283
 284        _logger.LogTraceSanitized("User {UserId} updating visibility for layer {LayerId} on map {MapId}", user.Id, layer
 285
 286        try
 287        {
 288            await _worldMapService.UpdateLayerVisibility(worldId, mapId, layerId, user.Id, request.IsEnabled);
 289            return NoContent();
 290        }
 291        catch (ArgumentException ex)
 292        {
 293            _logger.LogWarningSanitized(ex, "Invalid layer visibility update request");
 294            return BadRequest(new { error = ex.Message });
 295        }
 296        catch (UnauthorizedAccessException ex)
 297        {
 298            _logger.LogWarningSanitized(ex, "Unauthorized layer visibility update");
 299            return Forbid();
 300        }
 301    }
 302
 303    /// <summary>
 304    /// PUT /world/{worldId}/maps/{mapId}/layers/reorder — Reorder layers by full ordered layer ID list.
 305    /// </summary>
 306    [HttpPut("{mapId:guid}/layers/reorder")]
 307    public async Task<IActionResult> ReorderLayers(
 308        Guid worldId,
 309        Guid mapId,
 310        [FromBody] ReorderLayersRequest request)
 311    {
 312        var user = await _currentUserService.GetRequiredUserAsync();
 313
 314        if (request == null)
 315        {
 316            return BadRequest(new { error = "Invalid request body" });
 317        }
 318
 319        _logger.LogTraceSanitized("User {UserId} reordering layers on map {MapId}", user.Id, mapId);
 320
 321        try
 322        {
 323            await _worldMapService.ReorderLayers(worldId, mapId, user.Id, request.LayerIds);
 324            return NoContent();
 325        }
 326        catch (ArgumentException ex)
 327        {
 328            _logger.LogWarningSanitized(ex, "Invalid layer reorder request");
 329            return BadRequest(new { error = ex.Message });
 330        }
 331        catch (UnauthorizedAccessException ex)
 332        {
 333            _logger.LogWarningSanitized(ex, "Unauthorized layer reorder request");
 334            return Forbid();
 335        }
 336    }
 337
 338    /// <summary>
 339    /// GET /world/{worldId}/maps — List all maps for a world, sorted by name.
 340    /// </summary>
 341    [HttpGet]
 342    public async Task<ActionResult<IEnumerable<MapSummaryDto>>> ListMapsForWorld(Guid worldId)
 343    {
 344        var user = await _currentUserService.GetRequiredUserAsync();
 345        _logger.LogTraceSanitized("User {UserId} listing maps for world {WorldId}", user.Id, worldId);
 346
 347        try
 348        {
 349            var maps = await _worldMapService.ListMapsForWorldAsync(worldId, user.Id);
 350            return Ok(maps);
 351        }
 352        catch (UnauthorizedAccessException ex)
 353        {
 354            _logger.LogWarningSanitized(ex, "Unauthorized map list");
 355            return StatusCode(403, new { error = ex.Message });
 356        }
 357    }
 358
 359    /// <summary>
 360    /// GET /world/{worldId}/maps/autocomplete?query=... — List minimal map suggestions for editor autocomplete.
 361    /// </summary>
 362    [HttpGet("autocomplete")]
 363    public async Task<ActionResult<IEnumerable<MapAutocompleteDto>>> AutocompleteMaps(Guid worldId, [FromQuery] string? 
 364    {
 365        var user = await _currentUserService.GetRequiredUserAsync();
 366        _logger.LogTraceSanitized("User {UserId} requesting map autocomplete for world {WorldId}", user.Id, worldId);
 367
 368        try
 369        {
 370            var maps = await _worldMapService.SearchMapsForWorldAsync(worldId, user.Id, query);
 371            return Ok(maps);
 372        }
 373        catch (UnauthorizedAccessException ex)
 374        {
 375            _logger.LogWarningSanitized(ex, "Unauthorized map autocomplete request");
 376            return StatusCode(403, new { error = ex.Message });
 377        }
 378    }
 379
 380    /// <summary>
 381    /// POST /world/{worldId}/maps/{mapId}/pins — Create a pin on a map.
 382    /// </summary>
 383    [HttpPost("{mapId:guid}/pins")]
 384    public async Task<ActionResult<MapPinResponseDto>> CreatePin(
 385        Guid worldId, Guid mapId, [FromBody] MapPinCreateDto dto)
 386    {
 387        var user = await _currentUserService.GetRequiredUserAsync();
 388
 389        if (dto == null)
 390        {
 391            return BadRequest(new { error = "Invalid request body" });
 392        }
 393
 394        _logger.LogTraceSanitized("User {UserId} creating pin on map {MapId}", user.Id, mapId);
 395
 396        try
 397        {
 398            var pin = await _worldMapService.CreatePinAsync(worldId, mapId, user.Id, dto);
 399            return Ok(pin);
 400        }
 401        catch (UnauthorizedAccessException ex)
 402        {
 403            _logger.LogWarningSanitized(ex, "Unauthorized pin create");
 404            return StatusCode(403, new { error = ex.Message });
 405        }
 406        catch (ArgumentException ex)
 407        {
 408            _logger.LogWarningSanitized(ex, "Invalid pin create request");
 409            return BadRequest(new { error = ex.Message });
 410        }
 411        catch (InvalidOperationException ex)
 412        {
 413            _logger.LogWarningSanitized(ex, "Pin create target not found");
 414            return NotFound(new { error = ex.Message });
 415        }
 416    }
 417
 418    /// <summary>
 419    /// GET /world/{worldId}/maps/{mapId}/pins — List pins for a map.
 420    /// </summary>
 421    [HttpGet("{mapId:guid}/pins")]
 422    public async Task<ActionResult<IEnumerable<MapPinResponseDto>>> ListPins(Guid worldId, Guid mapId)
 423    {
 424        var user = await _currentUserService.GetRequiredUserAsync();
 425        _logger.LogTraceSanitized("User {UserId} listing pins for map {MapId}", user.Id, mapId);
 426
 427        try
 428        {
 429            var pins = await _worldMapService.ListPinsForMapAsync(worldId, mapId, user.Id);
 430            return Ok(pins);
 431        }
 432        catch (UnauthorizedAccessException ex)
 433        {
 434            _logger.LogWarningSanitized(ex, "Unauthorized pin list");
 435            return StatusCode(403, new { error = ex.Message });
 436        }
 437        catch (InvalidOperationException ex)
 438        {
 439            _logger.LogWarningSanitized(ex, "Pin list target not found");
 440            return NotFound(new { error = ex.Message });
 441        }
 442    }
 443
 444    /// <summary>
 445    /// PATCH /world/{worldId}/maps/{mapId}/pins/{pinId} — Update pin position.
 446    /// </summary>
 447    [HttpPatch("{mapId:guid}/pins/{pinId:guid}")]
 448    public async Task<ActionResult<MapPinResponseDto>> UpdatePinPosition(
 449        Guid worldId, Guid mapId, Guid pinId, [FromBody] MapPinPositionUpdateDto dto)
 450    {
 451        var user = await _currentUserService.GetRequiredUserAsync();
 452
 453        if (dto == null)
 454        {
 455            return BadRequest(new { error = "Invalid request body" });
 456        }
 457
 458        _logger.LogTraceSanitized("User {UserId} updating pin {PinId} on map {MapId}", user.Id, pinId, mapId);
 459
 460        try
 461        {
 462            var pin = await _worldMapService.UpdatePinPositionAsync(worldId, mapId, pinId, user.Id, dto);
 463            return Ok(pin);
 464        }
 465        catch (UnauthorizedAccessException ex)
 466        {
 467            _logger.LogWarningSanitized(ex, "Unauthorized pin update");
 468            return StatusCode(403, new { error = ex.Message });
 469        }
 470        catch (ArgumentException ex)
 471        {
 472            _logger.LogWarningSanitized(ex, "Invalid pin update request");
 473            return BadRequest(new { error = ex.Message });
 474        }
 475        catch (InvalidOperationException ex)
 476        {
 477            _logger.LogWarningSanitized(ex, "Pin update target not found");
 478            return NotFound(new { error = ex.Message });
 479        }
 480    }
 481
 482    /// <summary>
 483    /// DELETE /world/{worldId}/maps/{mapId}/pins/{pinId} — Delete a pin from a map.
 484    /// </summary>
 485    [HttpDelete("{mapId:guid}/pins/{pinId:guid}")]
 486    public async Task<IActionResult> DeletePin(Guid worldId, Guid mapId, Guid pinId)
 487    {
 488        var user = await _currentUserService.GetRequiredUserAsync();
 489        _logger.LogTraceSanitized("User {UserId} deleting pin {PinId} on map {MapId}", user.Id, pinId, mapId);
 490
 491        try
 492        {
 493            await _worldMapService.DeletePinAsync(worldId, mapId, pinId, user.Id);
 494            return NoContent();
 495        }
 496        catch (UnauthorizedAccessException ex)
 497        {
 498            _logger.LogWarningSanitized(ex, "Unauthorized pin delete");
 499            return StatusCode(403, new { error = ex.Message });
 500        }
 501        catch (InvalidOperationException ex)
 502        {
 503            _logger.LogWarningSanitized(ex, "Pin delete target not found");
 504            return NotFound(new { error = ex.Message });
 505        }
 506    }
 507
 508    /// <summary>
 509    /// GET /world/{worldId}/maps/features/autocomplete?query=... — List minimal map feature suggestions for editor auto
 510    /// </summary>
 511    [HttpGet("features/autocomplete")]
 512    public async Task<ActionResult<IEnumerable<MapFeatureAutocompleteDto>>> AutocompleteFeatures(Guid worldId, [FromQuer
 513    {
 514        var user = await _currentUserService.GetRequiredUserAsync();
 515        _logger.LogTraceSanitized("User {UserId} requesting feature autocomplete for world {WorldId}", user.Id, worldId)
 516
 517        try
 518        {
 519            var features = await _worldMapService.SearchMapFeaturesForWorldAsync(worldId, user.Id, query);
 520            return Ok(features);
 521        }
 522        catch (UnauthorizedAccessException ex)
 523        {
 524            _logger.LogWarningSanitized(ex, "Unauthorized feature autocomplete request");
 525            return StatusCode(403, new { error = ex.Message });
 526        }
 527    }
 528
 529    /// <summary>
 530    /// GET /world/{worldId}/maps/{mapId}/features/autocomplete?query=... — List minimal map feature suggestions for a s
 531    /// </summary>
 532    [HttpGet("{mapId:guid}/features/autocomplete")]
 533    public async Task<ActionResult<IEnumerable<MapFeatureAutocompleteDto>>> AutocompleteFeaturesForMap(
 534        Guid worldId,
 535        Guid mapId,
 536        [FromQuery] string? query = null)
 537    {
 538        var user = await _currentUserService.GetRequiredUserAsync();
 539        _logger.LogTraceSanitized(
 540            "User {UserId} requesting feature autocomplete for map {MapId} in world {WorldId}",
 541            user.Id,
 542            mapId,
 543            worldId);
 544
 545        try
 546        {
 547            var features = await _worldMapService.SearchMapFeaturesForMapAsync(worldId, mapId, user.Id, query);
 548            return Ok(features);
 549        }
 550        catch (UnauthorizedAccessException ex)
 551        {
 552            _logger.LogWarningSanitized(ex, "Unauthorized map-scoped feature autocomplete request");
 553            return StatusCode(403, new { error = ex.Message });
 554        }
 555        catch (InvalidOperationException ex)
 556        {
 557            _logger.LogWarningSanitized(ex, "Map-scoped feature autocomplete target not found");
 558            return NotFound(new { error = ex.Message });
 559        }
 560    }
 561
 562    /// <summary>
 563    /// POST /world/{worldId}/maps/{mapId}/features — Create an additive map feature.
 564    /// </summary>
 565    [HttpPost("{mapId:guid}/features")]
 566    public async Task<ActionResult<MapFeatureDto>> CreateFeature(
 567        Guid worldId,
 568        Guid mapId,
 569        [FromBody] MapFeatureCreateDto dto)
 570    {
 571        var user = await _currentUserService.GetRequiredUserAsync();
 572
 573        if (dto == null)
 574        {
 575            return BadRequest(new { error = "Invalid request body" });
 576        }
 577
 578        try
 579        {
 580            var feature = await _worldMapService.CreateFeatureAsync(worldId, mapId, user.Id, dto);
 581            return Ok(feature);
 582        }
 583        catch (UnauthorizedAccessException ex)
 584        {
 585            _logger.LogWarningSanitized(ex, "Unauthorized feature create");
 586            return StatusCode(403, new { error = ex.Message });
 587        }
 588        catch (ArgumentException ex)
 589        {
 590            _logger.LogWarningSanitized(ex, "Invalid feature create request");
 591            return BadRequest(new { error = ex.Message });
 592        }
 593        catch (InvalidOperationException ex)
 594        {
 595            _logger.LogWarningSanitized(ex, "Feature create target not found");
 596            return NotFound(new { error = ex.Message });
 597        }
 598    }
 599
 600    /// <summary>
 601    /// GET /world/{worldId}/maps/{mapId}/features — List additive map features.
 602    /// </summary>
 603    [HttpGet("{mapId:guid}/features")]
 604    public async Task<ActionResult<IEnumerable<MapFeatureDto>>> ListFeatures(Guid worldId, Guid mapId)
 605    {
 606        var user = await _currentUserService.GetRequiredUserAsync();
 607
 608        try
 609        {
 610            var features = await _worldMapService.ListFeaturesForMapAsync(worldId, mapId, user.Id);
 611            return Ok(features);
 612        }
 613        catch (UnauthorizedAccessException ex)
 614        {
 615            _logger.LogWarningSanitized(ex, "Unauthorized feature list");
 616            return StatusCode(403, new { error = ex.Message });
 617        }
 618        catch (InvalidOperationException ex)
 619        {
 620            _logger.LogWarningSanitized(ex, "Feature list target not found");
 621            return NotFound(new { error = ex.Message });
 622        }
 623    }
 624
 625    /// <summary>
 626    /// GET /world/{worldId}/maps/{mapId}/features/{featureId} — Get a single map feature.
 627    /// </summary>
 628    [HttpGet("{mapId:guid}/features/{featureId:guid}")]
 629    public async Task<ActionResult<MapFeatureDto>> GetFeature(Guid worldId, Guid mapId, Guid featureId)
 630    {
 631        var user = await _currentUserService.GetRequiredUserAsync();
 632
 633        try
 634        {
 635            var feature = await _worldMapService.GetFeatureAsync(worldId, mapId, featureId, user.Id);
 636            return feature == null
 637                ? NotFound(new { error = "Feature not found" })
 638                : Ok(feature);
 639        }
 640        catch (UnauthorizedAccessException ex)
 641        {
 642            _logger.LogWarningSanitized(ex, "Unauthorized feature get");
 643            return StatusCode(403, new { error = ex.Message });
 644        }
 645        catch (InvalidOperationException ex)
 646        {
 647            _logger.LogWarningSanitized(ex, "Feature get target not found");
 648            return NotFound(new { error = ex.Message });
 649        }
 650    }
 651
 652    /// <summary>
 653    /// GET /world/{worldId}/maps/{mapId}/features/{featureId}/session-references — List session-note references for a m
 654    /// </summary>
 655    [HttpGet("{mapId:guid}/features/{featureId:guid}/session-references")]
 656    public async Task<ActionResult<IEnumerable<MapFeatureSessionReferenceDto>>> ListFeatureSessionReferences(
 657        Guid worldId,
 658        Guid mapId,
 659        Guid featureId)
 660    {
 661        var user = await _currentUserService.GetRequiredUserAsync();
 662
 663        try
 664        {
 665            var references = await _worldMapService.ListSessionReferencesForFeatureAsync(worldId, mapId, featureId, user
 666            return Ok(references);
 667        }
 668        catch (UnauthorizedAccessException ex)
 669        {
 670            _logger.LogWarningSanitized(ex, "Unauthorized feature session reference list");
 671            return StatusCode(403, new { error = ex.Message });
 672        }
 673        catch (InvalidOperationException ex)
 674        {
 675            _logger.LogWarningSanitized(ex, "Feature session reference list target not found");
 676            return NotFound(new { error = ex.Message });
 677        }
 678    }
 679
 680    /// <summary>
 681    /// PUT /world/{worldId}/maps/{mapId}/features/{featureId} — Replace a map feature.
 682    /// </summary>
 683    [HttpPut("{mapId:guid}/features/{featureId:guid}")]
 684    public async Task<ActionResult<MapFeatureDto>> UpdateFeature(
 685        Guid worldId,
 686        Guid mapId,
 687        Guid featureId,
 688        [FromBody] MapFeatureUpdateDto dto)
 689    {
 690        var user = await _currentUserService.GetRequiredUserAsync();
 691
 692        if (dto == null)
 693        {
 694            return BadRequest(new { error = "Invalid request body" });
 695        }
 696
 697        try
 698        {
 699            var feature = await _worldMapService.UpdateFeatureAsync(worldId, mapId, featureId, user.Id, dto);
 700            return Ok(feature);
 701        }
 702        catch (UnauthorizedAccessException ex)
 703        {
 704            _logger.LogWarningSanitized(ex, "Unauthorized feature update");
 705            return StatusCode(403, new { error = ex.Message });
 706        }
 707        catch (ArgumentException ex)
 708        {
 709            _logger.LogWarningSanitized(ex, "Invalid feature update request");
 710            return BadRequest(new { error = ex.Message });
 711        }
 712        catch (InvalidOperationException ex)
 713        {
 714            _logger.LogWarningSanitized(ex, "Feature update target not found");
 715            return NotFound(new { error = ex.Message });
 716        }
 717    }
 718
 719    /// <summary>
 720    /// DELETE /world/{worldId}/maps/{mapId}/features/{featureId} — Delete a map feature.
 721    /// </summary>
 722    [HttpDelete("{mapId:guid}/features/{featureId:guid}")]
 723    public async Task<IActionResult> DeleteFeature(Guid worldId, Guid mapId, Guid featureId)
 724    {
 725        var user = await _currentUserService.GetRequiredUserAsync();
 726
 727        try
 728        {
 729            await _worldMapService.DeleteFeatureAsync(worldId, mapId, featureId, user.Id);
 730            return NoContent();
 731        }
 732        catch (UnauthorizedAccessException ex)
 733        {
 734            _logger.LogWarningSanitized(ex, "Unauthorized feature delete");
 735            return StatusCode(403, new { error = ex.Message });
 736        }
 737        catch (InvalidOperationException ex)
 738        {
 739            _logger.LogWarningSanitized(ex, "Feature delete target not found");
 740            return NotFound(new { error = ex.Message });
 741        }
 742    }
 743
 744    /// <summary>
 745    /// DELETE /world/{worldId}/maps/{mapId} — Permanently delete map metadata and all map blobs.
 746    /// </summary>
 747    [HttpDelete("{mapId:guid}")]
 748    public async Task<IActionResult> DeleteMap(Guid worldId, Guid mapId)
 749    {
 750        var user = await _currentUserService.GetRequiredUserAsync();
 751        _logger.LogTraceSanitized("User {UserId} deleting map {MapId}", user.Id, mapId);
 752
 753        try
 754        {
 755            await _worldMapService.DeleteMapAsync(worldId, mapId, user.Id);
 756            return NoContent();
 757        }
 758        catch (UnauthorizedAccessException ex)
 759        {
 760            _logger.LogWarningSanitized(ex, "Unauthorized map delete");
 761            return StatusCode(403, new { error = ex.Message });
 762        }
 763        catch (InvalidOperationException ex)
 764        {
 765            _logger.LogWarningSanitized(ex, "Map delete target not found");
 766            return NotFound(new { error = ex.Message });
 767        }
 768    }
 769
 770    /// <summary>
 771    /// POST /world/{worldId}/maps/{mapId}/request-basemap-upload — Generate a SAS URL for uploading a basemap.
 772    /// </summary>
 773    [HttpPost("{mapId:guid}/request-basemap-upload")]
 774    public async Task<ActionResult<RequestBasemapUploadResponseDto>> RequestBasemapUpload(
 775        Guid worldId, Guid mapId, [FromBody] RequestBasemapUploadDto dto)
 776    {
 777        var user = await _currentUserService.GetRequiredUserAsync();
 778
 779        if (dto == null)
 780        {
 781            return BadRequest(new { error = "Invalid request body" });
 782        }
 783
 784        _logger.LogTraceSanitized("User {UserId} requesting basemap upload for map {MapId}", user.Id, mapId);
 785
 786        try
 787        {
 788            var result = await _worldMapService.RequestBasemapUploadAsync(worldId, mapId, user.Id, dto);
 789            return Ok(result);
 790        }
 791        catch (UnauthorizedAccessException ex)
 792        {
 793            _logger.LogWarningSanitized(ex, "Unauthorized basemap upload request");
 794            return StatusCode(403, new { error = ex.Message });
 795        }
 796        catch (ArgumentException ex)
 797        {
 798            _logger.LogWarningSanitized(ex, "Invalid basemap upload request");
 799            return BadRequest(new { error = ex.Message });
 800        }
 801        catch (InvalidOperationException ex)
 802        {
 803            _logger.LogWarningSanitized(ex, "Map not found for basemap upload");
 804            return NotFound(new { error = ex.Message });
 805        }
 806    }
 807
 808    /// <summary>
 809    /// POST /world/{worldId}/maps/{mapId}/confirm-basemap-upload — Confirm the basemap upload completed.
 810    /// Filename and content-type were already persisted during the SAS request; no body required.
 811    /// </summary>
 812    [HttpPost("{mapId:guid}/confirm-basemap-upload")]
 813    public async Task<ActionResult<MapDto>> ConfirmBasemapUpload(Guid worldId, Guid mapId)
 814    {
 815        var user = await _currentUserService.GetRequiredUserAsync();
 816        _logger.LogTraceSanitized("User {UserId} confirming basemap upload for map {MapId}", user.Id, mapId);
 817
 818        try
 819        {
 820            var result = await _worldMapService.ConfirmBasemapUploadAsync(worldId, mapId, user.Id);
 821            return Ok(result);
 822        }
 823        catch (UnauthorizedAccessException ex)
 824        {
 825            _logger.LogWarningSanitized(ex, "Unauthorized basemap confirm");
 826            return StatusCode(403, new { error = ex.Message });
 827        }
 828        catch (InvalidOperationException ex)
 829        {
 830            _logger.LogWarningSanitized(ex, "Map not found for basemap confirm");
 831            return NotFound(new { error = ex.Message });
 832        }
 833    }
 834
 835    /// <summary>
 836    /// GET /world/{worldId}/maps/{mapId}/basemap — Get short-lived SAS read URL for basemap.
 837    /// </summary>
 838    [HttpGet("{mapId:guid}/basemap")]
 839    public async Task<ActionResult<GetBasemapReadUrlResponseDto>> GetBasemapReadUrl(Guid worldId, Guid mapId)
 840    {
 841        var user = await _currentUserService.GetRequiredUserAsync();
 842        _logger.LogTraceSanitized("User {UserId} requesting basemap read URL for map {MapId}", user.Id, mapId);
 843
 844        try
 845        {
 846            var result = await _worldMapService.GetBasemapReadUrlAsync(worldId, mapId, user.Id);
 847            return Ok(result);
 848        }
 849        catch (UnauthorizedAccessException ex)
 850        {
 851            _logger.LogWarningSanitized(ex, "Unauthorized basemap read request");
 852            return StatusCode(403, new { error = ex.Message });
 853        }
 854        catch (InvalidOperationException ex)
 855        {
 856            _logger.LogWarningSanitized(ex, "Basemap read request not found");
 857            return NotFound(new { error = ex.Message });
 858        }
 859    }
 860}