From 01a1ac4b7155f6b65d1b8d111f064514978626ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Fri, 26 Sep 2025 15:13:11 +0200 Subject: [PATCH] trabajando en usuarios --- pom.xml | 6 +- .../erp/config/SecurityConfig.java | 117 ++++++++++++++---- .../erp/datatables/DataTable.java | 98 +++++++++++++++ .../erp/datatables/DataTablesParser.java | 43 +++++++ .../erp/datatables/DataTablesRequest.java | 23 ++++ .../erp/datatables/DataTablesResponse.java | 17 +++ .../erp/datatables/DataTablesService.java | 48 +++++++ .../datatables/DataTablesSpecification.java | 44 +++++++ .../erp/home/HomeController.java | 5 + .../erp/login/LoginController.java | 3 + .../com/imprimelibros/erp/users/Role.java | 49 ++++++++ .../com/imprimelibros/erp/users/RoleDao.java | 9 ++ .../imprimelibros/erp/users/RoleDaoImpl.java | 33 +++++ .../com/imprimelibros/erp/users/User.java | 116 +++++++++++++++++ .../erp/users/UserController.java | 46 +++++++ .../com/imprimelibros/erp/users/UserDao.java | 12 ++ .../erp/users/UserDetailsImpl.java | 89 +++++++++++++ .../imprimelibros/erp/users/UserService.java | 8 ++ .../erp/users/UserServiceImpl.java | 44 +++++++ src/main/resources/application.properties | 7 ++ src/main/resources/i18n/app_es.properties | 11 +- src/main/resources/i18n/login_es.properties | 15 ++- src/main/resources/static/assets/css/app.css | 22 ++-- .../templates/imprimelibros/layout.html | 2 +- .../login/_items/_forgot-pass.html | 0 .../imprimelibros/login/_items/_login.html | 59 +++++++++ .../imprimelibros/login/_items/_signup.html | 0 .../templates/imprimelibros/login/login.html | 76 ++---------- .../imprimelibros/partials/sidebar.html | 23 +++- .../imprimelibros/partials/topbar.html | 51 +++----- 30 files changed, 937 insertions(+), 139 deletions(-) create mode 100644 src/main/java/com/imprimelibros/erp/datatables/DataTable.java create mode 100644 src/main/java/com/imprimelibros/erp/datatables/DataTablesParser.java create mode 100644 src/main/java/com/imprimelibros/erp/datatables/DataTablesRequest.java create mode 100644 src/main/java/com/imprimelibros/erp/datatables/DataTablesResponse.java create mode 100644 src/main/java/com/imprimelibros/erp/datatables/DataTablesService.java create mode 100644 src/main/java/com/imprimelibros/erp/datatables/DataTablesSpecification.java create mode 100644 src/main/java/com/imprimelibros/erp/users/Role.java create mode 100644 src/main/java/com/imprimelibros/erp/users/RoleDao.java create mode 100644 src/main/java/com/imprimelibros/erp/users/RoleDaoImpl.java create mode 100644 src/main/java/com/imprimelibros/erp/users/User.java create mode 100644 src/main/java/com/imprimelibros/erp/users/UserController.java create mode 100644 src/main/java/com/imprimelibros/erp/users/UserDao.java create mode 100644 src/main/java/com/imprimelibros/erp/users/UserDetailsImpl.java create mode 100644 src/main/java/com/imprimelibros/erp/users/UserService.java create mode 100644 src/main/java/com/imprimelibros/erp/users/UserServiceImpl.java create mode 100644 src/main/resources/templates/imprimelibros/login/_items/_forgot-pass.html create mode 100644 src/main/resources/templates/imprimelibros/login/_items/_login.html create mode 100644 src/main/resources/templates/imprimelibros/login/_items/_signup.html diff --git a/pom.xml b/pom.xml index a6602d9..33df75e 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,10 @@ nz.net.ultraq.thymeleaf thymeleaf-layout-dialect + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + org.springframework.boot spring-boot-starter-web @@ -85,7 +89,7 @@ spring-boot-starter-test test - + org.springframework.security spring-security-test diff --git a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java index cbb0be0..8558778 100644 --- a/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java +++ b/src/main/java/com/imprimelibros/erp/config/SecurityConfig.java @@ -1,36 +1,105 @@ package com.imprimelibros.erp.config; +import javax.sql.DataSource; + +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.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; +import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; + +import com.imprimelibros.erp.users.User; +import com.imprimelibros.erp.users.UserDao; +import com.imprimelibros.erp.users.UserDetailsImpl; @Configuration public class SecurityConfig { - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests(auth -> auth - .requestMatchers( - "/", - "/assets/**", - "/css/**", - "/js/**", - "/images/**", - "/public/**", - "/error", - "/presupuesto/public/**", - "/favicon.ico") - .permitAll() - .anyRequest().authenticated()) - .csrf(csrf -> csrf - .ignoringRequestMatchers("/presupuesto/public/**")) - .formLogin(login -> login - .loginPage("/login") - .permitAll()) - .logout(logout -> logout.permitAll()); + private final DataSource dataSource; - return http.build(); - } + 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); + }; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + // 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 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/**")) + + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/", "/login", + "/assets/**", "/css/**", "/js/**", "/images/**", + "/public/**", "/presupuesto/public/**", + "/error", "/favicon.ico") + .permitAll() + .anyRequest().authenticated()) + + .authorizeHttpRequests(configurer -> configurer + .requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN") + ) + .formLogin(login -> login + .loginPage("/login").permitAll() + .loginProcessingUrl("/login") + .usernameParameter("username") + .passwordParameter("password") + .defaultSuccessUrl("/", true)) + + // ===== Remember Me ===== + .rememberMe(rm -> rm + .key(keyRememberMe) // clave secreta + .rememberMeParameter("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())) + + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/") + .invalidateHttpSession(true) + .deleteCookies("JSESSIONID", "IMPRIMELIBROS_REMEMBER") + .permitAll()); + + return http.build(); + } } diff --git a/src/main/java/com/imprimelibros/erp/datatables/DataTable.java b/src/main/java/com/imprimelibros/erp/datatables/DataTable.java new file mode 100644 index 0000000..27685bf --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/datatables/DataTable.java @@ -0,0 +1,98 @@ +package com.imprimelibros.erp.datatables; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.data.domain.*; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class DataTable { + + public interface FilterHook extends BiConsumer, DataTablesRequest> {} + public interface SpecBuilder { void add(Specification extra); } + + private final JpaSpecificationExecutor repo; + private final Class entityClass; + private final DataTablesRequest dt; + private final List searchable; + private final List>> adders = new ArrayList<>(); + private final List, Map>> editors = new ArrayList<>(); + private final List> filters = new ArrayList<>(); + private Specification baseSpec = (root,q,cb) -> cb.conjunction(); + private final ObjectMapper om = new ObjectMapper(); + + private DataTable(JpaSpecificationExecutor repo, Class entityClass, DataTablesRequest dt, List searchable) { + this.repo = repo; this.entityClass = entityClass; this.dt = dt; this.searchable = searchable; + } + + public static DataTable of(JpaSpecificationExecutor repo, Class entityClass, DataTablesRequest dt, List searchable) { + return new DataTable<>(repo, entityClass, dt, searchable); + } + + /** Equivalente a tu $q->where(...): establece condición base */ + public DataTable where(Specification spec) { this.baseSpec = this.baseSpec.and(spec); return this; } + + /** add("campo", fn(entity)->valor|Map) */ + public DataTable add(String field, Function fn) { + adders.add(entity -> { + Map m = new HashMap<>(); + m.put(field, fn.apply(entity)); + return m; + }); + return this; + } + + /** add(fn(entity)->Map) para devolver objetos anidados como tu "logo" */ + public DataTable add(Function> fn) { adders.add(fn); return this; } + + /** edit("campo", fn(entity)->valor) sobreescribe un campo existente o lo crea si no existe */ + public DataTable edit(String field, Function fn) { + 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 filter(FilterHook hook) { filters.add(hook); return this; } + + public DataTablesResponse> toJson(long totalCount) { + // Construye spec con búsqueda global + base + filtros custom + Specification spec = baseSpec.and(DataTablesSpecification.build(dt, searchable)); + final Specification[] holder = new Specification[]{ spec }; + filters.forEach(h -> h.accept(extra -> holder[0] = holder[0].and(extra), dt)); + spec = holder[0]; + + // Sort + Sort sort = Sort.unsorted(); + if (!dt.order.isEmpty() && !dt.columns.isEmpty()) { + List 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)); + } + sort = Sort.by(orders); + } + + // Page + int page = dt.length > 0 ? dt.start / dt.length : 0; + Pageable pageable = dt.length > 0 ? PageRequest.of(page, dt.length, sort) : Pageable.unpaged(); + + var p = repo.findAll(holder[0], pageable); + long filtered = p.getTotalElements(); + + // Mapear entidad -> Map base (via Jackson) + add/edit + List> data = new ArrayList<>(); + for (T e : p.getContent()) { + Map 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); + row.remove("__entity"); + data.add(row); + } + return new DataTablesResponse<>(dt.draw, totalCount, filtered, data); + } +} diff --git a/src/main/java/com/imprimelibros/erp/datatables/DataTablesParser.java b/src/main/java/com/imprimelibros/erp/datatables/DataTablesParser.java new file mode 100644 index 0000000..ba87f28 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/datatables/DataTablesParser.java @@ -0,0 +1,43 @@ +package com.imprimelibros.erp.datatables; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.*; + +public class DataTablesParser { + public static DataTablesRequest from(HttpServletRequest req) { + DataTablesRequest dt = new DataTablesRequest(); + dt.draw = parseInt(req.getParameter("draw"), 0); + dt.start = parseInt(req.getParameter("start"), 0); + dt.length = parseInt(req.getParameter("length"), 10); + + if (req.getParameter("search[value]") != null) { + dt.search.value = req.getParameter("search[value]"); + dt.search.regex = Boolean.parseBoolean(req.getParameter("search[regex]")); + } + + for (int i=0;; i++) { + String data = req.getParameter("columns["+i+"][data]"); + if (data == null) break; + DataTablesRequest.Column c = new DataTablesRequest.Column(); + c.data = data; + c.name = Optional.ofNullable(req.getParameter("columns["+i+"][name]")).orElse(data); + c.searchable = Boolean.parseBoolean(Optional.ofNullable(req.getParameter("columns["+i+"][searchable]")).orElse("true")); + c.orderable = Boolean.parseBoolean(Optional.ofNullable(req.getParameter("columns["+i+"][orderable]")).orElse("true")); + c.search.value = Optional.ofNullable(req.getParameter("columns["+i+"][search][value]")).orElse(""); + dt.columns.add(c); + } + for (int i=0;; i++) { + String colIdx = req.getParameter("order["+i+"][column]"); + if (colIdx == null) break; + DataTablesRequest.Order o = new DataTablesRequest.Order(); + o.column = parseInt(colIdx,0); + o.dir = Optional.ofNullable(req.getParameter("order["+i+"][dir]")).orElse("asc"); + dt.order.add(o); + } + + // guarda TODOS los params crudos (para filtros custom) + req.getParameterMap().forEach((k,v) -> dt.raw.put(k, v!=null && v.length>0 ? v[0] : null)); + return dt; + } + private static int parseInt(String s, int def){ try{return Integer.parseInt(s);}catch(Exception e){return def;}} +} \ No newline at end of file diff --git a/src/main/java/com/imprimelibros/erp/datatables/DataTablesRequest.java b/src/main/java/com/imprimelibros/erp/datatables/DataTablesRequest.java new file mode 100644 index 0000000..3ce3800 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/datatables/DataTablesRequest.java @@ -0,0 +1,23 @@ +package com.imprimelibros.erp.datatables; + +import java.util.*; + +public class DataTablesRequest { + public int draw; + public int start; + public int length; + public Search search = new Search(); + public List order = new ArrayList<>(); + public List columns = new ArrayList<>(); + public Map raw = new HashMap<>(); // <- params extra + + public static class Search { public String value=""; public boolean regex; } + public static class Order { public int column; public String dir; } + public static class Column { + public String data; + public String name; + public boolean searchable=true; + public boolean orderable=true; + public Search search=new Search(); + } +} \ No newline at end of file diff --git a/src/main/java/com/imprimelibros/erp/datatables/DataTablesResponse.java b/src/main/java/com/imprimelibros/erp/datatables/DataTablesResponse.java new file mode 100644 index 0000000..78442e6 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/datatables/DataTablesResponse.java @@ -0,0 +1,17 @@ +package com.imprimelibros.erp.datatables; + +import java.util.List; + +public class DataTablesResponse { + public int draw; + public long recordsTotal; + public long recordsFiltered; + public List data; + + public DataTablesResponse(int draw, long total, long filtered, List data) { + this.draw = draw; + this.recordsTotal = total; + this.recordsFiltered = filtered; + this.data = data; + } +} diff --git a/src/main/java/com/imprimelibros/erp/datatables/DataTablesService.java b/src/main/java/com/imprimelibros/erp/datatables/DataTablesService.java new file mode 100644 index 0000000..dfa5e69 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/datatables/DataTablesService.java @@ -0,0 +1,48 @@ +package com.imprimelibros.erp.datatables; + +import org.springframework.data.domain.*; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.ArrayList; +import java.util.List; + +public class DataTablesService { + + public static DataTablesResponse handle( + DataTablesRequest dt, + JpaSpecificationExecutor repo, + long totalCount, // count sin filtros (cacheable) + List searchableFields, + Class entityClass + ) { + // Spec (filtros) + Specification spec = DataTablesSpecification.build(dt, searchableFields); + + // Sort + Sort sort = Sort.unsorted(); + if (!dt.order.isEmpty() && !dt.columns.isEmpty()) { + List 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)); + } + sort = Sort.by(orders); + } + + // Page + int page = dt.length > 0 ? dt.start / dt.length : 0; + Pageable pageable = dt.length > 0 ? PageRequest.of(page, dt.length, sort) : Pageable.unpaged(); + + // Query + Page result = repo.findAll(spec, pageable); + long filtered = result.getTotalElements(); + + return new DataTablesResponse<>( + dt.draw, + totalCount, + filtered, + result.getContent() + ); + } +} diff --git a/src/main/java/com/imprimelibros/erp/datatables/DataTablesSpecification.java b/src/main/java/com/imprimelibros/erp/datatables/DataTablesSpecification.java new file mode 100644 index 0000000..2b582be --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/datatables/DataTablesSpecification.java @@ -0,0 +1,44 @@ +package com.imprimelibros.erp.datatables; + +import org.springframework.data.jpa.domain.Specification; +import jakarta.persistence.criteria.*; +import java.util.ArrayList; +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 + * @param searchableFields campos del entity para el buscador global + */ + public static Specification build(DataTablesRequest dt, List searchableFields) { + return (root, query, cb) -> { + List ands = new ArrayList<>(); + + // Filtro por columna (si quieres soportarlo) + 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)); + } + } + + // Búsqueda global + if (dt.search != null && dt.search.value != null && !dt.search.value.isEmpty() && !searchableFields.isEmpty()) { + String term = "%" + dt.search.value.trim().toLowerCase() + "%"; + List ors = new ArrayList<>(); + for (String f : searchableFields) { + ors.add(cb.like(cb.lower(root.get(f).as(String.class)), term)); + } + ands.add(cb.or(ors.toArray(new Predicate[0]))); + } + + return ands.isEmpty() ? cb.conjunction() : cb.and(ands.toArray(new Predicate[0])); + }; + } + + private static Predicate like(CriteriaBuilder cb, Path path, String value) { + return cb.like(cb.lower(path.as(String.class)), "%" + value.trim().toLowerCase() + "%"); + } +} diff --git a/src/main/java/com/imprimelibros/erp/home/HomeController.java b/src/main/java/com/imprimelibros/erp/home/HomeController.java index 9ba71aa..a42041c 100644 --- a/src/main/java/com/imprimelibros/erp/home/HomeController.java +++ b/src/main/java/com/imprimelibros/erp/home/HomeController.java @@ -43,6 +43,11 @@ public class HomeController { model.addAttribute("ancho_alto_min", variableService.getValorEntero("ancho_alto_min")); model.addAttribute("ancho_alto_max", variableService.getValorEntero("ancho_alto_max")); } + else{ + // empty translations for authenticated users + Map translations = Map.of(); + model.addAttribute("languageBundle", translations); + } return "imprimelibros/home"; } } diff --git a/src/main/java/com/imprimelibros/erp/login/LoginController.java b/src/main/java/com/imprimelibros/erp/login/LoginController.java index 538dabe..9a7d960 100644 --- a/src/main/java/com/imprimelibros/erp/login/LoginController.java +++ b/src/main/java/com/imprimelibros/erp/login/LoginController.java @@ -11,6 +11,9 @@ public class LoginController { @GetMapping("/login") public String index(Model model, Locale locale) { + model.addAttribute("form", "_login"); return "imprimelibros/login/login"; } + + } diff --git a/src/main/java/com/imprimelibros/erp/users/Role.java b/src/main/java/com/imprimelibros/erp/users/Role.java new file mode 100644 index 0000000..5ae2cf2 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/Role.java @@ -0,0 +1,49 @@ +package com.imprimelibros.erp.users; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "roles") +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "name") + private String name; + + public Role() { + } + + public Role(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Role{" + "id=" + id + ", name='" + name + '\'' + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/imprimelibros/erp/users/RoleDao.java b/src/main/java/com/imprimelibros/erp/users/RoleDao.java new file mode 100644 index 0000000..6208d8a --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/RoleDao.java @@ -0,0 +1,9 @@ +package com.imprimelibros.erp.users; + +import com.imprimelibros.erp.users.Role; + +public interface RoleDao { + + public Role findRoleByName(String theRoleName); + +} diff --git a/src/main/java/com/imprimelibros/erp/users/RoleDaoImpl.java b/src/main/java/com/imprimelibros/erp/users/RoleDaoImpl.java new file mode 100644 index 0000000..2c808b9 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/RoleDaoImpl.java @@ -0,0 +1,33 @@ +package com.imprimelibros.erp.users; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import org.springframework.stereotype.Repository; + +@Repository +public class RoleDaoImpl implements RoleDao { + + private EntityManager entityManager; + + public RoleDaoImpl(EntityManager theEntityManager) { + entityManager = theEntityManager; + } + + @Override + public Role findRoleByName(String theRoleName) { + + // retrieve/read from database using name + TypedQuery theQuery = entityManager.createQuery("from Role where name=:roleName", Role.class); + theQuery.setParameter("roleName", theRoleName); + + Role theRole = null; + + try { + theRole = theQuery.getSingleResult(); + } catch (Exception e) { + theRole = null; + } + + return theRole; + } +} diff --git a/src/main/java/com/imprimelibros/erp/users/User.java b/src/main/java/com/imprimelibros/erp/users/User.java new file mode 100644 index 0000000..2bb794b --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/User.java @@ -0,0 +1,116 @@ +package com.imprimelibros.erp.users; + +import jakarta.persistence.*; +import java.util.Collection; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "fullname") + private String fullName; + + @Column(name = "username") + private String userName; + + @Column(name = "password") + private String password; + + @Column(name = "enabled") + private boolean enabled; + + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @JoinTable(name = "users_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id")) + private Collection roles; + + + /* Constructors */ + public User() { + } + + public User(String fullName, String userName, String password, boolean enabled) { + this.fullName = fullName; + this.userName = userName; + this.password = password; + this.enabled = enabled; + } + + public User(String fullName, String userName, String password, boolean enabled, + Collection roles) { + this.fullName = fullName; + this.userName = userName; + this.password = password; + this.enabled = enabled; + this.roles = roles; + } + + /* Getters and 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 getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Collection getRoles() { + return roles; + } + + public void setRoles(Collection roles) { + this.roles = roles; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", fullName='" + fullName + '\'' + + ", userName='" + userName + '\'' + + ", password='" + password + '\'' + + ", enabled=" + enabled + + ", roles=" + roles + + '}'; + } + +} diff --git a/src/main/java/com/imprimelibros/erp/users/UserController.java b/src/main/java/com/imprimelibros/erp/users/UserController.java new file mode 100644 index 0000000..1beeeb4 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/UserController.java @@ -0,0 +1,46 @@ +package com.imprimelibros.erp.users; + +import com.imprimelibros.erp.datatables.DataTablesResponse; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.security.access.prepost.PreAuthorize; + +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.List; + +@Controller +@RequestMapping("/users") +public class UserController { + + private UserDao repo; + + public UserController(UserDao repo, UserService userService) { + this.repo = repo; + } + + @PreAuthorize("hasRole('ADMIN') or hasRole('SUPERADMIN')") + @GetMapping("/") + public DataTablesResponse> datatable(HttpServletRequest request) { + + DataTablesRequest dt = DataTablesParser.from(request); + + Specification 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); + } +} diff --git a/src/main/java/com/imprimelibros/erp/users/UserDao.java b/src/main/java/com/imprimelibros/erp/users/UserDao.java new file mode 100644 index 0000000..5ab6648 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/UserDao.java @@ -0,0 +1,12 @@ +package com.imprimelibros.erp.users; + +import org.springframework.stereotype.Repository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +@Repository +public interface UserDao extends JpaRepository, JpaSpecificationExecutor { + + + User findByUserNameAndEnabledTrue(String userName); +} diff --git a/src/main/java/com/imprimelibros/erp/users/UserDetailsImpl.java b/src/main/java/com/imprimelibros/erp/users/UserDetailsImpl.java new file mode 100644 index 0000000..8708373 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/UserDetailsImpl.java @@ -0,0 +1,89 @@ +package com.imprimelibros.erp.users; + +import com.imprimelibros.erp.users.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Adaptador de la entidad User a Spring Security. + */ +public class UserDetailsImpl implements UserDetails { + + private final User user; + + public UserDetailsImpl(User user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + // Si tu User tiene un Set: + Set roles = user.getRoles().stream() + .map(r -> r.getName()) // ejemplo: "ADMIN", "USER" + .collect(Collectors.toSet()); + + return roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + .collect(Collectors.toSet()); + } + + @Override + public String getPassword() { + return user.getPassword(); // debe estar encriptado (BCrypt) + } + + @Override + public String getUsername() { + return user.getUserName(); + } + + public String getFullname() { + + return user.getFullName(); + } + + // 👇 si en la vista usas principal.role, añade este también + public String getRole() { + return user.getRoles().stream() + .map(r -> r.getName()) // "ADMIN", "USER", ... + .findFirst() + .orElse("-"); + } + + /** (Opcional) Todos los roles “limpios” por si quieres listarlos. */ + public java.util.Set getRoleNames() { + return user.getRoles().stream() + .map(r -> r.getName()) + .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); + } + + @Override + public boolean isAccountNonExpired() { + return true; // puedes añadir lógica si quieres + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; // igual que arriba + } + + @Override + public boolean isEnabled() { + return user.isEnabled(); + } + + public User getUser() { + return user; + } +} diff --git a/src/main/java/com/imprimelibros/erp/users/UserService.java b/src/main/java/com/imprimelibros/erp/users/UserService.java new file mode 100644 index 0000000..dbf43f9 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/UserService.java @@ -0,0 +1,8 @@ +package com.imprimelibros.erp.users; + +import com.imprimelibros.erp.users.User; +import org.springframework.security.core.userdetails.UserDetailsService; + +public interface UserService extends UserDetailsService { + public User findByUserName(String userName); +} diff --git a/src/main/java/com/imprimelibros/erp/users/UserServiceImpl.java b/src/main/java/com/imprimelibros/erp/users/UserServiceImpl.java new file mode 100644 index 0000000..47727b4 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/users/UserServiceImpl.java @@ -0,0 +1,44 @@ +package com.imprimelibros.erp.users; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import java.util.Collection; +import java.util.stream.Collectors; + +@Service +public class UserServiceImpl implements UserService { + + private UserDao userDao; + private RoleDao roleDao; + + public UserServiceImpl(UserDao userDao, RoleDao roleDao) { + this.userDao = userDao; + this.roleDao = roleDao; + } + + @Override + public User findByUserName(String userName) { + // check the database if the user already exists + return userDao.findByUserNameAndEnabledTrue(userName); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userDao.findByUserNameAndEnabledTrue(username); + if (user == null) { + throw new UsernameNotFoundException("No existe usuario: " + username); + } + return new UserDetailsImpl(user); + } + + /*public List getUsersList() { + return userDao.findAll(); + }*/ + + private Collection mapRolesToAuthorities(Collection roles) { + return roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList()); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cddf59d..f893082 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -47,3 +47,10 @@ spring.web.resources.chain.enabled=true spring.web.resources.chain.strategy.content.enabled=true spring.web.resources.chain.strategy.content.paths=/assets/** + +# +# Session timeout +# +server.servlet.session.timeout=30m + +security.rememberme.key=N`BY^YRVO:/\H$hsKxNq \ No newline at end of file diff --git a/src/main/resources/i18n/app_es.properties b/src/main/resources/i18n/app_es.properties index 59a5250..1043a15 100644 --- a/src/main/resources/i18n/app_es.properties +++ b/src/main/resources/i18n/app_es.properties @@ -6,4 +6,13 @@ app.cancelar=Cancelar app.guardar=Guardar app.editar=Editar app.eliminar=Eliminar -app.imprimir=Imprimir \ No newline at end of file +app.imprimir=Imprimir + +app.bienvenido=Bienvenido +app.perfil=Perfil +app.mensajes=Mensajes +app.logout=Cerrar sesión + +app.sidebar.inicio=Inicio +app.sidebar.usuarios=Usuarios +app.sidebar.configuracion=Configuración \ No newline at end of file diff --git a/src/main/resources/i18n/login_es.properties b/src/main/resources/i18n/login_es.properties index 9ab97d8..a9cb6cb 100644 --- a/src/main/resources/i18n/login_es.properties +++ b/src/main/resources/i18n/login_es.properties @@ -1 +1,14 @@ -login.login=Iniciar sesión \ No newline at end of file +login.login=Iniciar sesión +login.welcome=Bienvenido +login.subtitle=Inicia sesión para continuar: +login.email=Correo electrónico +login.password=Contraseña +login.forgotPassword=¿Olvidaste tu contraseña? +login.rememberMe=Recuérdame +login.submit=Enviar +login.error=Correo electrónico o contraseña incorrectos. +login.slogan=imprimelibros.com
Especialistas en impresión de libros +login.email-placeholder=Introduce tu correo electrónico +login.password-placeholder=Introduce tu contraseña +login.new-account=¿No tienes una cuenta? +login.sign-up=Regístrate \ No newline at end of file diff --git a/src/main/resources/static/assets/css/app.css b/src/main/resources/static/assets/css/app.css index 1dd3650..0111cf3 100644 --- a/src/main/resources/static/assets/css/app.css +++ b/src/main/resources/static/assets/css/app.css @@ -3460,7 +3460,7 @@ File: Main Css File bottom: 0; left: 0; top: 0; - opacity: 0.7; + opacity: 0.4; background-color: #000; } @@ -12726,16 +12726,18 @@ span.flatpickr-weekday { padding: 2px 16px; } -/*.auth-bg-cover { - /*background: linear-gradient(-45deg, #432874 50%, #984c0c); -}*/ +/* Imagen de fondo completa */ +.auth-bg-cover { + background: url("../images/cover-bg-login.png") center center / cover no-repeat; + position: relative; + z-index: 1; +} + +/* El overlay que tapa la imagen */ .auth-bg-cover > .bg-overlay { - /*background-image: url("../images/cover-pattern.png");*/ - background-image: url("../images/cover-bg-login.png"); - background-position: center; - background-size: cover; - opacity: 1; - background-color: transparent; + /*background: none !important; /* quítalo si no quieres oscuridad */ + /* O bien hazlo más sutil, ejemplo: */ + background: rgba(0,0,0,0.10) !important; } .auth-bg-cover .footer { color: rgba(255, 255, 255, 0.5); diff --git a/src/main/resources/templates/imprimelibros/layout.html b/src/main/resources/templates/imprimelibros/layout.html index 572e2c0..28314dd 100644 --- a/src/main/resources/templates/imprimelibros/layout.html +++ b/src/main/resources/templates/imprimelibros/layout.html @@ -1,7 +1,7 @@ + xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> diff --git a/src/main/resources/templates/imprimelibros/login/_items/_forgot-pass.html b/src/main/resources/templates/imprimelibros/login/_items/_forgot-pass.html new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/templates/imprimelibros/login/_items/_login.html b/src/main/resources/templates/imprimelibros/login/_items/_login.html new file mode 100644 index 0000000..b97eda3 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/login/_items/_login.html @@ -0,0 +1,59 @@ +
+
+
+
¡Bienvenido!
+

Inicie sesión para continuar:

+
+ +
+
+ + + +
+ + +
+ +
+ + +
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+

+ ¿No tienes una cuenta? + + Regístrate + +

+
+
+
\ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/login/_items/_signup.html b/src/main/resources/templates/imprimelibros/login/_items/_signup.html new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/templates/imprimelibros/login/login.html b/src/main/resources/templates/imprimelibros/login/login.html index 190c674..ff5be3e 100644 --- a/src/main/resources/templates/imprimelibros/login/login.html +++ b/src/main/resources/templates/imprimelibros/login/login.html @@ -1,12 +1,11 @@ - + - - Login - ImprimeLibros + @@ -23,13 +22,14 @@
-
+
-
+
- -

imprimelibros.com
- Los especialistas en impresión de libros

+

+ imprimelibros.com
+ Especialistas en impresión de libros

@@ -37,61 +37,9 @@
-
-
-
Bienvenido!
-

Inicie sesión para continuar:

-
- -
-
- - - -
- - -
- -
- - -
- - -
-
- -
- - -
- -
- -
- - -
-
- -
-

¿No tienes una cuenta? - Regístrate

-
-
+
+
diff --git a/src/main/resources/templates/imprimelibros/partials/sidebar.html b/src/main/resources/templates/imprimelibros/partials/sidebar.html index 8425d9d..a31419f 100644 --- a/src/main/resources/templates/imprimelibros/partials/sidebar.html +++ b/src/main/resources/templates/imprimelibros/partials/sidebar.html @@ -9,7 +9,7 @@ - + @@ -18,11 +18,12 @@ - +
@@ -32,14 +33,24 @@
-
diff --git a/src/main/resources/templates/imprimelibros/partials/topbar.html b/src/main/resources/templates/imprimelibros/partials/topbar.html index a7976cf..82f9040 100644 --- a/src/main/resources/templates/imprimelibros/partials/topbar.html +++ b/src/main/resources/templates/imprimelibros/partials/topbar.html @@ -69,46 +69,31 @@
@@ -122,6 +107,10 @@
+ +
+ +