package com.imprimelibros.erp.users; import com.imprimelibros.erp.datatables.DataTablesResponse; import com.imprimelibros.erp.i18n.TranslationService; import com.imprimelibros.erp.users.validation.UserForm; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.MessageSource; import org.springframework.data.jpa.domain.Specification; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; import com.imprimelibros.erp.datatables.DataTablesRequest; import com.imprimelibros.erp.datatables.DataTablesParser; import com.imprimelibros.erp.config.Sanitizer; import com.imprimelibros.erp.datatables.DataTable; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.time.LocalDateTime; import java.util.List; import java.util.Locale; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PathVariable; @Controller @PreAuthorize("hasRole('ADMIN') or hasRole('SUPERADMIN')") @RequestMapping("/users") public class UserController { private UserDao repo; private RoleDao roleRepo; private MessageSource messageSource; private Sanitizer sanitizer; private PasswordEncoder passwordEncoder; private TranslationService translationService; public UserController(UserDao repo, UserService userService, MessageSource messageSource, Sanitizer sanitizer, PasswordEncoder passwordEncoder, RoleDao roleRepo, TranslationService translationService) { this.repo = repo; this.messageSource = messageSource; this.sanitizer = sanitizer; this.roleRepo = roleRepo; this.passwordEncoder = passwordEncoder; this.translationService = translationService; } @GetMapping public String list(Model model, Authentication authentication, Locale locale) { List keys = List.of( "usuarios.delete.title", "usuarios.delete.text", "usuarios.eliminar", "usuarios.delete.button", "app.yes", "app.cancelar", "usuarios.delete.ok.title", "usuarios.delete.ok.text"); Map translations = translationService.getTranslations(locale, keys); model.addAttribute("languageBundle", translations); return "imprimelibros/users/users-list"; } // IMPORTANTE: asegúrate de que el controller es @RestController O anota el // método con @ResponseBody. @GetMapping(value = "/datatable", produces = "application/json") @ResponseBody public DataTablesResponse> datatable(HttpServletRequest request, Authentication authentication, Locale locale) { DataTablesRequest dt = DataTablesParser.from(request); // // OJO: en la whitelist mete solo columnas "reales" y escalares (no relaciones). // Si 'role' es relación, sácalo de aquí: List searchable = List.of("fullName", "userName", "enabled", "rolesConcat"); // <- busca por roles de // verdad List orderable = List.of("id", "fullName", "userName", "enabled", "roleRank"); // <- permite ordenar por // estas // columnas Specification base = (root, query, cb) -> cb.conjunction(); long total = repo.count(); return DataTable .of(repo, User.class, dt, searchable) // 'searchable' en DataTable.java // edita columnas "reales": .orderable(orderable) .edit("enabled", (User u) -> { if (u.isEnabled()) { return "" + messageSource.getMessage("usuarios.tabla.activo", null, locale) + ""; } else { return "" + messageSource.getMessage("usuarios.tabla.inactivo", null, locale) + ""; } }) // acciones virtuales: .add("roles", (User u) -> u.getRoles().stream() .map(Role::getName) .map(String::toLowerCase) .map(rol -> "" + messageSource.getMessage("usuarios.rol." + rol, null, locale) + "") .collect(Collectors.joining(" "))) .add("actions", (user) -> { boolean isSuperAdmin = authentication.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_SUPERADMIN")); if (!isSuperAdmin) { return "
\n" + " \n" + "
"; } else { // Admin editando otro admin o usuario normal: puede editarse y eliminarse return "
\n" + " \n" + " \n" + "
"; } }) .where(base) // Filtros custom: .filter((builder, req) -> { // f_enabled: 'true' | 'false' | '' String fEnabled = Optional.ofNullable(req.raw.get("f_enabled")).orElse("").trim(); if (!fEnabled.isEmpty()) { boolean enabledVal = Boolean.parseBoolean(fEnabled); builder.add((root, q, cb) -> cb.equal(root.get("enabled"), enabledVal)); } // f_role: 'USER' | 'ADMIN' | 'SUPERADMIN' | '' String fRole = Optional.ofNullable(req.raw.get("f_role")).orElse("").trim(); if (!fRole.isEmpty()) { builder.add((root, q, cb) -> { // join a roles; marca la query como distinct para evitar duplicados var r = root.join("roles", jakarta.persistence.criteria.JoinType.LEFT); q.distinct(true); return cb.equal(r.get("name"), fRole); }); } }) .toJson(total); } @GetMapping("form") public String getForm(@RequestParam(required = false) Long id, @ModelAttribute("user") UserForm form, BindingResult binding, Model model, HttpServletResponse response, Locale locale) { if (id != null) { var opt = repo.findById(id); if (opt.isEmpty()) { binding.reject("usuarios.error.noEncontrado", messageSource.getMessage("usuarios.error.noEncontrado", null, locale)); response.setStatus(404); model.addAttribute("action", "/users/" + id); return "imprimelibros/users/user-form :: userForm"; } User u = opt.get(); // map ENTIDAD -> DTO (¡no metas la entidad en "user"!) form.setId(u.getId()); form.setFullName(u.getFullName()); form.setUserName(u.getUserName()); form.setEnabled(u.isEnabled()); form.setRoleName(u.getRoles().stream().findFirst().map(Role::getName).orElse("USER")); form.setPassword(null); form.setConfirmPassword(null); model.addAttribute("action", "/users/" + id); } else { // Crear: valores por defecto form.setEnabled(true); model.addAttribute("action", "/users"); } return "imprimelibros/users/user-form :: userForm"; } @PostMapping public String create( @Validated(UserForm.Create.class) @ModelAttribute("user") UserForm form, BindingResult binding, Model model, HttpServletResponse response, Locale locale) { String normalized = sanitizer.plain(form.getUserName().trim()); if (repo.existsByUserNameIgnoreCase(normalized)) { binding.rejectValue("userName", "validation.unique", messageSource.getMessage("usuarios.error.duplicado", null, locale)); } var optRole = roleRepo.findRoleByName(form.getRoleName()); if (optRole.isEmpty()) { binding.rejectValue("roleName", "usuarios.errores.rol.invalido", messageSource.getMessage("usuarios.error.rol", null, locale)); } if (binding.hasErrors()) { response.setStatus(422); // <- clave model.addAttribute("action", "/users"); return "imprimelibros/users/user-form :: userForm"; } User u = new User(); u.setFullName(sanitizer.plain(form.getFullName())); u.setUserName(normalized.toLowerCase()); u.setPassword(passwordEncoder.encode(form.getPassword())); java.util.Set roles = new java.util.HashSet<>(); roles.add(optRole.get()); u.setRoles(roles); u.setEnabled(Boolean.TRUE.equals(form.getEnabled())); try { repo.save(u); } catch (org.springframework.dao.DataIntegrityViolationException ex) { // carrera contra otra inserción: vuelve como error de campo binding.rejectValue("userName", "validation.unique", messageSource.getMessage("usuarios.error.duplicado", null, locale)); response.setStatus(422); model.addAttribute("action", "/users"); return "imprimelibros/users/user-form :: userForm"; } response.setStatus(204); return null; } @PutMapping("/{id}") public String edit( @PathVariable Long id, @Validated(UserForm.Update.class) @ModelAttribute("user") UserForm form, BindingResult binding, Model model, HttpServletResponse response, Locale locale) { var uOpt = repo.findById(id); if (uOpt.isEmpty()) { binding.reject("usuarios.error.noEncontrado", messageSource.getMessage("usuarios.error.noEncontrado", null, locale)); } String normalized = sanitizer.plain(form.getUserName()).trim(); if (repo.existsByUserNameIgnoreCaseAndIdNot(normalized, id)) { binding.rejectValue("userName", "validation.unique", messageSource.getMessage("usuarios.error.duplicado", null, locale)); } var optRole = roleRepo.findRoleByName(form.getRoleName()); if (optRole.isEmpty()) { binding.rejectValue("roleName", "usuarios.errores.rol.invalido", messageSource.getMessage("usuarios.error.rol", null, locale)); } if (binding.hasErrors()) { response.setStatus(422); model.addAttribute("action", "/users/" + id); return "imprimelibros/users/user-form :: userForm"; } var u = uOpt.get(); u.setFullName(sanitizer.plain(form.getFullName()).trim()); u.setUserName(normalized.toLowerCase()); if (form.getPassword() != null && !form.getPassword().isBlank()) { u.setPassword(passwordEncoder.encode(form.getPassword())); } u.setRoles(new java.util.HashSet<>(java.util.List.of(optRole.get()))); u.setEnabled(Boolean.TRUE.equals(form.getEnabled())); try { repo.save(u); } catch (org.springframework.dao.DataIntegrityViolationException ex) { binding.rejectValue("userName", "validation.unique", messageSource.getMessage("usuarios.error.duplicado", null, locale)); response.setStatus(422); model.addAttribute("action", "/users/" + id); return "imprimelibros/users/user-form :: userForm"; } response.setStatus(204); return null; } @DeleteMapping("/{id}") @Transactional public ResponseEntity delete(@PathVariable Long id, Authentication auth, Locale locale) { return repo.findById(id).map(u -> { if (auth != null && u.getUserName().equalsIgnoreCase(auth.getName())) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(Map.of("message", messageSource.getMessage("usuarios.error.delete-self", null, locale))); } try { Long currentUserId = null; if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) { currentUserId = udi.getId(); } else if (auth != null) { currentUserId = repo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null); // fallback } u.setDeleted(true); u.setDeletedAt(LocalDateTime.now()); u.setDeletedBy(currentUserId); // Soft-delete de los vínculos (si usas cascade REMOVE + @SQLDelete en UserRole, // podrías omitir este foreach y dejar que JPA lo haga) u.getRolesLink().forEach(UserRole::softDelete); repo.save(u); // ← NO delete(); guardamos el soft delete con deleted_by relleno return ResponseEntity.ok(Map.of("message", messageSource.getMessage("usuarios.exito.eliminado", null, locale))); } catch (Exception ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of("message", messageSource.getMessage("usuarios.error.delete-internal-error", null, locale))); } }).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND) .body(Map.of("message", messageSource.getMessage("usuarios.error.not-found", null, locale)))); } }