mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-12 16:38:48 +00:00
350 lines
16 KiB
Java
350 lines
16 KiB
Java
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<String> 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<String, String> 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<Map<String, Object>> 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<String> searchable = List.of("fullName", "userName", "enabled", "rolesConcat"); // <- busca por roles de
|
|
// verdad
|
|
List<String> orderable = List.of("id", "fullName", "userName", "enabled", "roleRank"); // <- permite ordenar por
|
|
// estas
|
|
// columnas
|
|
|
|
Specification<User> 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 "<span class=\"badge bg-success\" >"
|
|
+ messageSource.getMessage("usuarios.tabla.activo", null, locale) + "</span>";
|
|
} else {
|
|
return "<span class=\"badge bg-danger\" >"
|
|
+ messageSource.getMessage("usuarios.tabla.inactivo", null, locale) + "</span>";
|
|
}
|
|
})
|
|
// acciones virtuales:
|
|
.add("roles", (User u) -> u.getRoles().stream()
|
|
.map(Role::getName)
|
|
.map(String::toLowerCase)
|
|
.map(rol -> "<span class=\"badge bg-primary\">" +
|
|
messageSource.getMessage("usuarios.rol." + rol, null, locale) + "</span>")
|
|
.collect(Collectors.joining(" ")))
|
|
.add("actions", (user) -> {
|
|
|
|
boolean isSuperAdmin = authentication.getAuthorities().stream()
|
|
.anyMatch(a -> a.getAuthority().equals("ROLE_SUPERADMIN"));
|
|
|
|
if (!isSuperAdmin) {
|
|
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
|
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
|
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
|
|
+
|
|
" </div>";
|
|
} else {
|
|
// Admin editando otro admin o usuario normal: puede editarse y eliminarse
|
|
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
|
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
|
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
|
|
+
|
|
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
|
+ "\" class=\"link-danger btn-delete-user fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>\n"
|
|
+
|
|
" </div>";
|
|
}
|
|
})
|
|
.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<Role> 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))));
|
|
}
|
|
}
|