< Summary

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

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
GetImage()0%7280%

File(s)

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

#LineLine coverage
 1using Chronicis.Api.Data;
 2using Chronicis.Api.Infrastructure;
 3using Chronicis.Api.Services;
 4using Microsoft.AspNetCore.Authorization;
 5using Microsoft.AspNetCore.Mvc;
 6using Microsoft.EntityFrameworkCore;
 7
 8namespace Chronicis.Api.Controllers;
 9
 10/// <summary>
 11/// Proxy endpoint for serving inline article images.
 12/// Resolves document IDs to fresh SAS download URLs via 302 redirect.
 13/// This avoids storing expiring SAS URLs in article HTML content.
 14/// </summary>
 15[Route("api/images")]
 16public class ImagesController : ControllerBase
 17{
 18    private readonly ChronicisDbContext _db;
 19    private readonly IBlobStorageService _blobStorage;
 20    private readonly ICurrentUserService _currentUserService;
 21    private readonly ILogger<ImagesController> _logger;
 22
 023    public ImagesController(
 024        ChronicisDbContext db,
 025        IBlobStorageService blobStorage,
 026        ICurrentUserService currentUserService,
 027        ILogger<ImagesController> logger)
 28    {
 029        _db = db;
 030        _blobStorage = blobStorage;
 031        _currentUserService = currentUserService;
 032        _logger = logger;
 033    }
 34
 35    /// <summary>
 36    /// GET /api/images/{documentId} - Redirect to a fresh SAS download URL for the image.
 37    /// Authenticated users who are members of (or own) the world can access images.
 38    /// </summary>
 39    [HttpGet("{documentId:guid}")]
 40    [Authorize]
 41    public async Task<IActionResult> GetImage(Guid documentId)
 42    {
 043        var user = await _currentUserService.GetRequiredUserAsync();
 44
 045        var document = await _db.WorldDocuments
 046            .AsNoTracking()
 047            .Include(d => d.World)
 048            .FirstOrDefaultAsync(d => d.Id == documentId);
 49
 050        if (document == null)
 51        {
 052            return NotFound();
 53        }
 54
 55        // Check access: user must be the world owner or a member
 056        var hasAccess = document.World.OwnerId == user.Id
 057            || await _db.WorldMembers.AnyAsync(m => m.WorldId == document.WorldId && m.UserId == user.Id);
 58
 059        if (!hasAccess)
 60        {
 061            return Forbid();
 62        }
 63
 64        // Verify it's an image content type
 065        if (!document.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
 66        {
 067            _logger.LogWarning("Non-image document {DocumentId} requested via image proxy", documentId);
 068            return BadRequest(new { error = "Document is not an image" });
 69        }
 70
 071        var downloadUrl = await _blobStorage.GenerateDownloadSasUrlAsync(document.BlobPath);
 72
 073        return Redirect(downloadUrl);
 074    }
 75}