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();
}
}