using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Meezi.API.Services; using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Data; using Meezi.Infrastructure.Services.Platform; using Meezi.Shared; namespace Meezi.API.Controllers; [Route("api/cafes/{cafeId}/media")] public class MediaController : CafeApiControllerBase { private readonly IMediaStorageService _media; public MediaController(IMediaStorageService media) => _media = media; [HttpPost("menu-image")] [RequestSizeLimit(5 * 1024 * 1024)] public Task UploadMenuImage( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return Task.FromResult(permDenied); return Upload(cafeId, file, tenant, _media.SaveMenuImageAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); } [HttpPost("menu-video")] [RequestSizeLimit(25 * 1024 * 1024)] public Task UploadMenuVideo( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return Task.FromResult(permDenied); return Upload(cafeId, file, tenant, _media.SaveMenuVideoAsync, "INVALID_FILE", "Use MP4/WebM/MOV up to 25MB.", cancellationToken); } [HttpPost("menu-model3d")] [RequestSizeLimit(8 * 1024 * 1024)] public async Task UploadMenuModel3d( string cafeId, IFormFile file, ITenantContext tenant, IPlatformCatalogService catalog, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var planTier = tenant.PlanTier ?? PlanTier.Free; if (!await catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, "menu_3d", cancellationToken)) { return StatusCode( StatusCodes.Status403Forbidden, new ApiResponse( false, null, new ApiError("PLAN_FEATURE_DISABLED", "3D menu is not included in your plan. Upgrade to enable it."))); } return await Upload( cafeId, file, tenant, _media.SaveMenuModel3dAsync, "INVALID_FILE", "Use GLB (.glb) up to 8MB.", cancellationToken); } [HttpPost("table-image")] [RequestSizeLimit(5 * 1024 * 1024)] public Task UploadTableImage( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return Task.FromResult(permDenied); return Upload(cafeId, file, tenant, _media.SaveTableImageAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); } [HttpPost("table-video")] [RequestSizeLimit(25 * 1024 * 1024)] public Task UploadTableVideo( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return Task.FromResult(permDenied); return Upload(cafeId, file, tenant, _media.SaveTableVideoAsync, "INVALID_FILE", "Use MP4/WebM/MOV up to 25MB.", cancellationToken); } [HttpPost("cafe-logo")] [RequestSizeLimit(5 * 1024 * 1024)] public Task UploadCafeLogo( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); if (EnsurePermission(tenant, Permission.ManageCafeSettings) is { } permDenied) return Task.FromResult(permDenied); return Upload(cafeId, file, tenant, _media.SaveCafeLogoAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); } [HttpPost("cafe-cover")] [RequestSizeLimit(5 * 1024 * 1024)] public Task UploadCafeCover( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); if (EnsurePermission(tenant, Permission.ManageCafeSettings) is { } permDenied) return Task.FromResult(permDenied); return Upload(cafeId, file, tenant, _media.SaveCafeCoverAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); } /// Media library for this café — previously uploaded files so the UI can /// reuse one instead of re-uploading. Deduplication means each distinct file appears once. [HttpGet] public async Task ListMedia( string cafeId, ITenantContext tenant, [FromServices] AppDbContext db, CancellationToken cancellationToken, [FromQuery] string? kind = null, [FromQuery] int limit = 60) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; var query = db.MediaAssets.AsNoTracking().Where(m => m.CafeId == cafeId); if (!string.IsNullOrWhiteSpace(kind)) query = query.Where(m => m.Kind == kind); var items = await query .OrderByDescending(m => m.CreatedAt) .Take(Math.Clamp(limit, 1, 200)) .Select(m => new MediaAssetDto( m.Id, m.Url, m.Kind, m.ContentType, m.SizeBytes, m.OriginalFileName, m.CreatedAt)) .ToListAsync(cancellationToken); return Ok(new ApiResponse>(true, items)); } private async Task Upload( string cafeId, IFormFile file, ITenantContext tenant, Func> save, string errorCode, string errorMessage, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (file is null || file.Length == 0) return BadRequest(new ApiResponse(false, null, new ApiError("INVALID_FILE", "No file uploaded."))); var url = await save(cafeId, file, cancellationToken); if (url is null) return BadRequest(new ApiResponse(false, null, new ApiError(errorCode, errorMessage))); return Ok(new ApiResponse(true, new UploadResultDto(url))); } } public record UploadResultDto(string Url); public record MediaAssetDto( string Id, string Url, string Kind, string ContentType, long SizeBytes, string? OriginalFileName, DateTime CreatedAt);