Files
MilkyShots/Lactose/Controllers/PersonController.cs
T

244 lines
11 KiB
C#

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;
/// <summary>
/// Manages people (faces/individuals identified in assets).
/// </summary>
[ApiController]
[Authorize]
[Route("api/[controller]")]
public class PersonController(
IPersonRepository personRepository,
IAlbumRepository albumRepository,
IAssetRepository assetRepository,
LactoseAuthService authService
) : ControllerBase {
/// <summary>
/// Gets a person by ID with their associated albums, filtered by access level.
/// </summary>
/// <summary>
/// Gets a person by ID with their associated albums, filtered by access level and paginated.
/// </summary>
/// <param name="id">The person ID.</param>
/// <param name="albumPaging">Pagination, search, and sort parameters for albums.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The person details with albums, or 404 if not found.</returns>
[HttpGet("{id}")]
public async Task<ActionResult<PersonDetailedDto>> Get([FromRoute] Guid id, [FromQuery] PagedSearchParametersDto albumPaging, CancellationToken cancellationToken) {
var uid = authService.GetUserData(User)?.Id;
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
var person = await personRepository.FindVisibleAsync(id, uid, accessLevel,
albumPaging.Search, albumPaging.SortBy, albumPaging.SortAsc,
albumPaging.Page, albumPaging.PageSize, cancellationToken);
if (person == null) return NotFound();
return Ok(person.ToPersonDetailedDto(accessLevel, uid));
}
/// <summary>
/// Searches people with pagination, optional search term, sort options, filtering out cosplayers with no visible content for non-admin users.
/// </summary>
/// <param name="pagedSearch">Pagination, search, and sort parameters.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>A paginated list of person previews.</returns>
[HttpGet]
public async Task<ActionResult<List<PersonPreviewDto>>> GetAll([FromQuery] PersonSearchParametersDto pagedSearch, CancellationToken cancellationToken) {
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 = await personRepository.SearchQueryAsync(
pagedSearch.Search ?? string.Empty,
pagedSearch.Page,
pagedSearch.PageSize,
pagedSearch.SortBy,
pagedSearch.SortAsc,
uid ?? default,
accessLevel,
cancellationToken
);
return Ok(people);
}
/// <summary>
/// Updates a person.
/// </summary>
/// <param name="id">The person ID.</param>
/// <param name="dto">The updated person data.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>200 on success.</returns>
[HttpPost("{id}")]
[Authorize(Roles = "Maintainer,Curator,Admin")]
public async Task<IActionResult> Update([FromRoute] Guid id, [FromBody] PersonUpdateDto dto, CancellationToken cancellationToken) {
var uid = authService.GetUserData(User)?.Id;
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
var person = await personRepository.FindAsync(id, cancellationToken);
if (person == null) return NotFound();
if (accessLevel == EAccessLevel.Maintainer) {
if (uid == null || !await personRepository.IsMaintainerOfAsync(uid.Value, id, cancellationToken))
return Forbid();
}
person.Name = dto.Name ?? person.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)
await personRepository.SetMaintainersAsync(person.Id, dto.MaintainerUserIds, cancellationToken);
person.UpdatedAt = DateTime.UtcNow;
await personRepository.SaveAsync(cancellationToken);
return Ok();
}
/// <summary>
/// Updates multiple people in a bulk operation, optionally cascading a visibility change to their albums and assets.
/// </summary>
/// <param name="personBulkDto">The bulk update data.</param>
/// <returns>200 on success with an optional cascade result.</returns>
[HttpPost]
[Authorize(Roles = "Admin, Curator")]
public async Task<IActionResult> BulkUpdate([FromBody] BulkDto<PersonUpdateDto> personBulkDto) {
var uid = authService.GetUserData(User)?.Id;
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
var result = await personRepository.ExecuteInTransactionAsync(async () => {
var people = await personRepository.FindBulkAsync(personBulkDto.Ids, CancellationToken.None);
foreach (var person in people) {
person.Name = personBulkDto.Data.Name ?? person.Name;
person.ProfileAssetId = personBulkDto.Data.ProfileAssetId ?? person.ProfileAssetId;
person.ProfileCropX = personBulkDto.Data.ProfileCropX ?? person.ProfileCropX;
person.ProfileCropY = personBulkDto.Data.ProfileCropY ?? person.ProfileCropY;
person.ProfileCropZoom = personBulkDto.Data.ProfileCropZoom ?? person.ProfileCropZoom;
person.Visibility = personBulkDto.Data.Visibility ?? person.Visibility;
person.UpdatedAt = DateTime.UtcNow;
if (personBulkDto.Data.MaintainerUserIds != null && accessLevel >= EAccessLevel.Curator)
await personRepository.SetMaintainersAsync(person.Id, personBulkDto.Data.MaintainerUserIds, CancellationToken.None);
}
var result = new CascadeResultDto { PeopleUpdated = people.Count };
var cascade = personBulkDto.Cascade;
var visibility = personBulkDto.Data.Visibility;
if (cascade != null && visibility.HasValue) {
if (cascade.ToAlbums)
result.AlbumsUpdated = await albumRepository.BulkSetVisibilityByPersonIdsAsync(personBulkDto.Ids, visibility.Value, CancellationToken.None);
if (cascade.ToAssets)
result.AssetsUpdated = await assetRepository.BulkSetVisibilityByPersonIdsAsync(personBulkDto.Ids, visibility.Value, CancellationToken.None);
}
await personRepository.SaveAsync(CancellationToken.None);
return result;
});
return Ok(result);
}
/// <summary>
/// Deletes a person by ID (hard delete). Cascades to unlink all associated albums.
/// </summary>
/// <param name="id">The person ID.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>200 on success, 404 if not found.</returns>
[HttpDelete("{id}")]
[Authorize(Roles = "Admin, Curator")]
public async Task<IActionResult> Delete([FromRoute] Guid id, CancellationToken cancellationToken) {
var person = await personRepository.FindAsync(id, cancellationToken);
if (person == null) return NotFound();
// Unlink all albums owned by this person
var albums = await albumRepository.FindByPersonAsync(id, cancellationToken);
foreach (var album in albums) {
album.PersonOwnerId = null;
}
personRepository.Remove(person);
await personRepository.SaveAsync(cancellationToken);
return Ok();
}
/// <summary>
/// Deletes multiple people in a bulk operation (hard delete). Cascades to unlink all associated albums.
/// </summary>
/// <param name="ids">The list of person IDs to delete.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>200 on success.</returns>
[HttpDelete]
[Authorize(Roles = "Admin, Curator")]
public async Task<IActionResult> BulkDelete([FromBody] List<Guid> ids, CancellationToken cancellationToken) {
var people = await personRepository.FindBulkAsync(ids, cancellationToken);
foreach (var person in people) {
// Unlink all albums owned by this person
var albums = await albumRepository.FindByPersonAsync(person.Id, cancellationToken);
foreach (var album in albums) {
album.PersonOwnerId = null;
}
personRepository.Remove(person);
}
await personRepository.SaveAsync(cancellationToken);
return Ok();
}
/// <summary>
/// Merges multiple source people into a destination person.
/// Albums, faces, and maintainers are reassigned to the destination, then source people are hard-deleted.
/// </summary>
/// <param name="dto">The merge request with destination and source IDs.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>200 on success, 400 if destination is in sources or not found.</returns>
[HttpPost("merge")]
[Authorize(Roles = "Admin, Curator")]
public async Task<IActionResult> Merge([FromBody] PersonMergeDto dto, CancellationToken cancellationToken) {
if (dto.SourceIds.Contains(dto.DestinationId))
return BadRequest("Destination person ID must not appear in source person IDs.");
if (await personRepository.FindAsync(dto.DestinationId, cancellationToken) == null)
return NotFound("Destination person not found.");
await personRepository.MergePeopleAsync(dto.DestinationId, dto.SourceIds, cancellationToken);
await personRepository.SaveAsync(cancellationToken);
return Ok();
}
/// <summary>
/// Creates a new person.
/// </summary>
/// <param name="dto">The person creation data.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>200 with the new person ID.</returns>
[HttpPut]
[Authorize(Roles = "Admin, Curator")]
public async Task<ActionResult<Guid>> Create([FromBody] PersonCreateDto dto, CancellationToken cancellationToken) {
var person = new Models.Person {
Id = Guid.NewGuid(),
Name = dto.Name,
ProfileAssetId = dto.ProfileAssetId,
CreatedAt = DateTime.UtcNow
};
personRepository.Insert(person);
await personRepository.SaveAsync(cancellationToken);
return Ok(person.Id);
}
}