using Butter.Dtos;
using Butter.Dtos.Person;
using Butter.Types;
using Lactose.Mapper;
using Lactose.Repositories;
using Lactose.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Lactose.Controllers;
///
/// Manages people (faces/individuals identified in assets).
///
[ApiController]
[Authorize]
[Route("api/[controller]")]
public class PersonController(
IPersonRepository personRepository,
IAlbumRepository albumRepository,
LactoseAuthService authService
) : ControllerBase {
///
/// Gets a person by ID with their associated albums, filtered by access level.
///
///
/// Gets a person by ID with their associated albums, filtered by access level and paginated.
///
/// The person ID.
/// Pagination, search, and sort parameters for albums.
/// The person details with albums, or 404 if not found.
[HttpGet("{id}")]
public ActionResult Get([FromRoute] Guid id, [FromQuery] PagedSearchParametersDto albumPaging) {
var uid = authService.GetUserData(User)?.Id;
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
var person = personRepository.FindVisible(id, uid, accessLevel,
albumPaging.Search, albumPaging.SortBy, albumPaging.SortAsc,
albumPaging.Page, albumPaging.PageSize);
if (person == null) return NotFound();
return Ok(person.ToPersonDetailedDto(accessLevel));
}
///
/// Searches people with pagination, optional search term, sort options, filtering out cosplayers with no visible content for non-admin users.
///
/// Pagination, search, and sort parameters.
/// A paginated list of person previews.
[HttpGet]
public ActionResult> GetAll([FromQuery] PersonSearchParametersDto pagedSearch) {
var uid = authService.GetUserData(User)?.Id;
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
if (pagedSearch.Page < 0 || pagedSearch.PageSize < 1 || pagedSearch.PageSize > PagedParametersDto.MaxPageSize) { return BadRequest(); }
var people = personRepository.SearchQuery(
pagedSearch.Search ?? string.Empty,
pagedSearch.Page,
pagedSearch.PageSize,
pagedSearch.SortBy,
pagedSearch.SortAsc,
uid ?? default,
accessLevel
).ToList();
return Ok(people);
}
///
/// Updates a person.
///
/// The person ID.
/// The updated person data.
/// 200 on success.
[HttpPost("{id}")]
[Authorize(Roles = "Maintainer,Curator,Admin")]
public IActionResult Update([FromRoute] Guid id, [FromBody] PersonUpdateDto dto) {
var uid = authService.GetUserData(User)?.Id;
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
var person = personRepository.Find(id);
if (person == null) return NotFound();
if (accessLevel == EAccessLevel.Maintainer) {
if (uid == null || !personRepository.IsMaintainerOf(uid.Value, id))
return Forbid();
}
person.Name = dto.Name;
person.ProfileAssetId = dto.ProfileAssetId;
person.ProfileCropX = dto.ProfileCropX;
person.ProfileCropY = dto.ProfileCropY;
person.ProfileCropZoom = dto.ProfileCropZoom;
if (dto.Visibility != null && accessLevel >= EAccessLevel.Maintainer)
person.Visibility = dto.Visibility.Value;
if (dto.MaintainerUserIds != null && accessLevel >= EAccessLevel.Curator)
personRepository.SetMaintainers(person.Id, dto.MaintainerUserIds);
person.UpdatedAt = DateTime.UtcNow;
personRepository.Save();
return Ok();
}
///
/// Updates multiple people in a bulk operation.
///
/// The bulk update data.
/// 200 on success.
[HttpPost]
[Authorize(Roles = "Admin, Curator")]
public IActionResult BulkUpdate([FromBody] BulkDto personBulkDto) {
var people = personRepository.FindBulk(personBulkDto.Ids).ToList();
foreach (var person in people) {
person.Name = personBulkDto.Data.Name;
person.ProfileAssetId = personBulkDto.Data.ProfileAssetId;
person.ProfileCropX = personBulkDto.Data.ProfileCropX;
person.ProfileCropY = personBulkDto.Data.ProfileCropY;
person.ProfileCropZoom = personBulkDto.Data.ProfileCropZoom;
person.UpdatedAt = DateTime.UtcNow;
}
personRepository.Save();
return Ok();
}
///
/// Deletes a person by ID (soft delete).
///
/// The person ID.
/// 200 on success, 404 if not found.
///
/// Deletes a person by ID (hard delete). Cascades to unlink all associated albums.
///
/// 200 on success, 404 if not found.
[HttpDelete("{id}")]
[Authorize(Roles = "Admin, Curator")]
public IActionResult Delete([FromRoute] Guid id) {
var person = personRepository.Find(id);
if (person == null) return NotFound();
// Unlink all albums owned by this person
var albums = albumRepository.FindByPerson(id).ToList();
foreach (var album in albums) {
album.PersonOwnerId = null;
}
personRepository.Remove(person);
personRepository.Save();
return Ok();
}
///
/// Deletes multiple people in a bulk operation.
///
/// The list of person IDs to delete.
/// 200 on success.
///
/// Deletes multiple people in a bulk operation (hard delete). Cascades to unlink all associated albums.
///
/// 200 on success.
[HttpDelete]
[Authorize(Roles = "Admin, Curator")]
public IActionResult BulkDelete([FromBody] List ids) {
var people = personRepository.FindBulk(ids).ToList();
foreach (var person in people) {
// Unlink all albums owned by this person
var albums = albumRepository.FindByPerson(person.Id).ToList();
foreach (var album in albums) {
album.PersonOwnerId = null;
}
personRepository.Remove(person);
}
personRepository.Save();
return Ok();
}
///
/// Creates a new person.
///
/// The person creation data.
/// 200 with the new person ID.
[HttpPut]
[Authorize(Roles = "Admin, Curator")]
public ActionResult Create([FromBody] PersonCreateDto dto) {
var person = new Models.Person {
Id = Guid.NewGuid(),
Name = dto.Name,
ProfileAssetId = dto.ProfileAssetId,
CreatedAt = DateTime.UtcNow
};
personRepository.Insert(person);
personRepository.Save();
return Ok(person.Id);
}
}