mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-13 00:48:49 +00:00
lista de usuarios terminada
This commit is contained in:
@ -22,7 +22,7 @@ public class InternationalizationConfig implements WebMvcConfigurer {
|
||||
@Bean
|
||||
public LocaleResolver localeResolver() {
|
||||
SessionLocaleResolver slr = new SessionLocaleResolver();
|
||||
slr.setDefaultLocale(Locale.forLanguageTag("es")); // idioma por defecto
|
||||
slr.setDefaultLocale(Locale.forLanguageTag("es-ES")); // idioma por defecto
|
||||
return slr;
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,8 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
@ -22,84 +24,107 @@ import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
private final DataSource dataSource;
|
||||
private final DataSource dataSource;
|
||||
|
||||
public SecurityConfig(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
public SecurityConfig(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
// UserDetailsService para autenticación por username
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService(UserDao repo) {
|
||||
return username -> {
|
||||
User u = repo.findByUserNameAndEnabledTrue(username);
|
||||
if (u == null)
|
||||
throw new UsernameNotFoundException("No existe: " + username);
|
||||
return new UserDetailsImpl(u);
|
||||
};
|
||||
}
|
||||
// ========== Beans base ==========
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService(UserDao repo) {
|
||||
return username -> {
|
||||
User u = repo.findByUserNameAndEnabledTrue(username);
|
||||
if (u == null) throw new UsernameNotFoundException("No existe: " + username);
|
||||
return new UserDetailsImpl(u);
|
||||
};
|
||||
}
|
||||
|
||||
// Repositorio de tokens persistentes (usa la tabla 'persistent_logins')
|
||||
@Bean
|
||||
public PersistentTokenRepository persistentTokenRepository() {
|
||||
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
|
||||
repo.setDataSource(dataSource);
|
||||
// Descomenta una única vez si quieres que cree la tabla automáticamente:
|
||||
// repo.setCreateTableOnStartup(true);
|
||||
return repo;
|
||||
}
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http, @Value("${security.rememberme.key}") String keyRememberMe) throws Exception {
|
||||
http
|
||||
.sessionManagement(session -> session
|
||||
.invalidSessionUrl("/login?expired")
|
||||
.maximumSessions(1) // opcional: limita sesiones concurrentes
|
||||
)
|
||||
// CSRF habilitado; ignoramos endpoints públicos del presupuesto (AJAX)
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers("/presupuesto/public/**"))
|
||||
// Provider que soporta UsernamePasswordAuthenticationToken
|
||||
@Bean
|
||||
public AuthenticationProvider daoAuthenticationProvider(
|
||||
UserDetailsService userDetailsService,
|
||||
PasswordEncoder passwordEncoder) {
|
||||
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
"/", "/login",
|
||||
"/assets/**", "/css/**", "/js/**", "/images/**",
|
||||
"/public/**", "/presupuesto/public/**",
|
||||
"/error", "/favicon.ico")
|
||||
.permitAll()
|
||||
.anyRequest().authenticated())
|
||||
DaoAuthenticationProvider p = new DaoAuthenticationProvider();
|
||||
p.setUserDetailsService(userDetailsService);
|
||||
p.setPasswordEncoder(passwordEncoder);
|
||||
return p;
|
||||
}
|
||||
|
||||
.authorizeHttpRequests(configurer -> configurer
|
||||
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
||||
)
|
||||
.formLogin(login -> login
|
||||
.loginPage("/login").permitAll()
|
||||
.loginProcessingUrl("/login")
|
||||
.usernameParameter("username")
|
||||
.passwordParameter("password")
|
||||
.defaultSuccessUrl("/", true))
|
||||
// Remember-me (tabla persistent_logins)
|
||||
@Bean
|
||||
public PersistentTokenRepository persistentTokenRepository() {
|
||||
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
|
||||
repo.setDataSource(dataSource);
|
||||
// repo.setCreateTableOnStartup(true); // solo 1ª vez si necesitas crear la tabla
|
||||
return repo;
|
||||
}
|
||||
|
||||
// ===== Remember Me =====
|
||||
.rememberMe(rm -> rm
|
||||
.key(keyRememberMe) // clave secreta
|
||||
.rememberMeParameter("remember-me") // <input name="remember-me">
|
||||
.rememberMeCookieName("IMPRIMELIBROS_REMEMBER")
|
||||
.tokenValiditySeconds(60 * 60 * 24 * 14) // 14 días
|
||||
.userDetailsService(userDetailsService(null)) // se inyecta el bean real
|
||||
// en runtime
|
||||
.tokenRepository(persistentTokenRepository()))
|
||||
// ========== Filtro de seguridad ==========
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(
|
||||
HttpSecurity http,
|
||||
@Value("${security.rememberme.key}") String keyRememberMe,
|
||||
UserDetailsService userDetailsService,
|
||||
PersistentTokenRepository tokenRepo,
|
||||
AuthenticationProvider daoAuthenticationProvider
|
||||
) throws Exception {
|
||||
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/logout")
|
||||
.logoutSuccessUrl("/")
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID", "IMPRIMELIBROS_REMEMBER")
|
||||
.permitAll());
|
||||
http
|
||||
// Registra explícitamente el provider para Username/Password
|
||||
.authenticationProvider(daoAuthenticationProvider)
|
||||
|
||||
return http.build();
|
||||
}
|
||||
.sessionManagement(session -> session
|
||||
.invalidSessionUrl("/login?expired")
|
||||
.maximumSessions(1)
|
||||
)
|
||||
|
||||
.csrf(csrf -> csrf
|
||||
.ignoringRequestMatchers("/presupuesto/public/**")
|
||||
)
|
||||
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/", "/login",
|
||||
"/assets/**", "/css/**", "/js/**", "/images/**",
|
||||
"/public/**", "/presupuesto/public/**",
|
||||
"/error", "/favicon.ico").permitAll()
|
||||
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
.formLogin(login -> login
|
||||
.loginPage("/login").permitAll()
|
||||
.loginProcessingUrl("/login")
|
||||
.usernameParameter("username")
|
||||
.passwordParameter("password")
|
||||
.defaultSuccessUrl("/", true)
|
||||
.failureUrl("/login?error") // útil para diagnosticar
|
||||
)
|
||||
|
||||
.rememberMe(rm -> rm
|
||||
.key(keyRememberMe)
|
||||
.rememberMeParameter("remember-me")
|
||||
.rememberMeCookieName("IMPRIMELIBROS_REMEMBER")
|
||||
.tokenValiditySeconds(60 * 60 * 24 * 2)
|
||||
.userDetailsService(userDetailsService)
|
||||
.tokenRepository(tokenRepo)
|
||||
)
|
||||
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/logout")
|
||||
.logoutSuccessUrl("/")
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID", "IMPRIMELIBROS_REMEMBER")
|
||||
.permitAll()
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,68 +12,117 @@ import java.util.function.Function;
|
||||
|
||||
public class DataTable<T> {
|
||||
|
||||
public interface FilterHook<T> extends BiConsumer<SpecBuilder<T>, DataTablesRequest> {}
|
||||
public interface SpecBuilder<T> { void add(Specification<T> extra); }
|
||||
public interface FilterHook<T> extends BiConsumer<SpecBuilder<T>, DataTablesRequest> {
|
||||
}
|
||||
|
||||
public interface SpecBuilder<T> {
|
||||
void add(Specification<T> extra);
|
||||
}
|
||||
|
||||
private final JpaSpecificationExecutor<T> repo;
|
||||
private final Class<T> entityClass;
|
||||
private final DataTablesRequest dt;
|
||||
private final List<String> searchable;
|
||||
private final List<Function<T, Map<String,Object>>> adders = new ArrayList<>();
|
||||
private final List<Function<Map<String,Object>, Map<String,Object>>> editors = new ArrayList<>();
|
||||
private final List<Function<T, Map<String, Object>>> adders = new ArrayList<>();
|
||||
private final List<Function<Map<String, Object>, Map<String, Object>>> editors = 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 DataTable(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt, List<String> searchable) {
|
||||
this.repo = repo; this.entityClass = entityClass; this.dt = dt; this.searchable = searchable;
|
||||
private DataTable(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
|
||||
List<String> searchable) {
|
||||
this.repo = repo;
|
||||
this.entityClass = entityClass;
|
||||
this.dt = dt;
|
||||
this.searchable = searchable;
|
||||
}
|
||||
|
||||
public static <T> DataTable<T> of(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt, List<String> searchable) {
|
||||
public static <T> DataTable<T> of(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
|
||||
List<String> searchable) {
|
||||
return new DataTable<>(repo, entityClass, dt, searchable);
|
||||
}
|
||||
|
||||
/** Equivalente a tu $q->where(...): establece condición base */
|
||||
public DataTable<T> where(Specification<T> spec) { this.baseSpec = this.baseSpec.and(spec); return this; }
|
||||
public DataTable<T> where(Specification<T> spec) {
|
||||
this.baseSpec = this.baseSpec.and(spec);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** add("campo", fn(entity)->valor|Map) */
|
||||
public DataTable<T> add(String field, Function<T, Object> fn) {
|
||||
adders.add(entity -> {
|
||||
Map<String,Object> m = new HashMap<>();
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put(field, fn.apply(entity));
|
||||
return m;
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/** add(fn(entity)->Map<String,Object>) para devolver objetos anidados como tu "logo" */
|
||||
public DataTable<T> add(Function<T, Map<String,Object>> fn) { adders.add(fn); return this; }
|
||||
/**
|
||||
* add(fn(entity)->Map<String,Object>) para devolver objetos anidados como tu
|
||||
* "logo"
|
||||
*/
|
||||
public DataTable<T> add(Function<T, Map<String, Object>> fn) {
|
||||
adders.add(fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** edit("campo", fn(entity)->valor) sobreescribe un campo existente o lo crea si no existe */
|
||||
/**
|
||||
* edit("campo", fn(entity)->valor) sobreescribe un campo existente o lo crea si
|
||||
* no existe
|
||||
*/
|
||||
public DataTable<T> edit(String field, Function<T, Object> fn) {
|
||||
editors.add(row -> { row.put(field, fn.apply((T)row.get("__entity"))); return row; });
|
||||
editors.add(row -> {
|
||||
row.put(field, fn.apply((T) row.get("__entity")));
|
||||
return row;
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/** filter((builder, req) -> builder.add(miExtraSpec(req))) */
|
||||
public DataTable<T> filter(FilterHook<T> hook) { filters.add(hook); return this; }
|
||||
public DataTable<T> filter(FilterHook<T> hook) {
|
||||
filters.add(hook);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DataTablesResponse<Map<String,Object>> toJson(long totalCount) {
|
||||
public DataTablesResponse<Map<String, Object>> toJson(long totalCount) {
|
||||
// Construye spec con búsqueda global + base + filtros custom
|
||||
Specification<T> spec = baseSpec.and(DataTablesSpecification.build(dt, searchable));
|
||||
final Specification<T>[] holder = new Specification[]{ spec };
|
||||
final Specification<T>[] holder = new Specification[] { spec };
|
||||
filters.forEach(h -> h.accept(extra -> holder[0] = holder[0].and(extra), dt));
|
||||
spec = holder[0];
|
||||
|
||||
// Sort
|
||||
// Sort
|
||||
Sort sort = Sort.unsorted();
|
||||
if (!dt.order.isEmpty() && !dt.columns.isEmpty()) {
|
||||
List<Sort.Order> orders = new ArrayList<>();
|
||||
for (var o : dt.order) {
|
||||
String field = dt.columns.get(o.column).name;
|
||||
orders.add(new Sort.Order("desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC, field));
|
||||
var col = dt.columns.get(o.column);
|
||||
String field = col != null ? col.name : null;
|
||||
|
||||
if (field == null || field.isBlank())
|
||||
continue;
|
||||
if (!col.orderable)
|
||||
continue;
|
||||
if (!searchable.contains(field))
|
||||
continue; // << usa tu whitelist
|
||||
|
||||
orders.add(new Sort.Order(
|
||||
"desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC,
|
||||
field));
|
||||
}
|
||||
if (!orders.isEmpty()) {
|
||||
sort = Sort.by(orders);
|
||||
} else {
|
||||
for (var c : dt.columns) {
|
||||
if (c != null && c.orderable && c.name != null && !c.name.isBlank()
|
||||
&& searchable.contains(c.name)) {
|
||||
sort = Sort.by(c.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sort = Sort.by(orders);
|
||||
}
|
||||
|
||||
// Page
|
||||
@ -84,12 +133,14 @@ public class DataTable<T> {
|
||||
long filtered = p.getTotalElements();
|
||||
|
||||
// Mapear entidad -> Map base (via Jackson) + add/edit
|
||||
List<Map<String,Object>> data = new ArrayList<>();
|
||||
List<Map<String, Object>> data = new ArrayList<>();
|
||||
for (T e : p.getContent()) {
|
||||
Map<String,Object> row = om.convertValue(e, Map.class);
|
||||
Map<String, Object> row = om.convertValue(e, Map.class);
|
||||
row.put("__entity", e); // para editores que necesiten la entidad
|
||||
for (var ad : adders) row.putAll(ad.apply(e));
|
||||
for (var ed : editors) ed.apply(row);
|
||||
for (var ad : adders)
|
||||
row.putAll(ad.apply(e));
|
||||
for (var ed : editors)
|
||||
ed.apply(row);
|
||||
row.remove("__entity");
|
||||
data.add(row);
|
||||
}
|
||||
|
||||
@ -12,10 +12,9 @@ public class DataTablesService {
|
||||
public static <T> DataTablesResponse<T> handle(
|
||||
DataTablesRequest dt,
|
||||
JpaSpecificationExecutor<T> repo,
|
||||
long totalCount, // count sin filtros (cacheable)
|
||||
long totalCount, // count sin filtros (cacheable)
|
||||
List<String> searchableFields,
|
||||
Class<T> entityClass
|
||||
) {
|
||||
Class<T> entityClass) {
|
||||
// Spec (filtros)
|
||||
Specification<T> spec = DataTablesSpecification.build(dt, searchableFields);
|
||||
|
||||
@ -23,11 +22,37 @@ public class DataTablesService {
|
||||
Sort sort = Sort.unsorted();
|
||||
if (!dt.order.isEmpty() && !dt.columns.isEmpty()) {
|
||||
List<Sort.Order> orders = new ArrayList<>();
|
||||
for (DataTablesRequest.Order o : dt.order) {
|
||||
String field = dt.columns.get(o.column).name;
|
||||
orders.add(new Sort.Order("desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC, field));
|
||||
for (var o : dt.order) {
|
||||
var col = dt.columns.get(o.column);
|
||||
String field = col != null ? col.name : null;
|
||||
|
||||
// Acepta solo columnas válidas: no vacías, marcadas como orderable y en la
|
||||
// whitelist "searchable"
|
||||
if (field == null || field.isBlank())
|
||||
continue;
|
||||
if (!col.orderable)
|
||||
continue;
|
||||
if (!searchableFields.contains(field))
|
||||
continue;
|
||||
|
||||
orders.add(new Sort.Order(
|
||||
"desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC,
|
||||
field));
|
||||
}
|
||||
if (!orders.isEmpty()) {
|
||||
sort = Sort.by(orders);
|
||||
} else {
|
||||
// Fallback: primera columna de dt.columns que sea orderable y esté en la
|
||||
// whitelist
|
||||
for (var c : dt.columns) {
|
||||
if (c != null && c.orderable && c.name != null && !c.name.isBlank()
|
||||
&& searchableFields.contains(c.name)) {
|
||||
sort = Sort.by(c.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Si no hay ninguna válida, sort se queda UNSORTED
|
||||
}
|
||||
sort = Sort.by(orders);
|
||||
}
|
||||
|
||||
// Page
|
||||
@ -42,7 +67,6 @@ public class DataTablesService {
|
||||
dt.draw,
|
||||
totalCount,
|
||||
filtered,
|
||||
result.getContent()
|
||||
);
|
||||
result.getContent());
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,30 +8,42 @@ import java.util.List;
|
||||
public class DataTablesSpecification {
|
||||
|
||||
/**
|
||||
* Crea una Specification con búsqueda global y por columna (LIKE case-insensitive)
|
||||
* @param dt request de datatables
|
||||
* Crea una Specification con búsqueda global y por columna (LIKE
|
||||
* case-insensitive)
|
||||
*
|
||||
* @param dt request de datatables
|
||||
* @param searchableFields campos del entity para el buscador global
|
||||
*/
|
||||
public static <T> Specification<T> build(DataTablesRequest dt, List<String> searchableFields) {
|
||||
return (root, query, cb) -> {
|
||||
List<Predicate> ands = new ArrayList<>();
|
||||
|
||||
// Filtro por columna (si quieres soportarlo)
|
||||
// Filtro por columna (si lo usas en el cliente)
|
||||
for (int i = 0; i < dt.columns.size(); i++) {
|
||||
DataTablesRequest.Column col = dt.columns.get(i);
|
||||
if (col.searchable && col.search != null && col.search.value != null && !col.search.value.isEmpty()) {
|
||||
ands.add(like(cb, root.get(col.name), col.search.value));
|
||||
try {
|
||||
ands.add(like(cb, root.get(col.name), col.search.value));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// columna no mapeada o relación: la ignoramos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Búsqueda global
|
||||
if (dt.search != null && dt.search.value != null && !dt.search.value.isEmpty() && !searchableFields.isEmpty()) {
|
||||
if (dt.search != null && dt.search.value != null && !dt.search.value.isEmpty()
|
||||
&& !searchableFields.isEmpty()) {
|
||||
String term = "%" + dt.search.value.trim().toLowerCase() + "%";
|
||||
List<Predicate> ors = new ArrayList<>();
|
||||
for (String f : searchableFields) {
|
||||
ors.add(cb.like(cb.lower(root.get(f).as(String.class)), term));
|
||||
try {
|
||||
ors.add(cb.like(cb.lower(root.get(f).as(String.class)), term));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// campo no simple: lo saltamos
|
||||
}
|
||||
}
|
||||
ands.add(cb.or(ors.toArray(new Predicate[0])));
|
||||
if (!ors.isEmpty())
|
||||
ands.add(cb.or(ors.toArray(new Predicate[0])));
|
||||
}
|
||||
|
||||
return ands.isEmpty() ? cb.conjunction() : cb.and(ands.toArray(new Predicate[0]));
|
||||
|
||||
@ -4,43 +4,79 @@ import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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 com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.datatables.DataTable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@Controller
|
||||
@PreAuthorize("hasRole('ADMIN') or hasRole('SUPERADMIN')")
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
public class UserController {
|
||||
|
||||
private UserDao repo;
|
||||
private MessageSource messageSource;
|
||||
|
||||
public UserController(UserDao repo, UserService userService) {
|
||||
public UserController(UserDao repo, UserService userService, MessageSource messageSource) {
|
||||
this.repo = repo;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN') or hasRole('SUPERADMIN')")
|
||||
@GetMapping("/")
|
||||
public DataTablesResponse<Map<String,Object>> datatable(HttpServletRequest request) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
@GetMapping
|
||||
public String list(Model model, Authentication authentication, Locale locale) {
|
||||
|
||||
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, 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> whitelist = List.of("fullName", "userName", "enabled");
|
||||
|
||||
Specification<User> base = (root, query, cb) -> cb.conjunction();
|
||||
long total = repo.count();
|
||||
|
||||
return DataTable
|
||||
.of(repo, User.class, dt, List.of(
|
||||
"username", "email", "role" // campos buscables
|
||||
))
|
||||
.where(base)
|
||||
.toJson(total);
|
||||
.of(repo, User.class, dt, whitelist) // 'searchable' en DataTable.java
|
||||
.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>";
|
||||
}
|
||||
})
|
||||
// si 'role' es relación, crea una columna calculada “segura”:
|
||||
// acciones virtuales:
|
||||
.add("roles", (User u) -> u.getRoles().stream().map(Role::getName).collect(Collectors.joining(", ")))
|
||||
.add("actions", (user) -> {
|
||||
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() + "\" class=\"link-danger fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>\n" +
|
||||
" </div>";
|
||||
})
|
||||
.where(base)
|
||||
.toJson(total);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user