mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-28 22:58:49 +00:00
falta borrar y busqueda por columnas
This commit is contained in:
13
pom.xml
13
pom.xml
@ -102,6 +102,19 @@
|
|||||||
<version>1.17.2</version>
|
<version>1.17.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jsoup</groupId>
|
||||||
|
<artifactId>jsoup</artifactId>
|
||||||
|
<version>1.17.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Escape seguro al renderizar -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.owasp.encoder</groupId>
|
||||||
|
<artifactId>encoder</artifactId>
|
||||||
|
<version>1.3.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
25
src/main/java/com/imprimelibros/erp/config/Sanitizer.java
Normal file
25
src/main/java/com/imprimelibros/erp/config/Sanitizer.java
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package com.imprimelibros.erp.config;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.safety.Safelist;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class Sanitizer {
|
||||||
|
|
||||||
|
// Sin HTML: todo a texto plano
|
||||||
|
public String plain(String input) {
|
||||||
|
if (input == null) return null;
|
||||||
|
String cleaned = Jsoup.clean(input, Safelist.none());
|
||||||
|
return cleaned.strip();
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML mínimo permitido (opcional)
|
||||||
|
public String minimalHtml(String input) {
|
||||||
|
if (input == null) return null;
|
||||||
|
Safelist wl = Safelist.basic(); // b, i, em, strong, a...
|
||||||
|
wl.addTags("ul","ol","li"); // añade lo que necesites
|
||||||
|
wl.addAttributes("a","rel","nofollow"); // endurece enlaces
|
||||||
|
return Jsoup.clean(input, wl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@ import javax.sql.DataSource;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
|||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.BiFunction;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class DataTable<T> {
|
public class DataTable<T> {
|
||||||
@ -28,6 +27,7 @@ public class DataTable<T> {
|
|||||||
private final List<FilterHook<T>> filters = new ArrayList<>();
|
private final List<FilterHook<T>> filters = new ArrayList<>();
|
||||||
private Specification<T> baseSpec = (root, q, cb) -> cb.conjunction();
|
private Specification<T> baseSpec = (root, q, cb) -> cb.conjunction();
|
||||||
private final ObjectMapper om = new ObjectMapper();
|
private final ObjectMapper om = new ObjectMapper();
|
||||||
|
private List<String> orderable = null;
|
||||||
|
|
||||||
private DataTable(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
|
private DataTable(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
|
||||||
List<String> searchable) {
|
List<String> searchable) {
|
||||||
@ -79,6 +79,15 @@ public class DataTable<T> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DataTable<T> orderable(List<String> fields) {
|
||||||
|
this.orderable = fields;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getOrderable() {
|
||||||
|
return (orderable == null || orderable.isEmpty()) ? this.searchable : this.orderable;
|
||||||
|
}
|
||||||
|
|
||||||
/** filter((builder, req) -> builder.add(miExtraSpec(req))) */
|
/** filter((builder, req) -> builder.add(miExtraSpec(req))) */
|
||||||
public DataTable<T> filter(FilterHook<T> hook) {
|
public DataTable<T> filter(FilterHook<T> hook) {
|
||||||
filters.add(hook);
|
filters.add(hook);
|
||||||
@ -105,7 +114,7 @@ public class DataTable<T> {
|
|||||||
continue;
|
continue;
|
||||||
if (!col.orderable)
|
if (!col.orderable)
|
||||||
continue;
|
continue;
|
||||||
if (!searchable.contains(field))
|
if (!getOrderable().contains(field))
|
||||||
continue; // << usa tu whitelist
|
continue; // << usa tu whitelist
|
||||||
|
|
||||||
orders.add(new Sort.Order(
|
orders.add(new Sort.Order(
|
||||||
@ -117,7 +126,7 @@ public class DataTable<T> {
|
|||||||
} else {
|
} else {
|
||||||
for (var c : dt.columns) {
|
for (var c : dt.columns) {
|
||||||
if (c != null && c.orderable && c.name != null && !c.name.isBlank()
|
if (c != null && c.orderable && c.name != null && !c.name.isBlank()
|
||||||
&& searchable.contains(c.name)) {
|
&& getOrderable().contains(c.name)) {
|
||||||
sort = Sort.by(c.name);
|
sort = Sort.by(c.name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package com.imprimelibros.erp.presupuesto;
|
|||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|||||||
@ -6,14 +6,12 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.BiFunction;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import com.imprimelibros.erp.i18n.TranslationService;
|
|||||||
import com.imprimelibros.erp.presupuesto.Presupuesto;
|
import com.imprimelibros.erp.presupuesto.Presupuesto;
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
package com.imprimelibros.erp.users;
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
import com.imprimelibros.erp.users.Role;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface RoleDao {
|
public interface RoleDao {
|
||||||
|
|
||||||
public Role findRoleByName(String theRoleName);
|
Optional<Role> findRoleByName(String theRoleName);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,9 @@ package com.imprimelibros.erp.users;
|
|||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import jakarta.persistence.TypedQuery;
|
import jakarta.persistence.TypedQuery;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@ -14,7 +17,7 @@ public class RoleDaoImpl implements RoleDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Role findRoleByName(String theRoleName) {
|
public Optional<Role> findRoleByName(String theRoleName) {
|
||||||
|
|
||||||
// retrieve/read from database using name
|
// retrieve/read from database using name
|
||||||
TypedQuery<Role> theQuery = entityManager.createQuery("from Role where name=:roleName", Role.class);
|
TypedQuery<Role> theQuery = entityManager.createQuery("from Role where name=:roleName", Role.class);
|
||||||
@ -27,7 +30,6 @@ public class RoleDaoImpl implements RoleDao {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
theRole = null;
|
theRole = null;
|
||||||
}
|
}
|
||||||
|
return Optional.ofNullable(theRole);
|
||||||
return theRole;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,35 +1,67 @@
|
|||||||
package com.imprimelibros.erp.users;
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.util.Collection;
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.hibernate.annotations.Formula;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "users")
|
@Table(name = "users", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(name = "uk_users_username", columnNames = "username")
|
||||||
|
})
|
||||||
public class User {
|
public class User {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@Column(name = "id")
|
@Column(name = "id")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Column(name = "fullname")
|
@Column(name = "fullname")
|
||||||
|
@NotBlank(message = "{validation.required}")
|
||||||
private String fullName;
|
private String fullName;
|
||||||
|
|
||||||
@Column(name = "username")
|
@Column(name = "username", nullable = false, length = 190)
|
||||||
|
@Email(message = "{validation.email}")
|
||||||
|
@NotBlank(message = "{validation.required}")
|
||||||
private String userName;
|
private String userName;
|
||||||
|
|
||||||
@Column(name = "password")
|
@Column(name = "password")
|
||||||
|
@NotBlank(message = "{validation.required}")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@Column(name = "enabled")
|
@Column(name = "enabled")
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
|
|
||||||
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
|
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
|
||||||
@JoinTable(name = "users_roles",
|
@JoinTable(name = "users_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
|
||||||
joinColumns = @JoinColumn(name = "user_id"),
|
private Set<Role> roles = new java.util.HashSet<>();
|
||||||
inverseJoinColumns = @JoinColumn(name = "role_id"))
|
|
||||||
private Collection<Role> roles;
|
|
||||||
|
|
||||||
|
// SUPERADMIN=3, ADMIN=2, USER=1 (ajusta a tus nombres reales)
|
||||||
|
@Formula("""
|
||||||
|
(
|
||||||
|
select coalesce(max(
|
||||||
|
case r.name
|
||||||
|
when 'SUPERADMIN' then 3
|
||||||
|
when 'ADMIN' then 2
|
||||||
|
else 1
|
||||||
|
end
|
||||||
|
), 0)
|
||||||
|
from users_roles ur
|
||||||
|
join roles r on r.id = ur.role_id
|
||||||
|
where ur.user_id = id
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
private Integer roleRank;
|
||||||
|
|
||||||
|
@Formula("""
|
||||||
|
(select group_concat(lower(r.name) order by r.name separator ', ')
|
||||||
|
from users_roles ur join roles r on r.id = ur.role_id
|
||||||
|
where ur.user_id = id)
|
||||||
|
""")
|
||||||
|
private String rolesConcat;
|
||||||
|
|
||||||
/* Constructors */
|
/* Constructors */
|
||||||
public User() {
|
public User() {
|
||||||
@ -43,7 +75,7 @@ public class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public User(String fullName, String userName, String password, boolean enabled,
|
public User(String fullName, String userName, String password, boolean enabled,
|
||||||
Collection<Role> roles) {
|
Set<Role> roles) {
|
||||||
this.fullName = fullName;
|
this.fullName = fullName;
|
||||||
this.userName = userName;
|
this.userName = userName;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
@ -93,14 +125,22 @@ public class User {
|
|||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<Role> getRoles() {
|
public Set<Role> getRoles() {
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRoles(Collection<Role> roles) {
|
public void setRoles(Set<Role> roles) {
|
||||||
this.roles = roles;
|
this.roles = roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getRoleRank() {
|
||||||
|
return roleRank;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRolesConcat() {
|
||||||
|
return rolesConcat;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "User{" +
|
return "User{" +
|
||||||
@ -112,5 +152,5 @@ public class User {
|
|||||||
", roles=" + roles +
|
", roles=" + roles +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,41 @@
|
|||||||
package com.imprimelibros.erp.users;
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||||
|
import com.imprimelibros.erp.users.validation.UserForm;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
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.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||||
|
import com.imprimelibros.erp.config.Sanitizer;
|
||||||
import com.imprimelibros.erp.datatables.DataTable;
|
import com.imprimelibros.erp.datatables.DataTable;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
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
|
@Controller
|
||||||
@PreAuthorize("hasRole('ADMIN') or hasRole('SUPERADMIN')")
|
@PreAuthorize("hasRole('ADMIN') or hasRole('SUPERADMIN')")
|
||||||
@ -29,11 +43,18 @@ import java.util.Locale;
|
|||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
private UserDao repo;
|
private UserDao repo;
|
||||||
|
private RoleDao roleRepo;
|
||||||
private MessageSource messageSource;
|
private MessageSource messageSource;
|
||||||
|
private Sanitizer sanitizer;
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
public UserController(UserDao repo, UserService userService, MessageSource messageSource) {
|
public UserController(UserDao repo, UserService userService, MessageSource messageSource, Sanitizer sanitizer,
|
||||||
|
PasswordEncoder passwordEncoder, RoleDao roleRepo) {
|
||||||
this.repo = repo;
|
this.repo = repo;
|
||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
|
this.sanitizer = sanitizer;
|
||||||
|
this.roleRepo = roleRepo;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@ -52,31 +73,215 @@ public class UserController {
|
|||||||
|
|
||||||
// OJO: en la whitelist mete solo columnas "reales" y escalares (no relaciones).
|
// OJO: en la whitelist mete solo columnas "reales" y escalares (no relaciones).
|
||||||
// Si 'role' es relación, sácalo de aquí:
|
// Si 'role' es relación, sácalo de aquí:
|
||||||
List<String> whitelist = List.of("fullName", "userName", "enabled");
|
List<String> searchable = List.of("fullName", "userName", "enabled", "rolesConcat"); // <- busca por roles de
|
||||||
|
// verdad
|
||||||
|
List<String> orderable = List.of("fullName", "userName", "enabled", "roleRank"); // <- permite ordenar por estas columnas
|
||||||
|
|
||||||
Specification<User> base = (root, query, cb) -> cb.conjunction();
|
Specification<User> base = (root, query, cb) -> cb.conjunction();
|
||||||
long total = repo.count();
|
long total = repo.count();
|
||||||
|
|
||||||
return DataTable
|
return DataTable
|
||||||
.of(repo, User.class, dt, whitelist) // 'searchable' en DataTable.java
|
.of(repo, User.class, dt, searchable) // 'searchable' en DataTable.java
|
||||||
|
// edita columnas "reales":
|
||||||
|
.orderable(orderable)
|
||||||
.edit("enabled", (User u) -> {
|
.edit("enabled", (User u) -> {
|
||||||
if (u.isEnabled()) {
|
if (u.isEnabled()) {
|
||||||
return "<span class=\"badge bg-success\" >" + messageSource.getMessage("usuarios.tabla.activo", null, locale) + "</span>";
|
return "<span class=\"badge bg-success\" >"
|
||||||
|
+ messageSource.getMessage("usuarios.tabla.activo", null, locale) + "</span>";
|
||||||
} else {
|
} else {
|
||||||
return "<span class=\"badge bg-danger\" >" + messageSource.getMessage("usuarios.tabla.inactivo", null, locale) + "</span>";
|
return "<span class=\"badge bg-danger\" >"
|
||||||
|
+ messageSource.getMessage("usuarios.tabla.inactivo", null, locale) + "</span>";
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// si 'role' es relación, crea una columna calculada “segura”:
|
|
||||||
// acciones virtuales:
|
// acciones virtuales:
|
||||||
.add("roles", (User u) -> u.getRoles().stream().map(Role::getName).collect(Collectors.joining(", ")))
|
.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) -> {
|
.add("actions", (user) -> {
|
||||||
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
||||||
" <a href=\"/users/" + user.getId() + "\" class=\"link-success fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n" +
|
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
||||||
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId() + "\" class=\"link-danger fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>\n" +
|
+ "\" 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 fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>\n" +
|
||||||
" </div>";
|
" </div>";
|
||||||
})
|
})
|
||||||
.where(base)
|
.where(base)
|
||||||
.toJson(total);
|
.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}")
|
||||||
|
@ResponseBody
|
||||||
|
public void delete(@PathVariable Long id, HttpServletResponse response, Authentication authentication) {
|
||||||
|
var uOpt = repo.findById(id);
|
||||||
|
if (uOpt.isEmpty()) {
|
||||||
|
response.setStatus(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var u = uOpt.get();
|
||||||
|
String currentUserName = authentication.getName();
|
||||||
|
if (u.getUserName().equalsIgnoreCase(currentUserName)) {
|
||||||
|
response.setStatus(403); // no puede borrarse a sí mismo
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
repo.delete(u);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
response.setStatus(500);
|
||||||
|
}
|
||||||
|
// Si llegamos aquí, la eliminación fue exitosa
|
||||||
|
/*
|
||||||
|
* response.setStatus(204);
|
||||||
|
* response.getWriter().flush();
|
||||||
|
* response.getWriter().close();
|
||||||
|
*/
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,4 +9,6 @@ public interface UserDao extends JpaRepository<User, Long>, JpaSpecificationExec
|
|||||||
|
|
||||||
|
|
||||||
User findByUserNameAndEnabledTrue(String userName);
|
User findByUserNameAndEnabledTrue(String userName);
|
||||||
|
boolean existsByUserNameIgnoreCase(String userName);
|
||||||
|
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package com.imprimelibros.erp.users;
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
import com.imprimelibros.erp.users.User;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package com.imprimelibros.erp.users;
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
import com.imprimelibros.erp.users.User;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
|
||||||
public interface UserService extends UserDetailsService {
|
public interface UserService extends UserDetailsService {
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.imprimelibros.erp.users.validation;
|
||||||
|
|
||||||
|
import jakarta.validation.Constraint;
|
||||||
|
import jakarta.validation.Payload;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import static java.lang.annotation.ElementType.TYPE;
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
@Target(TYPE)
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@Constraint(validatedBy = PasswordsMatchValidator.class)
|
||||||
|
public @interface PasswordsMatch {
|
||||||
|
String message() default "{usuarios.error.password-coinciden}";
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
String password();
|
||||||
|
String confirmPassword();
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.imprimelibros.erp.users.validation;
|
||||||
|
|
||||||
|
import jakarta.validation.ConstraintValidator;
|
||||||
|
import jakarta.validation.ConstraintValidatorContext;
|
||||||
|
import java.beans.PropertyDescriptor;
|
||||||
|
|
||||||
|
public class PasswordsMatchValidator implements ConstraintValidator<PasswordsMatch, Object> {
|
||||||
|
private String passwordField;
|
||||||
|
private String confirmPasswordField;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(PasswordsMatch constraint) {
|
||||||
|
this.passwordField = constraint.password();
|
||||||
|
this.confirmPasswordField = constraint.confirmPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(Object value, ConstraintValidatorContext context) {
|
||||||
|
try {
|
||||||
|
Object password = new PropertyDescriptor(passwordField, value.getClass())
|
||||||
|
.getReadMethod().invoke(value);
|
||||||
|
Object confirm = new PropertyDescriptor(confirmPasswordField, value.getClass())
|
||||||
|
.getReadMethod().invoke(value);
|
||||||
|
if (password == null && confirm == null) return true;
|
||||||
|
return password != null && password.equals(confirm);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,105 @@
|
|||||||
|
package com.imprimelibros.erp.users.validation;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO del formulario de creación/edición de usuarios
|
||||||
|
* (No ensucia la entidad y permite validaciones específicas de UI)
|
||||||
|
*/
|
||||||
|
@PasswordsMatch(password = "password", confirmPassword = "confirmPassword", groups = UserForm.Create.class)
|
||||||
|
public class UserForm {
|
||||||
|
|
||||||
|
/** Grupos de validación */
|
||||||
|
public interface Create {
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Update {
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank(message = "{usuarios.error.nombre}", groups = { Create.class, Update.class })
|
||||||
|
private String fullName;
|
||||||
|
|
||||||
|
@NotBlank(message = "{usuarios.error.email}", groups = { Create.class, Update.class })
|
||||||
|
@Email(message = "{usuarios.error.email.formato}", groups = { Create.class, Update.class })
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
@NotBlank(message = "{usuarios.error.rol}", groups = { Create.class, Update.class })
|
||||||
|
@Pattern(regexp = "USER|ADMIN|SUPERADMIN", message = "{usuarios.error.rol.invalido}", groups = { Create.class,
|
||||||
|
Update.class })
|
||||||
|
private String roleName;
|
||||||
|
|
||||||
|
// Obligatoria solo al crear
|
||||||
|
@NotBlank(message = "{usuarios.error.password.requerida}", groups = Create.class)
|
||||||
|
@Size(min = 6, message = "{usuarios.error.password.min}", groups = Create.class)
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
// Validada por @PasswordsMatch (y requerida al crear)
|
||||||
|
@NotBlank(message = "{usuarios.error.confirmPassword.requerida}", groups = Create.class)
|
||||||
|
private String confirmPassword;
|
||||||
|
|
||||||
|
@NotNull(groups = { Create.class, Update.class })
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
|
// ===== Getters / Setters =====
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFullName() {
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFullName(String fullName) {
|
||||||
|
this.fullName = fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserName() {
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserName(String userName) {
|
||||||
|
this.userName = userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRoleName() {
|
||||||
|
return roleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoleName(String roleName) {
|
||||||
|
this.roleName = roleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConfirmPassword() {
|
||||||
|
return confirmPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfirmPassword(String confirmPassword) {
|
||||||
|
this.confirmPassword = confirmPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(Boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -53,4 +53,9 @@ spring.web.resources.chain.strategy.content.paths=/assets/**
|
|||||||
#
|
#
|
||||||
server.servlet.session.timeout=30m
|
server.servlet.session.timeout=30m
|
||||||
|
|
||||||
security.rememberme.key=N`BY^YRVO:/\H$hsKxNq
|
security.rememberme.key=N`BY^YRVO:/\H$hsKxNq
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enable HiddenHttpMethodFilter to support PUT and DELETE methods in forms
|
||||||
|
#
|
||||||
|
spring.mvc.hiddenmethod.filter.enabled=true
|
||||||
@ -1,6 +1,12 @@
|
|||||||
usuarios.titulo=Usuarios
|
usuarios.titulo=Usuarios
|
||||||
usuarios.nuevo=Nuevo usuario
|
usuarios.nuevo=Nuevo usuario
|
||||||
|
usuarios.editar=Editar usuario
|
||||||
|
usuarios.add=Añadir usuario
|
||||||
|
usuarios.eliminar=Eliminar usuario
|
||||||
|
usuarios.confirmarEliminar=¿Está seguro de que desea eliminar este usuario?
|
||||||
|
usuarios.guardar=Guardar
|
||||||
|
|
||||||
|
usuarios.tabla.id=ID
|
||||||
usuarios.tabla.nombre=Nombre
|
usuarios.tabla.nombre=Nombre
|
||||||
usuarios.tabla.email=Correo electrónico
|
usuarios.tabla.email=Correo electrónico
|
||||||
usuarios.tabla.rol=Rol
|
usuarios.tabla.rol=Rol
|
||||||
@ -8,3 +14,31 @@ usuarios.tabla.estado=Estado
|
|||||||
usuarios.tabla.acciones=Acciones
|
usuarios.tabla.acciones=Acciones
|
||||||
usuarios.tabla.activo=Activo
|
usuarios.tabla.activo=Activo
|
||||||
usuarios.tabla.inactivo=Inactivo
|
usuarios.tabla.inactivo=Inactivo
|
||||||
|
|
||||||
|
usuarios.form.nombre=Nombre completo
|
||||||
|
usuarios.form.email=Correo electrónico
|
||||||
|
usuarios.form.password=Contraseña
|
||||||
|
usuarios.form.confirmarPassword=Confirmar contraseña
|
||||||
|
usuarios.form.rol=Rol
|
||||||
|
usuarios.form.estado=Estado
|
||||||
|
|
||||||
|
usuarios.rol.user=Usuario
|
||||||
|
usuarios.rol.admin=Administrador
|
||||||
|
usuarios.rol.superadmin=Super Administrador
|
||||||
|
|
||||||
|
usuarios.error.duplicado=Ya existe un usuario con este correo electrónico.
|
||||||
|
usuarios.error.general=Se ha producido un error al procesar la solicitud. Por favor, inténtelo de nuevo más tarde.
|
||||||
|
usuarios.error.noEncontrado=Usuario no encontrado.
|
||||||
|
|
||||||
|
usuarios.error.nombre=El nombre es obligatorio.
|
||||||
|
usuarios.error.email=El correo electrónico es obligatorio.
|
||||||
|
usuarios.error.email.formato=El correo electrónico no es válido.
|
||||||
|
usuarios.error.rol=El rol seleccionado no es válido.
|
||||||
|
usuarios.error.password.requerida=La contraseña es obligatoria.
|
||||||
|
usuarios.error.password.min=La contraseña debe tener al menos 6 caracteres.
|
||||||
|
usuarios.error.confirmPassword.requerida=La confirmación de la contraseña es obligatoria.
|
||||||
|
usuarios.error.password-coinciden=Las contraseñas no coinciden.
|
||||||
|
|
||||||
|
usuarios.exito.creado=Usuario creado con éxito.
|
||||||
|
usuarios.exito.actualizado=Usuario actualizado con éxito.
|
||||||
|
usuarios.exito.eliminado=Usuario eliminado con éxito.
|
||||||
@ -4,5 +4,7 @@ validation.min=El valor mínimo es {value}
|
|||||||
validation.max=El valor máximo es {value}
|
validation.max=El valor máximo es {value}
|
||||||
validation.typeMismatchMsg=Tipo de dato no válido
|
validation.typeMismatchMsg=Tipo de dato no válido
|
||||||
validation.patternMsg=El formato no es válido
|
validation.patternMsg=El formato no es válido
|
||||||
|
validation.unique=El valor ya existe y debe ser único
|
||||||
|
validation.email=El correo electrónico no es válido
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +1,84 @@
|
|||||||
$(() => {
|
$(() => {
|
||||||
const language = document.documentElement.lang || 'es-ES';
|
const language = document.documentElement.lang || 'es-ES';
|
||||||
|
|
||||||
const table = new DataTable('#users-datatable', {
|
const table = new DataTable('#users-datatable', {
|
||||||
processing: true,
|
processing: true, serverSide: true, pageLength: 50,
|
||||||
serverSide: true,
|
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||||
language: {
|
|
||||||
url: '/assets/libs/datatables/i18n/' + language + '.json'
|
|
||||||
},
|
|
||||||
responsive: true,
|
responsive: true,
|
||||||
ajax: {
|
ajax: { url: '/users/datatable', method: 'GET' },
|
||||||
url: '/users/datatable',
|
order: [[0,'asc']],
|
||||||
method: 'GET',
|
|
||||||
data: d => { /* extra params si quieres */ }
|
|
||||||
},
|
|
||||||
order: [[0, 'asc']],
|
|
||||||
columns: [
|
columns: [
|
||||||
{ data: 'fullName', name: 'fullname' },
|
{ data: 'id', name: 'id' , orderable: true },
|
||||||
{ data: 'userName', name: 'username' },
|
{ data: 'fullName', name: 'fullName' , orderable: true },
|
||||||
{ data: 'roles', name: 'roles' },
|
{ data: 'userName', name: 'userName' , orderable: true },
|
||||||
|
{ data: 'roles', name: 'roleRank' },
|
||||||
{ data: 'enabled', name: 'enabled', searchable: false },
|
{ data: 'enabled', name: 'enabled', searchable: false },
|
||||||
{ data: 'actions', name: 'actions' }
|
{ data: 'actions', name: 'actions' }
|
||||||
],
|
],
|
||||||
columnDefs: [
|
columnDefs: [{ targets: -1, orderable: false, searchable: false }]
|
||||||
// Desactiva orden y búsqueda en la columna de acciones
|
});
|
||||||
{ targets: -1, orderable: false, searchable: false }
|
|
||||||
]
|
const modalEl = document.getElementById('userFormModal');
|
||||||
|
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||||
|
|
||||||
|
// Abrir "Crear"
|
||||||
|
$('#addUserButton').on('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
$.get('/users/form', function (html) {
|
||||||
|
$('#userModalBody').html(html);
|
||||||
|
const title = $('#userModalBody #userForm').data('add');
|
||||||
|
$('#userFormModal .modal-title').text(title);
|
||||||
|
modal.show();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Abrir "Editar"
|
||||||
|
$(document).on('click', '.btn-edit-user', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = $(this).data('id');
|
||||||
|
$.get('/users/form', { id }, function (html) {
|
||||||
|
$('#userModalBody').html(html);
|
||||||
|
const title = $('#userModalBody #userForm').data('edit');
|
||||||
|
$('#userFormModal .modal-title').text(title);
|
||||||
|
modal.show();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit del form en el modal
|
||||||
|
$(document).on('submit', '#userForm', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const $form = $(this);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: $form.attr('action'),
|
||||||
|
type: 'POST', // PUT simulado via _method
|
||||||
|
data: $form.serialize(),
|
||||||
|
dataType: 'html',
|
||||||
|
success: function (html) {
|
||||||
|
// Si por cualquier motivo llega 200 con fragmento, lo insertamos igual
|
||||||
|
if (typeof html === 'string' && html.indexOf('id="userForm"') !== -1 && html.indexOf('<html') === -1) {
|
||||||
|
$('#userModalBody').html(html);
|
||||||
|
const isEdit = $('#userModalBody #userForm input[name="_method"][value="PUT"]').length > 0;
|
||||||
|
const title = $('#userModalBody #userForm').data(isEdit ? 'edit' : 'add');
|
||||||
|
$('#userFormModal .modal-title').text(title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Éxito real: cerrar y recargar tabla
|
||||||
|
modal.hide();
|
||||||
|
table.ajax.reload(null, false);
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
// Con 422 devolvemos el fragmento con errores aquí
|
||||||
|
if (xhr.status === 422 && xhr.responseText) {
|
||||||
|
$('#userModalBody').html(xhr.responseText);
|
||||||
|
const isEdit = $('#userModalBody #userForm input[name="_method"][value="PUT"]').length > 0;
|
||||||
|
const title = $('#userModalBody #userForm').data(isEdit ? 'edit' : 'add');
|
||||||
|
$('#userFormModal .modal-title').text(title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fallback
|
||||||
|
$('#userModalBody').html('<div class="p-3 text-danger">Error inesperado.</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,7 +7,6 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" th:id="${bodyId}">
|
<div class="modal-body" th:id="${bodyId}">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
<div th:fragment="userForm">
|
||||||
|
<form id="userForm" novalidate th:action="${action}" th:object="${user}" method="post" th:data-add="#{usuarios.add}"
|
||||||
|
th:data-edit="#{usuarios.editar}">
|
||||||
|
|
||||||
|
<input type="hidden" name="_method" value="PUT" th:if="${user.id != null}" />
|
||||||
|
|
||||||
|
<div th:if="${#fields.hasGlobalErrors()}" class="alert alert-danger">
|
||||||
|
<div th:each="e : ${#fields.globalErrors()}" th:text="${e}"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label th:text="#{usuarios.form.nombre}" for="nombre">Nombre</label>
|
||||||
|
<input type="text" class="form-control" id="nombre" th:field="*{fullName}"
|
||||||
|
th:classappend="${#fields.hasErrors('fullName')} ? ' is-invalid'" required>
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('fullName')}" th:errors="*{fullName}">Error</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label th:text="#{usuarios.form.email}" for="email">Correo electrónico</label>
|
||||||
|
<input type="email" class="form-control" id="email" th:field="*{userName}"
|
||||||
|
th:classappend="${#fields.hasErrors('userName')} ? ' is-invalid'" required>
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('userName')}" th:errors="*{userName}">Error</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label th:text="#{usuarios.form.password}" for="password">Contraseña</label>
|
||||||
|
<input type="password" class="form-control" id="password" th:field="*{password}" minlength="6"
|
||||||
|
th:attr="required=${user.id == null}" th:classappend="${#fields.hasErrors('password')} ? ' is-invalid'">
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Error</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label th:text="#{usuarios.form.confirmarPassword}" for="confirmPassword">Confirmar Contraseña</label>
|
||||||
|
<input type="password" class="form-control" id="confirmPassword" th:field="*{confirmPassword}" minlength="6"
|
||||||
|
th:attr="required=${user.id == null}"
|
||||||
|
th:classappend="${#fields.hasErrors('confirmPassword')} ? ' is-invalid'">
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('confirmPassword')}"
|
||||||
|
th:errors="*{confirmPassword}">Error</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label th:text="#{usuarios.form.rol}" for="rol">Rol</label>
|
||||||
|
<select class="form-control" id="rol" th:field="*{roleName}" required
|
||||||
|
th:classappend="${#fields.hasErrors('roleName')} ? ' is-invalid'">
|
||||||
|
<option value="USER" selected>Usuario</option>
|
||||||
|
<option value="ADMIN">Administrador</option>
|
||||||
|
<option value="SUPERADMIN">Super Administrador</option>
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('roleName')}" th:errors="*{roleName}">Error</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label th:text="#{usuarios.form.estado}" for="estado">Estado</label>
|
||||||
|
<select class="form-control" id="estado" th:field="*{enabled}" required
|
||||||
|
th:classappend="${#fields.hasErrors('enabled')} ? ' is-invalid'">
|
||||||
|
<option th:value="true" th:selected="${user.id == null or user.enabled}">Activo</option>
|
||||||
|
<option th:value="false" th:selected="${user.id != null and !user.enabled}">Inactivo</option>
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('enabled')}" th:errors="*{enabled}">Error</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3 justified-content-center d-flex">
|
||||||
|
<button type="submit" class="btn btn-secondary" th:text="#{usuarios.guardar}">Guardar</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@ -21,6 +21,11 @@
|
|||||||
<th:block layout:fragment="content">
|
<th:block layout:fragment="content">
|
||||||
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
|
||||||
|
<!-- Modales-->
|
||||||
|
<div
|
||||||
|
th:replace="imprimelibros/partials/modal-form :: modal('userFormModal', 'usuarios.add', 'modal-md', 'userModalBody')">
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
|
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
|
||||||
@ -30,9 +35,14 @@
|
|||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-secondary mb-3" id="addUserButton">
|
||||||
|
<i class="ri-add-line align-bottom me-1"></i> <span th:text="#{usuarios.add}">Añadir usuario</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<table id="users-datatable" class="table table-striped table-nowrap responsive w-100">
|
<table id="users-datatable" class="table table-striped table-nowrap responsive w-100">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th scope="col" th:text="#{usuarios.tabla.id}">ID</th>
|
||||||
<th scope="col" th:text="#{usuarios.tabla.nombre}">Nombre</th>
|
<th scope="col" th:text="#{usuarios.tabla.nombre}">Nombre</th>
|
||||||
<th scope="col" th:text="#{usuarios.tabla.email}">Correo electrónico</th>
|
<th scope="col" th:text="#{usuarios.tabla.email}">Correo electrónico</th>
|
||||||
<th scope="col" th:text="#{usuarios.tabla.rol}">Rol</th>
|
<th scope="col" th:text="#{usuarios.tabla.rol}">Rol</th>
|
||||||
|
|||||||
@ -3,10 +3,7 @@ package com.imprimelibros.erp;
|
|||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|||||||
Reference in New Issue
Block a user