using Butter.Dtos; using Butter.Dtos.Album; using Butter.Types; using Lactose.Mapper; using Lactose.Models; using Lactose.Repositories; using Lactose.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Lactose.Controllers; /// /// Manages albums — collections of assets. /// /// Authentication service. /// Album repository. /// Asset repository. /// Person repository. [ApiController] [Authorize] [Route("api/[controller]")] public class AlbumController( //TODO: Add logging //ILogger logger, LactoseAuthService authService, IAlbumRepository albumRepository, IAssetRepository assetRepository, IPersonRepository personRepository ) : ControllerBase { /// /// Gets an album by its ID with filtered assets based on access level. /// /// The album ID. /// The full album details with accessible assets, or 404 if not found. [HttpGet("{id}")] public ActionResult Get([FromRoute] Guid id) { Guid? uid = authService.GetUserData(User)?.Id; EAccessLevel accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User; var album = albumRepository.Find(id); // If the album does not exist, return a 404 if (album == null) return NotFound(); // If the album has no assets, return an empty list if (album.Assets == null) return Ok(album.ToAlbumFullDto(accessLevel)); //Storage for assets that will be sent out; List assets = new(); switch (accessLevel) { case EAccessLevel.User: // Public assets, protected assets for authenticated users, or user's own private assets assets.AddRange(album.Assets.Where(asset => asset.DeletedAt == null && ( asset.Visibility == EVisibility.Public || (asset.Visibility == EVisibility.Protected && uid.HasValue) || (asset.Visibility == EVisibility.Private && asset.UploadedBy == uid) ))); break; case EAccessLevel.Maintainer: { bool isMaintainer = uid.HasValue && album.PersonOwnerId.HasValue && personRepository.IsMaintainerOf(uid.Value, album.PersonOwnerId.Value); if (isMaintainer) { assets.AddRange(album.Assets.Where(asset => asset.DeletedAt == null)); } else { assets.AddRange(album.Assets.Where(asset => asset.DeletedAt == null && ( asset.Visibility == EVisibility.Public || (asset.Visibility == EVisibility.Protected && uid.HasValue) || (asset.Visibility == EVisibility.Private && asset.UploadedBy == uid) ))); } break; } case EAccessLevel.Curator: // all assets minus deleted assets.AddRange(album.Assets.Where(asset => asset.DeletedAt == null)); break; case EAccessLevel.Admin: // All assets assets.AddRange(album.Assets); break; } // changes the retrieved album to the new filtered album.Assets = assets.OrderBy(a => a.OriginalFilename).ToList(); return Ok(album.ToAlbumFullDto(accessLevel)); } /// /// Searches albums with pagination, optional search term, sort options, and optional unassigned filter. /// /// Pagination, search, sort, and filter parameters. /// A list of album previews accessible to the user. [HttpGet] public ActionResult> Search([FromQuery] AlbumSearchParametersDto pagingOptions) { var uid = authService.GetUserData(User)?.Id; var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User; if(pagingOptions.Page < 0 || pagingOptions.PageSize < 1 || pagingOptions.PageSize > PagedParametersDto.MaxPageSize) return BadRequest(); var albums = albumRepository.SearchQuery( pagingOptions.Search ?? string.Empty, pagingOptions.Page, pagingOptions.PageSize, pagingOptions.SortBy, pagingOptions.SortAsc, pagingOptions.Unassigned, uid ?? default, accessLevel, pagingOptions.UploadedBy ).ToList(); return Ok(albums); } /// /// Updates an existing album. /// /// The album ID. /// The album update data. /// 200 on success, 403 if forbidden, 404 if not found. [HttpPost("{id}")] [Authorize(Roles = "Maintainer,Curator,Admin")] public ActionResult Update([FromRoute] Guid id, [FromBody] AlbumUpdateDto albumDto) { Guid? uid = authService.GetUserData(User)?.Id; var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User; var album = albumRepository.Find(id); // If the album does not exist, return a 404 if (album == null) return NotFound(); // Maintainer scope check — must maintain the album's person if (accessLevel == EAccessLevel.Maintainer) { if (album.PersonOwnerId == null || !personRepository.IsMaintainerOf(uid!.Value, album.PersonOwnerId.Value)) return Forbid(); } else if (accessLevel < EAccessLevel.Curator) { return Forbid(); } // Update album properties album.Title = string.IsNullOrEmpty(albumDto.Name) ? album.Title : albumDto.Name; album.UpdatedAt = DateTime.UtcNow; album.PersonOwnerId = albumDto.RemovePerson ? null : albumDto.Person ?? album.PersonOwnerId; album.Visibility = albumDto.Visibility ?? album.Visibility; album.Assets = albumDto.Assets != null ? assetRepository.FindBulk(albumDto.Assets).ToList() : album.Assets; album.CoverAssetId = albumDto.CoverAssetId ?? album.CoverAssetId; // Save changes albumRepository.Update(album); albumRepository.Save(); return Ok(); } /// /// Updates multiple albums in a bulk operation. /// /// The bulk update data with album IDs and shared values. /// 200 on success, 403 if forbidden. [HttpPost] [Authorize(Roles = "Maintainer,Curator,Admin")] public ActionResult BulkUpdate([FromBody] BulkDto bulkDto) { var uid = authService.GetUserData(User)?.Id; var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User; var albums = albumRepository.FindBulk(bulkDto.Ids).ToList(); switch (accessLevel) { case EAccessLevel.User: // Users can't update albums return Forbid(); case EAccessLevel.Maintainer: // Maintainers can only update albums of persons they maintain albums = albums.Where(a => a.PersonOwnerId != null && personRepository.IsMaintainerOf(uid!.Value, a.PersonOwnerId.Value)).ToList(); if (albums.Count == 0) return Forbid(); break; case EAccessLevel.Curator: // Curators can update any album break; case EAccessLevel.Admin: // Admins can update any album break; } foreach (var album in albums) { album.Title = bulkDto.Data.Name ?? album.Title; album.UpdatedAt = DateTime.UtcNow; album.PersonOwnerId = bulkDto.Data.Person ?? album.PersonOwnerId; album.Visibility = bulkDto.Data.Visibility ?? album.Visibility; album.Assets = bulkDto.Data.Assets != null ? assetRepository.FindBulk(bulkDto.Data.Assets).ToList() : album.Assets; album.CoverAssetId = bulkDto.Data.CoverAssetId ?? album.CoverAssetId; } albumRepository.UpdateBulk(albums); albumRepository.Save(); return Ok(); } /// /// Creates a new album. /// /// The album creation data. /// 200 on success. [HttpPut] [Authorize(Roles = "Curator,Admin")] public ActionResult Create([FromBody] AlbumCreateDto albumDto) { var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User; var assets = albumDto.Assets != null ? assetRepository.FindBulk(albumDto.Assets).ToList() : new List(); var album = new Album { Id = Guid.NewGuid(), Title = albumDto.Name, CreatedAt = DateTime.UtcNow, PersonOwnerId = albumDto.Person ?? null, Visibility = albumDto.Visibility, CoverAssetId = albumDto.CoverAssetId ?? assets.OrderBy(a => a.OriginalFilename).FirstOrDefault()?.Id, Assets = assets }; albumRepository.Insert(album); albumRepository.Save(); return Ok(album.Id); } /// /// Removes multiple albums (soft delete). /// /// The list of album IDs to remove. /// 200 on success, 403 if forbidden. [HttpDelete] [Authorize(Roles = "Maintainer,Curator,Admin")] public ActionResult BulkDelete([FromBody] List albumIds) { var uid = authService.GetUserData(User)?.Id; var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User; var albums = albumRepository.FindBulk(albumIds).ToList(); // Filter by access level if (accessLevel == EAccessLevel.Maintainer) { albums = albums.Where(a => a.PersonOwnerId != null && personRepository.IsMaintainerOf(uid!.Value, a.PersonOwnerId.Value)).ToList(); } else if (accessLevel < EAccessLevel.Curator) { return Forbid(); } if (albums.Count == 0) return Forbid(); albums.ForEach(albumRepository.Remove); albumRepository.Save(); return Ok(); } /// /// Removes an album by its ID (soft delete). /// /// The album ID. /// 200 on success, 404 if not found, 403 if forbidden. [HttpDelete("{id}")] [Authorize(Roles = "Maintainer,Curator,Admin")] public ActionResult Delete(Guid id) { var uid = authService.GetUserData(User)?.Id; var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User; var album = albumRepository.Find(id); // If the album does not exist, return a 404 if (album == null) return NotFound(); if (accessLevel == EAccessLevel.Maintainer) { if (album.PersonOwnerId == null || !personRepository.IsMaintainerOf(uid!.Value, album.PersonOwnerId.Value)) return Forbid(); } else if (accessLevel < EAccessLevel.Curator) { return Forbid(); } albumRepository.Remove(album); albumRepository.Save(); return Ok(); } }