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