< Summary

Information
Class: Chronicis.Api.Controllers.PublicController
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Controllers/PublicController.cs
Line coverage
100%
Covered lines: 6
Uncovered lines: 0
Coverable lines: 6
Total lines: 227
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/PublicController.cs

#LineLine coverage
 1using Chronicis.Api.Services;
 2using Chronicis.Shared.DTOs;
 3using Chronicis.Shared.DTOs.Maps;
 4using Microsoft.AspNetCore.Mvc;
 5
 6namespace Chronicis.Api.Controllers;
 7
 8/// <summary>
 9/// API endpoints for anonymous public access to worlds and articles.
 10/// These endpoints do NOT require authentication.
 11/// </summary>
 12[ApiController]
 13[Route("public")]
 14public class PublicController : ControllerBase
 15{
 16    private readonly IPublicWorldService _publicWorldService;
 17    private readonly ILogger<PublicController> _logger;
 18
 819    public PublicController(
 820        IPublicWorldService publicWorldService,
 821        ILogger<PublicController> logger)
 22    {
 823        _publicWorldService = publicWorldService;
 824        _logger = logger;
 825    }
 26
 27    /// <summary>
 28    /// GET /api/public/worlds/{publicSlug} - Get a public world by its public slug.
 29    /// </summary>
 30    [HttpGet("worlds/{publicSlug}")]
 31    public async Task<ActionResult<WorldDetailDto>> GetPublicWorld(string publicSlug)
 32    {
 33        if (string.IsNullOrWhiteSpace(publicSlug))
 34        {
 35            return BadRequest(new { error = "Public slug is required" });
 36        }
 37
 38        _logger.LogTraceSanitized("Getting public world with slug '{PublicSlug}'", publicSlug);
 39
 40        var world = await _publicWorldService.GetPublicWorldAsync(publicSlug);
 41
 42        if (world == null)
 43        {
 44            return NotFound(new { error = "World not found or not public" });
 45        }
 46
 47        return Ok(world);
 48    }
 49
 50    /// <summary>
 51    /// GET /api/public/worlds/{publicSlug}/articles - Get the article tree for a public world.
 52    /// Returns a hierarchical tree structure with virtual groups (Campaigns, Player Characters, Wiki).
 53    /// Only returns articles with Public visibility.
 54    /// </summary>
 55    [HttpGet("worlds/{publicSlug}/articles")]
 56    public async Task<ActionResult<List<ArticleTreeDto>>> GetPublicArticleTree(string publicSlug)
 57    {
 58        if (string.IsNullOrWhiteSpace(publicSlug))
 59        {
 60            return BadRequest(new { error = "Public slug is required" });
 61        }
 62
 63        _logger.LogTraceSanitized("Getting public article tree for world '{PublicSlug}'", publicSlug);
 64
 65        var tree = await _publicWorldService.GetPublicArticleTreeAsync(publicSlug);
 66
 67        // If tree is empty, check if world exists
 68        if (!tree.Any())
 69        {
 70            var world = await _publicWorldService.GetPublicWorldAsync(publicSlug);
 71            if (world == null)
 72            {
 73                return NotFound(new { error = "World not found or not public" });
 74            }
 75        }
 76
 77        return Ok(tree);
 78    }
 79
 80    /// <summary>
 81    /// GET /api/public/worlds/{publicSlug}/articles/{*articlePath} - Get a specific public article by path.
 82    /// </summary>
 83    [HttpGet("worlds/{publicSlug}/articles/{*articlePath}")]
 84    public async Task<ActionResult<ArticleDto>> GetPublicArticle(string publicSlug, string articlePath)
 85    {
 86        if (string.IsNullOrWhiteSpace(publicSlug))
 87        {
 88            return BadRequest(new { error = "Public slug is required" });
 89        }
 90
 91        if (string.IsNullOrWhiteSpace(articlePath))
 92        {
 93            return BadRequest(new { error = "Article path is required" });
 94        }
 95
 96        _logger.LogTraceSanitized("Getting public article '{ArticlePath}' in world '{PublicSlug}'", articlePath, publicS
 97
 98        var article = await _publicWorldService.GetPublicArticleAsync(publicSlug, articlePath);
 99
 100        if (article == null)
 101        {
 102            return NotFound(new { error = "Article not found or not public" });
 103        }
 104
 105        return Ok(article);
 106    }
 107
 108    /// <summary>
 109    /// GET /api/public/worlds/{publicSlug}/articles/resolve/{articleId} - Resolve an article ID to its public URL path.
 110    /// </summary>
 111    [HttpGet("worlds/{publicSlug}/articles/resolve/{articleId:guid}")]
 112    public async Task<ActionResult<string>> ResolveArticlePath(string publicSlug, Guid articleId)
 113    {
 114        if (string.IsNullOrWhiteSpace(publicSlug))
 115        {
 116            return BadRequest(new { error = "Public slug is required" });
 117        }
 118
 119        _logger.LogTraceSanitized("Resolving article path for {ArticleId} in world '{PublicSlug}'", articleId, publicSlu
 120
 121        var path = await _publicWorldService.GetPublicArticlePathAsync(publicSlug, articleId);
 122
 123        if (path == null)
 124        {
 125            return NotFound(new { error = "Article not found or not public" });
 126        }
 127
 128        return Ok(path);
 129    }
 130
 131    /// <summary>
 132    /// GET /api/public/documents/{documentId} - Resolve a public inline image document to a fresh download URL.
 133    /// Only documents attached to public articles in public worlds are accessible.
 134    /// </summary>
 135    [HttpGet("documents/{documentId:guid}")]
 136    public async Task<IActionResult> GetPublicDocumentContent(Guid documentId)
 137    {
 138        var downloadUrl = await _publicWorldService.GetPublicDocumentDownloadUrlAsync(documentId);
 139
 140        if (string.IsNullOrWhiteSpace(downloadUrl))
 141        {
 142            return NotFound(new { error = "Document not found or not publicly accessible" });
 143        }
 144
 145        return Redirect(downloadUrl);
 146    }
 147
 148    /// <summary>
 149    /// GET /api/public/worlds/{publicSlug}/maps/{mapId}/basemap - Get a public basemap read URL.
 150    /// </summary>
 151    [HttpGet("worlds/{publicSlug}/maps/{mapId:guid}/basemap")]
 152    public async Task<ActionResult<GetBasemapReadUrlResponseDto>> GetPublicMapBasemap(string publicSlug, Guid mapId)
 153    {
 154        if (string.IsNullOrWhiteSpace(publicSlug))
 155        {
 156            return BadRequest(new { error = "Public slug is required" });
 157        }
 158
 159        var (basemap, error) = await _publicWorldService.GetPublicMapBasemapReadUrlAsync(publicSlug, mapId);
 160        if (basemap == null)
 161        {
 162            return NotFound(new { error = error ?? "Map not found or not public" });
 163        }
 164
 165        return Ok(basemap);
 166    }
 167
 168    /// <summary>
 169    /// GET /api/public/worlds/{publicSlug}/maps/{mapId}/layers - List public map layers.
 170    /// </summary>
 171    [HttpGet("worlds/{publicSlug}/maps/{mapId:guid}/layers")]
 172    public async Task<ActionResult<IEnumerable<MapLayerDto>>> GetPublicMapLayers(string publicSlug, Guid mapId)
 173    {
 174        if (string.IsNullOrWhiteSpace(publicSlug))
 175        {
 176            return BadRequest(new { error = "Public slug is required" });
 177        }
 178
 179        var layers = await _publicWorldService.GetPublicMapLayersAsync(publicSlug, mapId);
 180        if (layers == null)
 181        {
 182            return NotFound(new { error = "Map not found or not public" });
 183        }
 184
 185        return Ok(layers);
 186    }
 187
 188    /// <summary>
 189    /// GET /api/public/worlds/{publicSlug}/maps/{mapId}/pins - List public map pins.
 190    /// </summary>
 191    [HttpGet("worlds/{publicSlug}/maps/{mapId:guid}/pins")]
 192    public async Task<ActionResult<IEnumerable<MapPinResponseDto>>> GetPublicMapPins(string publicSlug, Guid mapId)
 193    {
 194        if (string.IsNullOrWhiteSpace(publicSlug))
 195        {
 196            return BadRequest(new { error = "Public slug is required" });
 197        }
 198
 199        var pins = await _publicWorldService.GetPublicMapPinsAsync(publicSlug, mapId);
 200        if (pins == null)
 201        {
 202            return NotFound(new { error = "Map not found or not public" });
 203        }
 204
 205        return Ok(pins);
 206    }
 207
 208    /// <summary>
 209    /// GET /api/public/worlds/{publicSlug}/maps/{mapId}/features - List public map features.
 210    /// </summary>
 211    [HttpGet("worlds/{publicSlug}/maps/{mapId:guid}/features")]
 212    public async Task<ActionResult<IEnumerable<MapFeatureDto>>> GetPublicMapFeatures(string publicSlug, Guid mapId)
 213    {
 214        if (string.IsNullOrWhiteSpace(publicSlug))
 215        {
 216            return BadRequest(new { error = "Public slug is required" });
 217        }
 218
 219        var features = await _publicWorldService.GetPublicMapFeaturesAsync(publicSlug, mapId);
 220        if (features == null)
 221        {
 222            return NotFound(new { error = "Map not found or not public" });
 223        }
 224
 225        return Ok(features);
 226    }
 227}