trabajando en usuarios

This commit is contained in:
2025-09-26 15:13:11 +02:00
parent 062a20c26a
commit 01a1ac4b71
30 changed files with 937 additions and 139 deletions

View File

@ -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") // <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()))
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "IMPRIMELIBROS_REMEMBER")
.permitAll());
return http.build();
}
}

View File

@ -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<T> {
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<FilterHook<T>> filters = new ArrayList<>();
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;
}
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; }
/** 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<>();
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; }
/** 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; });
return this;
}
/** filter((builder, req) -> builder.add(miExtraSpec(req))) */
public DataTable<T> filter(FilterHook<T> hook) { filters.add(hook); return this; }
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 };
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<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));
}
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<Map<String,Object>> data = new ArrayList<>();
for (T e : p.getContent()) {
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);
row.remove("__entity");
data.add(row);
}
return new DataTablesResponse<>(dt.draw, totalCount, filtered, data);
}
}

View File

@ -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;}}
}

View File

@ -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> order = new ArrayList<>();
public List<Column> columns = new ArrayList<>();
public Map<String,String> 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();
}
}

View File

@ -0,0 +1,17 @@
package com.imprimelibros.erp.datatables;
import java.util.List;
public class DataTablesResponse<T> {
public int draw;
public long recordsTotal;
public long recordsFiltered;
public List<T> data;
public DataTablesResponse(int draw, long total, long filtered, List<T> data) {
this.draw = draw;
this.recordsTotal = total;
this.recordsFiltered = filtered;
this.data = data;
}
}

View File

@ -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 <T> DataTablesResponse<T> handle(
DataTablesRequest dt,
JpaSpecificationExecutor<T> repo,
long totalCount, // count sin filtros (cacheable)
List<String> searchableFields,
Class<T> entityClass
) {
// Spec (filtros)
Specification<T> spec = DataTablesSpecification.build(dt, searchableFields);
// Sort
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));
}
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<T> result = repo.findAll(spec, pageable);
long filtered = result.getTotalElements();
return new DataTablesResponse<>(
dt.draw,
totalCount,
filtered,
result.getContent()
);
}
}

View File

@ -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 <T> Specification<T> build(DataTablesRequest dt, List<String> searchableFields) {
return (root, query, cb) -> {
List<Predicate> 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<Predicate> 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() + "%");
}
}

View File

@ -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<String, String> translations = Map.of();
model.addAttribute("languageBundle", translations);
}
return "imprimelibros/home";
}
}

View File

@ -11,6 +11,9 @@ public class LoginController {
@GetMapping("/login")
public String index(Model model, Locale locale) {
model.addAttribute("form", "_login");
return "imprimelibros/login/login";
}
}

View File

@ -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 + '\'' + '}';
}
}

View File

@ -0,0 +1,9 @@
package com.imprimelibros.erp.users;
import com.imprimelibros.erp.users.Role;
public interface RoleDao {
public Role findRoleByName(String theRoleName);
}

View File

@ -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<Role> 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;
}
}

View File

@ -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<Role> 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<Role> 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<Role> getRoles() {
return roles;
}
public void setRoles(Collection<Role> roles) {
this.roles = roles;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", fullName='" + fullName + '\'' +
", userName='" + userName + '\'' +
", password='" + password + '\'' +
", enabled=" + enabled +
", roles=" + roles +
'}';
}
}

View File

@ -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<Map<String,Object>> datatable(HttpServletRequest request) {
DataTablesRequest dt = DataTablesParser.from(request);
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);
}
}

View File

@ -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<User, Long>, JpaSpecificationExecutor<User> {
User findByUserNameAndEnabledTrue(String userName);
}

View File

@ -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<? extends GrantedAuthority> getAuthorities() {
// Si tu User tiene un Set<Role>:
Set<String> 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<String> 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;
}
}

View File

@ -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);
}

View File

@ -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<User> getUsersList() {
return userDao.findAll();
}*/
private Collection<? extends GrantedAuthority> mapRolesToAuthorities(Collection<Role> roles) {
return roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
}
}

View File

@ -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

View File

@ -6,4 +6,13 @@ app.cancelar=Cancelar
app.guardar=Guardar
app.editar=Editar
app.eliminar=Eliminar
app.imprimir=Imprimir
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

View File

@ -1 +1,14 @@
login.login=Iniciar sesión
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<br>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

View File

@ -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);

View File

@ -1,7 +1,7 @@
<html th:lang="${#locale.language}" th:with="isAuth=${#authorization.expression('isAuthenticated()')}"
th:attrappend="data-layout=${isAuth} ? 'semibox' : 'horizontal'" data-sidebar-visibility="show" data-topbar="light"
data-sidebar="light" data-sidebar-size="lg" data-sidebar-image="none" data-preloader="disable"
xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<th:block layout:fragment="pagetitle" />

View File

@ -0,0 +1,59 @@
<div th:fragment="_login">
<div class="p-lg-5 p-4">
<div>
<h5 class="text-primary" th:text="#{login.welcome}">¡Bienvenido!</h5>
<p class="text-muted" th:text="#{login.subtitle}">Inicie sesión para continuar:</p>
</div>
<div class="mt-4">
<form th:action="@{/login}" method="post">
<!-- CSRF obligatorio -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<div class="mb-3">
<label for="username" class="form-label" th:text="#{login.email}">Correo electrónico</label>
<input type="email" class="form-control" id="username" th:placeholder="#{login.email-placeholder}"
name="username">
</div>
<div class="mb-3">
<div class="float-end">
<a href="/auth-pass-reset-cover" class="text-muted" th:text="#{login.forgotPassword}">¿Olvidó su
contraseña?</a>
</div>
<label class="form-label" for="password-input" th:text="#{login.password}">Contraseña</label>
<div class="position-relative auth-pass-inputgroup mb-3">
<input type="password" class="form-control pe-5 password-input"
th:placeholder="#{login.password-placeholder}" id="password-input" name="password">
<button
class="btn btn-link position-absolute end-0 top-0 text-decoration-none text-muted password-addon"
type="button" id="password-addon"><i class="ri-eye-fill align-middle"></i></button>
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="remember-me" name="remember-me">
<label class="form-check-label" for="remember-me"
th:text="#{login.rememberMe}">Recuerdame</label>
</div>
<div class="mt-4">
<button class="btn btn-secondary w-100" type="submit" th:text="#{login.login}">Iniciar
Sesión</button>
</div>
</form>
</div>
<div class="mt-5 text-center">
<p class="mb-0">
<span th:text="#{login.new-account}">¿No tienes una cuenta?</span>
<a th:href="@{/auth-signup-cover}" class="fw-semibold text-primary text-decoration-underline"
th:text="#{login.sign-up}">
Regístrate
</a>
</p>
</div>
</div>
</div>

View File

@ -1,12 +1,11 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" lang="en"
data-layout="vertical" data-topbar="light" data-sidebar="dark" data-sidebar-size="lg" data-sidebar-image="none"
data-preloader="disable">
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
th:lang="${#locale.language}" data-layout="vertical" data-topbar="light" data-sidebar="dark" data-sidebar-size="lg"
data-sidebar-image="none" data-preloader="disable">
<head>
<!--page title-->
<!-- <div th:replace="partials/title-meta :: title-meta('Sign In')"></div> -->
<title>Login - ImprimeLibros</title>
<th:block layout:fragment="pagetitle" />
<!-- Page CSS -->
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
@ -23,13 +22,14 @@
<div class="card overflow-hidden">
<div class="row g-0">
<div class="col-lg-6">
<div class="p-lg-5 p-4 auth-one-bg h-100">
<div class="p-lg-1 p-1 auth-one-bg h-100">
<div class="bg-overlay"></div>
<div class="position-relative h-100 d-flex flex-column">
<div class="position-relative h-100 d-flex flex-column justify-content-end">
<div class="mt-auto">
<p class="fs-15 fst-italic text-center" style="color: lightgray;">imprimelibros.com <br>
Los especialistas en impresión de libros</p>
<p class="fs-18 fst-italic text-center" style="color: lightgray;"
th:utext="#{login.slogan}">
imprimelibros.com<br>
Especialistas en impresión de libros</p>
</div>
</div>
</div>
@ -37,61 +37,9 @@
<!-- end col -->
<div class="col-lg-6">
<div class="p-lg-5 p-4">
<div>
<h5 class="text-primary">Bienvenido!</h5>
<p class="text-muted">Inicie sesión para continuar:</p>
</div>
<div class="mt-4">
<form th:action="@{/login}" method="post">
<!-- CSRF obligatorio -->
<input type="hidden" th:name="${_csrf.parameterName}"
th:value="${_csrf.token}" />
<div class="mb-3">
<label for="username" class="form-label">Usuario</label>
<input type="text" class="form-control" id="username"
placeholder="Enter username">
</div>
<div class="mb-3">
<div class="float-end">
<a href="/auth-pass-reset-cover" class="text-muted">¿Olvidó su contraseña?</a>
</div>
<label class="form-label" for="password-input">Contraseña</label>
<div class="position-relative auth-pass-inputgroup mb-3">
<input type="password" class="form-control pe-5 password-input"
placeholder="Enter password" id="password-input">
<button
class="btn btn-link position-absolute end-0 top-0 text-decoration-none text-muted password-addon"
type="button" id="password-addon"><i
class="ri-eye-fill align-middle"></i></button>
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value=""
id="auth-remember-check">
<label class="form-check-label" for="auth-remember-check">Recuerdame</label>
</div>
<div class="mt-4">
<button class="btn btn-secondary w-100" type="submit">Iniciar
Sesión</button>
</div>
</form>
</div>
<div class="mt-5 text-center">
<p class="mb-0">¿No tienes una cuenta? <a href="/auth-signup-cover"
class="fw-semibold text-primary text-decoration-underline">
Regístrate</a> </p>
</div>
</div>
<div th:insert="~{${'imprimelibros/login/_items/' + form} :: ${form}}"></div>
</div>
<!-- end col -->
</div>
<!-- end row -->

View File

@ -9,7 +9,7 @@
<img src="/assets/images/logo-sm.png" alt="" height="22">
</span>
<span class="logo-lg">
<img src="/assets/images/logo-dark.png" alt="" height="17">
<img src="/assets/images/logo-dark.png" alt="" height="45">
</span>
</a>
<!-- Light Logo-->
@ -18,11 +18,12 @@
<img src="/assets/images/logo-sm.png" alt="" height="22">
</span>
<span class="logo-lg">
<img src="/assets/images/logo-light.png" alt="" height="17">
<img src="/assets/images/logo-light.png" alt="" height="45">
</span>
</a>
<button type="button" class="btn btn-sm p-0 fs-20 header-item float-end btn-vertical-sm-hover"
id="vertical-hover">
id="vertical-hover"
href="/#" data-bs-toggle="tooltip" data-bs-placement="right" title="Expand">
<i class="ri-record-circle-line"></i>
</button>
</div>
@ -32,14 +33,24 @@
<div id="two-column-menu">
</div>
<li href="/" class="menu-title"><span data-key="t-menu">Menu</span></li>
<ul class="navbar-nav" id="navbar-nav">
<li class="nav-item">
<a class="nav-link menu-link" href="/">
<i class="ri-home-line"></i> <span data-key="t-home">Inicio</span>
<i class="ri-home-line"></i> <span th:text="#{app.sidebar.inicio}">Inicio</span>
</a>
</li>
<!-- <div th:replace="~{printhub/partials/sidebarMenus/configurationMenu :: configuration}"></div> -->
<li class="nav-item">
<a class="nav-link menu-link" href="/">
<i class="ri-user-line"></i> <span th:text="#{app.sidebar.usuarios}">Usuarios</span>
</a>
</li>
<div th:if="${#authentication.principal.role == 'SUPERADMIN'}">
<li class="nav-item">
<a class="nav-link menu-link" href="/">
<i class="ri-settings-2-line"></i> <span th:text="#{app.sidebar.configuracion}">Configuración</span>
</a>
</li>
</div>
</ul>
</div>
<!-- Sidebar -->

View File

@ -69,46 +69,31 @@
<div class="dropdown ms-sm-3 header-item topbar-user">
<button type="button" class="btn" id="page-header-user-dropdown" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="d-flex align-items-center">
<img class="rounded-circle header-profile-user"
src="/assets/images/users/avatar-1.jpg" alt="Header Avatar">
<span class="text-start ms-xl-2">
<span class="d-none d-xl-inline-block ms-1 fw-medium user-name-text">Anna
Adame</span>
<span
class="d-none d-xl-block ms-1 fs-12 text-muted user-name-sub-text">Founder</span>
</span>
<span sec:authorize="isAuthenticated()" class="text-center ms-xl-2">
<span class="d-none d-xl-inline-block ms-1 fw-medium user-name-text"
th:text="${#authentication.principal.fullname}">Nombre</span>
<div th:if="${#authentication.principal.role != 'USER'}">
<span class="d-none d-xl-block ms-1 fs-12 text-muted user-name-sub-text"
th:text="${#authentication.principal.role}">Rol</span>
</div>
</span>
</button>
<div class="dropdown-menu dropdown-menu-end">
<!-- item-->
<h6 class="dropdown-header">Welcome Anna!</h6>
<h6 class="dropdown-header"><span th:text="#{app.bienvenido}">Bienvenido</span> <span
th:text="${#authentication.principal.fullname}">User</span> <span>!</span></h6>
<a class="dropdown-item" href="/pages-profile"><i
class="mdi mdi-account-circle text-muted fs-16 align-middle me-1"></i> <span
class="align-middle">Profile</span></a>
class="align-middle" th:text="#{app.perfil}">Perfil</span></a>
<a class="dropdown-item" href="/apps-chat"><i
class="mdi mdi-message-text-outline text-muted fs-16 align-middle me-1"></i>
<span class="align-middle">Messages</span></a>
<a class="dropdown-item" href="/apps-tasks-kanban"><i
class="mdi mdi-calendar-check-outline text-muted fs-16 align-middle me-1"></i>
<span class="align-middle">Taskboard</span></a>
<a class="dropdown-item" href="/pages-faqs"><i
class="mdi mdi-lifebuoy text-muted fs-16 align-middle me-1"></i> <span
class="align-middle">Help</span></a>
<span class="align-middle" th:text="#{app.mensajes}">Mensajes</span></a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/pages-profile"><i
class="mdi mdi-wallet text-muted fs-16 align-middle me-1"></i> <span
class="align-middle">Balance : <b>$5971.67</b></span></a>
<a class="dropdown-item" href="/pages-profile-settings"><span
class="badge bg-soft-success text-success mt-1 float-end">New</span><i
class="mdi mdi-cog-outline text-muted fs-16 align-middle me-1"></i> <span
class="align-middle">Settings</span></a>
<a class="dropdown-item" href="auth-lockscreen-basic"><i
class="mdi mdi-lock text-muted fs-16 align-middle me-1"></i> <span
class="align-middle">Lock screen</span></a>
<a class="dropdown-item" href="auth-logout-basic"><i
class="mdi mdi-logout text-muted fs-16 align-middle me-1"></i> <span
class="align-middle" data-key="t-logout">Logout</span></a>
<a class="dropdown-item" href="#"
onclick="document.getElementById('logoutForm').submit(); return false;">
<i class="mdi mdi-logout text-muted fs-16 align-middle me-1"></i>
<span class="align-middle" data-key="t-logout" th:text="#{app.logout}">Cerrar sesión</span>
</a>
</div>
</div>
</div>
@ -122,6 +107,10 @@
</div>
</div>
</div>
<form id="logoutForm" th:action="@{/logout}" method="post" class="d-none">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
</header>