UserController.java

package com.albertsilva.dev.asjcatalog.web.controller;

import java.net.URI;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import com.albertsilva.dev.asjcatalog.dto.user.request.UserCreateRequest;
import com.albertsilva.dev.asjcatalog.dto.user.request.UserUpdateRequest;
import com.albertsilva.dev.asjcatalog.dto.user.response.UserDetailsResponse;
import com.albertsilva.dev.asjcatalog.dto.user.response.UserResponse;
import com.albertsilva.dev.asjcatalog.service.UserService;
import com.albertsilva.dev.asjcatalog.web.exception.response.ProblemDetails;
import com.albertsilva.dev.asjcatalog.web.exception.response.ValidationError;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;

/**
 * Endpoints <b>administrativos de usuários</b> ({@code /api/v1/users}),
 * delegados a {@code UserService}. Os fluxos da própria conta ficam em
 * {@code AccountController}.
 *
 * <p>
 * <b>Acesso:</b> nenhuma rota é pública: a regra de URL exige autenticação em
 * todas, e o {@code @PreAuthorize} restringe a {@code ADMIN}, exceto duas rotas
 * com regra por usuário para {@code OPERATOR}: {@code GET /{id}} (usa
 * {@code AuthenticatedUserService.isCurrentUser(#id)}, com o {@code userId} do
 * JWT) e {@code PUT /{id}} (usa {@code authentication.principal.id}; ver a
 * documentação do método).
 * </p>
 *
 * <p>
 * <b>Logs:</b> {@code create} e {@code update} registram, em nível
 * {@code DEBUG}, o DTO inteiro; como o {@code toString()} de um {@code record}
 * inclui todos os componentes, isso <b>inclui a senha em texto</b> quando
 * informada.
 * </p>
 */
@Tag(name = "Usuários", description = "Operações administrativas para gerenciamento de usuários do sistema.")
@RestController
@RequestMapping("/api/v1/users")
public class UserController {

  private static final Logger logger = LoggerFactory.getLogger(UserController.class);

  private final UserService userService;

  /**
   * @param userService service de usuários
   */
  public UserController(UserService userService) {
    this.userService = userService;
  }

  /**
   * <b>{@code POST /api/v1/users}</b> — cria um usuário <b>ativo</b>.
   *
   * <ul>
   * <li><b>Acesso:</b> {@code ADMIN}.</li>
   * <li><b>Entrada:</b> {@link UserCreateRequest} com {@code @Valid} (e-mail
   * válido por DNS e único, senha forte de 10 a 72 caracteres sem dados
   * pessoais, {@code roleIds} existentes; {@code roleIds} nulo ou vazio cria
   * usuário sem roles, que não consegue autenticar).</li>
   * <li><b>Log:</b> {@code DEBUG} com o DTO inteiro, inclusive a senha.</li>
   * <li><b>Fluxo:</b> {@code UserService.create}.</li>
   * <li><b>Sucesso:</b> {@code 201} com {@link UserResponse} e {@code Location}
   * = URL da requisição + {@code /{id}}.</li>
   * <li><b>Erros:</b> {@code 422}; {@code 404} (role não encontrada no service);
   * {@code 403}; {@code 409}.</li>
   * </ul>
   *
   * @param userCreateRequest dados do usuário
   * @return resposta {@code 201} com o usuário criado
   */
  @Operation(summary = "Cria um novo usuário", description = "Cria um novo usuário no sistema e retorna o recurso criado. Requer autenticação com Bearer Token e permissão ADMIN.", security = @SecurityRequirement(name = "security"))
  @ApiResponses({
      @ApiResponse(responseCode = "201", description = "Usuário criado com sucesso", content = @Content(mediaType = "application/json", schema = @Schema(implementation = UserResponse.class))),
      @ApiResponse(responseCode = "400", description = "Requisição inválida", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "401", description = "Usuário não autenticado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "403", description = "Acesso negado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "409", description = "Conflito de dados", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "422", description = "Erro de validação", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ValidationError.class))),
      @ApiResponse(responseCode = "500", description = "Erro interno do servidor", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class)))
  })
  @PostMapping
  @PreAuthorize("hasRole('ADMIN')")
  public ResponseEntity<UserResponse> create(@Valid @RequestBody UserCreateRequest userCreateRequest) {
    logger.debug("Recebendo requisição para criar usuário: {}", userCreateRequest);
    UserResponse response = userService.create(userCreateRequest);
    URI uri = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(response.id()).toUri();

    logger.info("Usuário criado com sucesso. id: {}", response.id());
    return ResponseEntity.created(uri).body(response);
  }

  /**
   * <b>{@code GET /api/v1/users}</b> — lista paginada de usuários, com filtro
   * opcional por primeiro nome. Acesso: {@code ADMIN}. Parâmetros:
   * {@code firstName} (opcional) e {@code page}/{@code size}/{@code sort}.
   * Fluxo: {@code UserService.search}. Sucesso: {@code 200} com
   * {@code Page<UserResponse>} (sem senha nem {@code active}). Erros:
   * {@code 403}.
   *
   * @param firstName termo procurado no primeiro nome (opcional)
   * @param pageable  página, tamanho e ordenação
   * @return resposta {@code 200} com a página de usuários
   */
  @Operation(summary = "Lista usuários com paginação", description = "Retorna uma página de usuários filtrados por nome. Requer autenticação com Bearer Token e permissão ADMIN.", security = @SecurityRequirement(name = "security"))
  @ApiResponses({
      @ApiResponse(responseCode = "200", description = "Usuários consultados com sucesso", content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = UserResponse.class)))),
      @ApiResponse(responseCode = "401", description = "Usuário não autenticado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "403", description = "Acesso negado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "500", description = "Erro interno do servidor", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class)))
  })
  @GetMapping
  @PreAuthorize("hasRole('ADMIN')")
  public ResponseEntity<Page<UserResponse>> findAll(@RequestParam(required = false) String firstName,
      Pageable pageable) {

    logger.debug("Buscando usuários - firstName: {}, page: {}, size: {}, sort: {}",
        firstName,
        pageable.getPageNumber(),
        pageable.getPageSize(),
        pageable.getSort().isSorted() ? pageable.getSort() : "unsorted");

    Page<UserResponse> response = userService.search(firstName, pageable);

    logger.debug("Usuários encontrados: {}", response.getTotalElements());

    return ResponseEntity.ok(response);
  }

  /**
   * <b>{@code GET /api/v1/users/{id}}</b> — detalhes de um usuário.
   *
   * <ul>
   * <li><b>Acesso:</b> {@code ADMIN}, ou {@code OPERATOR} <b>somente para o
   * próprio usuário</b>: {@code @authenticatedUserService.isCurrentUser(#id)}
   * compara {@code #id} com o claim {@code userId} do JWT (esta regra é
   * coerente com a identidade do token).</li>
   * <li><b>Fluxo:</b> {@code UserService.findById}.</li>
   * <li><b>Sucesso:</b> {@code 200} com {@link UserDetailsResponse} (inclui
   * {@code active}; sem senha).</li>
   * <li><b>Erros:</b> {@code 404}; {@code 403} (inclusive {@code OPERATOR}
   * consultando outro usuário); {@code 401} se o JWT não resolver um
   * usuário.</li>
   * </ul>
   *
   * @param id identificador do usuário
   * @return resposta {@code 200} com os detalhes
   */
  @Operation(summary = "Busca um usuário pelo ID", description = "Retorna os dados completos de um usuário existente. Requer autenticação com Bearer Token e permissão ADMIN ou OPERATOR para o próprio usuário.", security = @SecurityRequirement(name = "security"))
  @ApiResponses({
      @ApiResponse(responseCode = "200", description = "Usuário encontrado com sucesso", content = @Content(mediaType = "application/json", schema = @Schema(implementation = UserDetailsResponse.class))),
      @ApiResponse(responseCode = "401", description = "Usuário não autenticado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "403", description = "Acesso negado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "404", description = "Recurso não encontrado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "500", description = "Erro interno do servidor", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class)))
  })
  @GetMapping(value = "/{id}")
  @PreAuthorize("hasRole('ADMIN') OR (hasRole('OPERATOR') AND @authenticatedUserService.isCurrentUser(#id))")
  public ResponseEntity<UserDetailsResponse> findById(@PathVariable Long id) {
    logger.debug("Buscando usuário por id: {}", id);

    UserDetailsResponse response = userService.findById(id);

    logger.debug("Usuário encontrado: id={}", id);
    return ResponseEntity.ok(response);
  }

  /**
   * <b>{@code PUT /api/v1/users/{id}}</b> — atualiza nome, e-mail e, se
   * informados, senha e roles.
   *
   * <ul>
   * <li><b>Acesso:</b> {@code ADMIN}, ou {@code OPERATOR} quando
   * {@code #id == authentication.principal.id}.</li>
   * <li><b>Atenção ao segundo termo:</b> {@code authentication} é um
   * {@code JwtAuthenticationToken} e seu principal é o {@code Jwt}; a propriedade
   * {@code id} do principal é {@code Jwt.getId()}, que devolve o claim
   * <b>{@code jti}</b> (id do token, texto), e não o {@code userId}. Como
   * {@code #id} é {@code Long}, a comparação tende a ser sempre falsa: na
   * prática, <b>{@code OPERATOR} não consegue atualizar nenhum usuário, nem a si
   * mesmo</b> (resposta esperada {@code 403}, a confirmar em execução);
   * {@code ADMIN} não é afetado, pois {@code hasRole('ADMIN')} vem antes.</li>
   * <li><b>Ordem:</b> a validação do corpo ({@code @Valid}, com
   * {@code @UserUpdateValid}) ocorre <b>antes</b> do {@code @PreAuthorize}; um
   * {@code OPERATOR} com corpo inválido recebe {@code 422} em vez de
   * {@code 403}.</li>
   * <li><b>Entrada:</b> {@link UserUpdateRequest} ({@code roleIds} nulo mantém
   * as roles; conjunto vazio remove todas; {@code password} nulo mantém a
   * senha).</li>
   * <li><b>Log:</b> {@code DEBUG} com o DTO inteiro, inclusive a senha.</li>
   * <li><b>Fluxo/sucesso:</b> {@code UserService.update}; {@code 200} com
   * {@link UserResponse}. <b>Erros:</b> {@code 422}, {@code 404}, {@code 403}.</li>
   * </ul>
   *
   * @param id                identificador do usuário
   * @param userUpdateRequest novos dados
   * @return resposta {@code 200} com o usuário atualizado
   */
  @Operation(summary = "Atualiza os dados de um usuário", description = "Atualiza completamente os dados de um usuário existente. Requer autenticação com Bearer Token e permissão ADMIN ou OPERATOR para o próprio usuário.", security = @SecurityRequirement(name = "security"))
  @ApiResponses({
      @ApiResponse(responseCode = "200", description = "Usuário atualizado com sucesso", content = @Content(mediaType = "application/json", schema = @Schema(implementation = UserResponse.class))),
      @ApiResponse(responseCode = "400", description = "Requisição inválida", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "401", description = "Usuário não autenticado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "403", description = "Acesso negado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "404", description = "Recurso não encontrado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "422", description = "Erro de validação", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ValidationError.class))),
      @ApiResponse(responseCode = "500", description = "Erro interno do servidor", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class)))
  })
  @PutMapping(value = "/{id}")
  @PreAuthorize("hasRole('ADMIN') OR (hasRole('OPERATOR') AND #id == authentication.principal.id)")
  public ResponseEntity<UserResponse> update(@PathVariable Long id,
      @Valid @RequestBody UserUpdateRequest userUpdateRequest) {
    logger.debug("Atualizando usuário id={} com dados: {}", id, userUpdateRequest);

    UserResponse response = userService.update(id, userUpdateRequest);

    logger.info("Usuário atualizado com sucesso. id={}", id);
    return ResponseEntity.ok(response);
  }

  /**
   * <b>{@code PATCH /api/v1/users/{id}/activate}</b> — ativa o usuário (sem
   * corpo). Acesso: {@code ADMIN}. Fluxo: {@code UserService.activate}. Sucesso:
   * {@code 204}. Erros: {@code 404}, {@code 403}.
   *
   * @param id identificador do usuário
   * @return resposta {@code 204}
   */
  @Operation(summary = "Ativa um usuário", description = "Ativa um usuário existente. Requer autenticação com Bearer Token e permissão ADMIN.", security = @SecurityRequirement(name = "security"))
  @ApiResponses({
      @ApiResponse(responseCode = "204", description = "Usuário ativado com sucesso"),
      @ApiResponse(responseCode = "401", description = "Usuário não autenticado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "403", description = "Acesso negado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "404", description = "Recurso não encontrado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "500", description = "Erro interno do servidor", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class)))
  })
  @PatchMapping("/{id}/activate")
  @PreAuthorize("hasRole('ADMIN')")
  public ResponseEntity<Void> activate(@PathVariable Long id) {
    logger.debug("Ativando usuário id={}", id);

    userService.activate(id);

    logger.info("Usuário ativado com sucesso. id={}", id);
    return ResponseEntity.noContent().build();
  }

  /**
   * <b>{@code PATCH /api/v1/users/{id}/deactivate}</b> — desativa o usuário (sem
   * corpo). Acesso: {@code ADMIN}. Fluxo: {@code UserService.deactivate}.
   * Sucesso: {@code 204}. Erros: {@code 404}, {@code 403}. Não revoga tokens já
   * emitidos.
   *
   * @param id identificador do usuário
   * @return resposta {@code 204}
   */
  @Operation(summary = "Desativa um usuário", description = "Desativa um usuário existente. Requer autenticação com Bearer Token e permissão ADMIN.", security = @SecurityRequirement(name = "security"))
  @ApiResponses({
      @ApiResponse(responseCode = "204", description = "Usuário desativado com sucesso"),
      @ApiResponse(responseCode = "401", description = "Usuário não autenticado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "403", description = "Acesso negado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "404", description = "Recurso não encontrado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "500", description = "Erro interno do servidor", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class)))
  })
  @PatchMapping("/{id}/deactivate")
  @PreAuthorize("hasRole('ADMIN')")
  public ResponseEntity<Void> deactivate(@PathVariable Long id) {
    logger.debug("Desativando usuário id={}", id);

    userService.deactivate(id);

    logger.info("Usuário desativado com sucesso. id={}", id);
    return ResponseEntity.noContent().build();
  }

  /**
   * <b>{@code DELETE /api/v1/users/{id}}</b> — remove fisicamente o usuário e seus
   * tokens de conta. Acesso: {@code ADMIN}. Fluxo: {@code UserService.delete}.
   * Sucesso: {@code 204}. Erros: {@code 404}, {@code 403}, {@code 409}.
   *
   * @param id identificador do usuário
   * @return resposta {@code 204}
   */
  @Operation(summary = "Remove um usuário", description = "Remove um usuário existente do sistema. Requer autenticação com Bearer Token e permissão ADMIN.", security = @SecurityRequirement(name = "security"))
  @ApiResponses({
      @ApiResponse(responseCode = "204", description = "Usuário deletado com sucesso"),
      @ApiResponse(responseCode = "401", description = "Usuário não autenticado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "403", description = "Acesso negado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "404", description = "Recurso não encontrado", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "409", description = "Conflito de dados", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class))),
      @ApiResponse(responseCode = "500", description = "Erro interno do servidor", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProblemDetails.class)))
  })
  @DeleteMapping(value = "/{id}")
  @PreAuthorize("hasRole('ADMIN')")
  public ResponseEntity<Void> delete(@PathVariable Long id) {
    logger.debug("Deletando usuário id={}", id);

    userService.delete(id);

    logger.info("Usuário deletado com sucesso. id={}", id);
    return ResponseEntity.noContent().build();
  }
}