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. [ApiController] [Authorize] [Route("api/[controller]")] public class AlbumController( //TODO: Add logging //ILogger logger, LactoseAuthService authService, IAlbumRepository albumRepository, IAssetRepository assetRepository ) : 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()); //Storage for assets that will be sent out; List assets = new(); switch (accessLevel) { case EAccessLevel.User: // Only publicly shared assets or owned by user assets.AddRange(album.Assets.Where(asset => (asset.IsPubliclyShared || asset.OwnerId == uid) && asset.DeletedAt == null)); // Add shared assets assets.AddRange(album.Assets.Where(asset => asset.SharedWith!.Any(user => user.Id == uid) && asset.DeletedAt == null)); 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()); } /// /// Searches albums with pagination and optional search term. /// /// Pagination and search parameters. /// A list of album previews accessible to the user. [HttpGet] public ActionResult> Search([FromQuery] PagedSearchParametersDto pagingOptions) { var uid = authService.GetUserData(User)?.Id; var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User; if(pagingOptions.Page < 0 || pagingOptions.PageSize < 0) return BadRequest(); var albums = albumRepository.SearchQuery(pagingOptions.Search ?? string.Empty, pagingOptions.Page, pagingOptions.PageSize).ToList(); switch (accessLevel) { default: case EAccessLevel.User: List filteredAlbums; // Only publicly shared albums or owned by user filteredAlbums = albums.Where(album => album.Assets != null && (album.UserOwnerId == uid || album.Assets.Any(asset => asset.IsPubliclyShared && asset.DeletedAt == null))).ToList(); // Add shared albums filteredAlbums.AddRange(albums.Where(album => album.Assets != null && album.Assets.Any(x => x.DeletedAt == null && x.SharedWith!.Any(user => user.Id == uid)))); return Ok(filteredAlbums.Select(x => x.ToAlbumPreviewDto()).ToList()); case EAccessLevel.Curator: case EAccessLevel.Admin: // All albums return Ok(albums.Select(x => x.ToAlbumPreviewDto()).ToList()); } } /// /// Gets all albums that have no person assigned. /// /// A list of unassigned album previews. [HttpGet("unassigned")] public ActionResult> GetUnassigned() { var albums = albumRepository.FindUnassigned().ToList(); return Ok(albums.Select(x => x.ToAlbumPreviewDto()).ToList()); } /// /// 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 = "Admin, Curator")] 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(); switch (accessLevel) { case EAccessLevel.User: // Users can't update albums return Forbid(); case EAccessLevel.Curator: // Curators can only update albums they own if (album.UserOwnerId != uid) return Forbid(); break; case EAccessLevel.Admin: // Admins can update any album break; } // Update album properties album.Title = albumDto.Name ?? album.Title; album.UpdatedAt = DateTime.UtcNow; album.PersonOwnerId = albumDto.Person ?? album.PersonOwnerId; album.UserOwnerId = albumDto.Owner ?? album.UserOwnerId; 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 = "Admin, Curator")] 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.Curator: // Curators can only update albums they own albums = albums.Where(x => x.UserOwnerId == uid).ToList(); 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.UserOwnerId = bulkDto.Data.Owner ?? album.UserOwnerId; 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 = "Admin, Curator")] public ActionResult Create([FromBody] AlbumCreateDto albumDto) { var uid = authService.GetUserData(User)?.Id; var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User; // If not admin, the owner of a new album is the current user albumDto.Owner = accessLevel == EAccessLevel.Admin ? albumDto.Owner : uid; 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, UserOwnerId = albumDto.Owner, CoverAssetId = albumDto.CoverAssetId ?? assets.OrderBy(a => a.OriginalFilename).FirstOrDefault()?.Id, Assets = assets }; albumRepository.Insert(album); albumRepository.Save(); return Ok(); } /// /// Removes multiple albums (soft delete). /// /// The list of album IDs to remove. /// 200 on success, 403 if forbidden. [HttpDelete] [Authorize(Roles = "Admin, Curator")] 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(); // If the user is not an admin, filter out albums they do not own if (accessLevel != EAccessLevel.Admin) { albums = albums.Where(x => x.UserOwnerId == uid).ToList(); } // If no albums are left after filtering, return a 403 if (albumIds.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 = "Admin, Curator")] 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 the user is not the owner or an admin, return a 403 if (accessLevel != EAccessLevel.Admin && album.UserOwnerId != uid) return Forbid(); albumRepository.Remove(album); albumRepository.Save(); return Ok(); } }