implementado el sistema de roles

This commit is contained in:
Jaime Jiménez
2025-06-26 15:21:28 +02:00
parent f5b2c0b509
commit 8001cf527a
21 changed files with 321 additions and 87 deletions

View File

@ -51,6 +51,10 @@
<groupId>nz.net.ultraq.thymeleaf</groupId> <groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId> <artifactId>thymeleaf-layout-dialect</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>

View File

@ -19,25 +19,27 @@ public class SecurityConfig {
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http http
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/register", "/assets/**", "/api/lang", "/api/lang/**").permitAll() .requestMatchers("/login", "/register", "/assets/**", "/api/lang", "/api/lang/**").permitAll()
.anyRequest().authenticated()) .anyRequest().authenticated())
.userDetailsService(userService) .userDetailsService(userService)
.formLogin(login -> login .formLogin(login -> login
.loginPage("/login") .loginPage("/login")
.loginProcessingUrl("/login") .loginProcessingUrl("/login")
.defaultSuccessUrl("/", true) .defaultSuccessUrl("/", true)
.failureUrl("/login?error=true") .failureUrl("/login?error=true")
.permitAll()) .permitAll())
.logout(logout -> logout .logout(logout -> logout
.logoutUrl("/logout") .logoutUrl("/logout")
.logoutSuccessHandler((request, response, authentication) -> { .logoutSuccessHandler((request, response, authentication) -> {
String lang = request.getParameter("lang"); String lang = request.getParameter("lang");
if (lang == null || lang.isBlank()) lang = "es"; if (lang == null || lang.isBlank()) lang = "es";
response.sendRedirect("/login?logout&lang=" + lang); response.sendRedirect("/login?logout&lang=" + lang);
})); }))
.exceptionHandling(ex -> ex
.accessDeniedPage("/error/403") // ✅ vista personalizada para acceso denegado
);
return http.build(); return http.build();
} }

View File

@ -2,6 +2,7 @@ package com.printhub.printhub.controller.configuration;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -46,6 +47,7 @@ public class PrintersController {
return "printhub/configuration/printers"; return "printhub/configuration/printers";
} }
@PreAuthorize("hasAuthority('PRINTERS:VIEW')")
@GetMapping("/datatable") @GetMapping("/datatable")
@ResponseBody @ResponseBody
public Map<String, Object> datatable(@RequestParam Map<String, String> params) { public Map<String, Object> datatable(@RequestParam Map<String, String> params) {

View File

@ -2,6 +2,12 @@ package com.printhub.printhub.entity.configuration;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.printhub.printhub.entity.security.Role;
@Entity @Entity
@Table(name = "users") @Table(name = "users")
@ -17,9 +23,6 @@ public class User {
@Column(nullable = false) @Column(nullable = false)
private String password; private String password;
@Column(nullable = false)
private String role; // Ej: ROLE_USER, ROLE_ADMIN
@Column(nullable = false) @Column(nullable = false)
private boolean enabled = true; private boolean enabled = true;
@ -40,57 +43,109 @@ public class User {
@JoinColumn(name = "customer_id") @JoinColumn(name = "customer_id")
private Customer customer; private Customer customer;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
public Set<Role> getRoles() {
return roles;
}
// Getters y Setters // Getters y Setters
public Long getId() { return id; } public Long getId() {
return id;
}
public void setId(Long id) { this.id = id; } public void setId(Long id) {
this.id = id;
}
public String getUsername() { return username; } public String getUsername() {
return username;
}
public void setUsername(String username) { this.username = username; } public void setUsername(String username) {
this.username = username;
}
public String getPassword() { return password; } public String getPassword() {
return password;
}
public void setPassword(String password) { this.password = password; } public void setPassword(String password) {
this.password = password;
}
public String getRole() { return role; } public boolean isEnabled() {
return enabled;
}
public void setRole(String role) { this.role = role; } public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isEnabled() { return enabled; } public String getFirstName() {
return firstName;
}
public void setEnabled(boolean enabled) { this.enabled = enabled; } public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getFirstName() { return firstName; } public String getLastName() {
return lastName;
}
public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getLastName() { return lastName; } public String getComments() {
return comments;
}
public void setLastName(String lastName) { this.lastName = lastName; } public void setComments(String comments) {
this.comments = comments;
}
public String getComments() { return comments; } public LocalDateTime getLastActive() {
return lastActive;
}
public void setComments(String comments) { this.comments = comments; } public void setLastActive(LocalDateTime lastActive) {
this.lastActive = lastActive;
}
public LocalDateTime getLastActive() { return lastActive; } public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setLastActive(LocalDateTime lastActive) { this.lastActive = lastActive; } public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getCreatedAt() { return createdAt; } public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getUpdatedAt() { return updatedAt; } public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
public LocalDateTime getDeletedAt() { return deletedAt; } public Customer getCustomer() {
return customer;
}
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; } public void setCustomer(Customer customer) {
this.customer = customer;
public Customer getCustomer() { return customer; } }
public void setCustomer(Customer customer) { this.customer = customer; }
} }

View File

@ -0,0 +1,19 @@
package com.printhub.printhub.entity.security;
import jakarta.persistence.*;
@Entity
@Table(name = "modules")
public class Module {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
public String getName() {
return name;
}
}

View File

@ -0,0 +1,27 @@
package com.printhub.printhub.entity.security;
import jakarta.persistence.*;
@Entity
@Table(name = "permissions", uniqueConstraints = @UniqueConstraint(columnNames = { "module_id", "action" }))
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "module_id", nullable = false)
private Module module;
public Module getModule() {
return module;
}
@Column(nullable = false)
private String action;
public String getAction() {
return action;
}
}

View File

@ -0,0 +1,43 @@
package com.printhub.printhub.entity.security;
import com.printhub.printhub.entity.configuration.User;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "roles_permissions", joinColumns = @JoinColumn(name = "role_id"), inverseJoinColumns = @JoinColumn(name = "permission_id"))
private Set<Permission> permissions = new HashSet<>();
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Permission> getPermissions() {
return permissions;
}
public void setPermissions(Set<Permission> permissions) {
this.permissions = permissions;
}
}

View File

@ -2,10 +2,13 @@ package com.printhub.printhub.repository.configuration;
import com.printhub.printhub.entity.configuration.User; import com.printhub.printhub.entity.configuration.User;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.stereotype.Repository;
import java.util.Optional; import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = { "roles", "roles.permissions", "roles.permissions.module" })
Optional<User> findByUsername(String username); Optional<User> findByUsername(String username);
} }

View File

@ -0,0 +1,10 @@
package com.printhub.printhub.repository.security;
import com.printhub.printhub.entity.security.Permission;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface PermissionRepository extends JpaRepository<Permission, Long> {
Optional<Permission> findByModule_NameAndAction(String moduleName, String action);
}

View File

@ -0,0 +1,10 @@
package com.printhub.printhub.repository.security;
import com.printhub.printhub.entity.security.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(String name);
}

View File

@ -1,6 +1,5 @@
package com.printhub.printhub.security; package com.printhub.printhub.security;
import com.printhub.printhub.entity.configuration.User;
import com.printhub.printhub.repository.configuration.UserRepository; import com.printhub.printhub.repository.configuration.UserRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;

View File

@ -2,14 +2,17 @@ package com.printhub.printhub.service.configuration;
import com.printhub.printhub.entity.configuration.User; import com.printhub.printhub.entity.configuration.User;
import com.printhub.printhub.repository.configuration.UserRepository; import com.printhub.printhub.repository.configuration.UserRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*; import org.springframework.security.core.userdetails.*;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections; import java.util.*;
import java.util.Optional; import java.util.stream.Collectors;
@Service @Service
public class UserService implements UserDetailsService { public class UserService implements UserDetailsService {
@ -20,31 +23,43 @@ public class UserService implements UserDetailsService {
@Autowired @Autowired
private PasswordEncoder passwordEncoder; private PasswordEncoder passwordEncoder;
public void register(String username, String password, String role) { public void register(String username, String password, String roleName) {
User user = new User(); User user = new User();
user.setUsername(username); user.setUsername(username);
user.setPassword(passwordEncoder.encode(password)); user.setPassword(passwordEncoder.encode(password));
user.setRole("ROLE_" + role.toUpperCase());
user.setEnabled(true); user.setEnabled(true);
// NOTA: aquí deberías asociar roles desde RoleRepository, lo dejamos en blanco
// por ahora
userRepository.save(user); userRepository.save(user);
} }
@Override @Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> userOpt = userRepository.findByUsername(username); User user = userRepository.findByUsername(username)
if (userOpt.isEmpty() || userOpt.get().getDeletedAt() != null) { .orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado"));
throw new UsernameNotFoundException("Usuario no encontrado o eliminado");
} List<GrantedAuthority> authorities = user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.map(permission -> new SimpleGrantedAuthority(
permission.getModule().getName().toUpperCase() + ":" + permission.getAction().toUpperCase()))
.distinct()
.collect(Collectors.toList());
// DEBUG: mostrar en consola los permisos asignados
/*System.out.println("→ Roles del usuario:");
user.getRoles().forEach(role -> {
System.out.println(" Role: " + role.getName());
role.getPermissions().forEach(
perm -> System.out.println(" Perm: " + perm.getModule().getName() + ":" + perm.getAction()));
});*/
User user = userOpt.get();
return new org.springframework.security.core.userdetails.User( return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getUsername(),
user.getPassword(), user.getPassword(),
user.isEnabled(), user.isEnabled(),
true, true, true, true,
true, authorities);
user.getDeletedAt() == null,
Collections.singletonList(new SimpleGrantedAuthority(user.getRole()))
);
} }
} }

View File

@ -1,7 +1,6 @@
package com.printhub.printhub.utils.datatables; package com.printhub.printhub.utils.datatables;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import java.lang.reflect.Field;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;

View File

@ -1,3 +1,5 @@
t-menu=Menu t-menu=Menu
t-menu-config=Configuration t-menu-config=Configuration
t-menu-config-impresioras=Printers t-menu-config-impresoras=Users
t-menu-config-clientes=Customers
t-menu-config-impresoras=Printers

View File

@ -2,8 +2,8 @@
t-paginas=Páginas t-paginas=Páginas
# Roles # Roles
t-role.ROLE_ADMIN=Administrador t-role.admin=Administrador
t-role.ROLE_USER=Usuario t-role.user=Usuario
# Topbar # Topbar
t-topbar-logout=Cerrar sesión t-topbar-logout=Cerrar sesión

View File

@ -1,3 +1,5 @@
t-menu=Menú t-menu=Menú
t-menu-config=Configuración t-menu-config=Configuración
t-menu-config-impresioras=Impresoras t-menu-config-impresoras=Usuarios
t-menu-config-clientes=Clientes
t-menu-config-impresoras=Impresoras

View File

@ -0,0 +1,27 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{printhub/layout}">
<th:block layout:fragment="pagetitle">
<!--page title-->
<div th:replace="~{printhub/partials/title-meta :: title-meta(${title})}"></div>
</th:block>
<head>
</head>
<body>
<div layout:fragment="content">
<!-- start page title -->
<div th:replace="~{printhub/partials/page-title :: page-title(${title},'Pages')}"></div>
<h1>403 - No tienes permiso para acceder a esta página.</h1>
<a href="/">Volver al inicio</a>
</div>
<th:block layout:fragment="pagejs">
</th:block>
</body>
</html>

View File

@ -12,7 +12,7 @@
<body> <body>
<div layout:fragment="content"> <div layout:fragment="content">
<!-- start page title --> <!-- start page title -->
<div th:replace="~{printhub/partials/page-title :: page-title(${title},'Pages')}"></div> <div th:replace="~{printhub/partials/page-title :: page-title(${title},'Pages')}"></div>
</div> </div>

View File

@ -1,7 +1,7 @@
<html th:lang="${#locale.language}" data-layout="semibox" data-sidebar-visibility="show" data-topbar="light" data-sidebar="light" <html th:lang="${#locale.language}" data-layout="semibox" data-sidebar-visibility="show" data-topbar="light" data-sidebar="light"
data-sidebar-size="lg" data-sidebar-image="none" data-preloader="disable" xmlns="http://www.thymeleaf.org" data-sidebar-size="lg" data-sidebar-image="none" data-preloader="disable" xmlns="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head> <head>
<style> <style>

View File

@ -1,16 +1,29 @@
<div th:fragment="configuration" th:remove="tag"> <div th:fragment="configuration" th:remove="tag">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link menu-link" href="#configurationMenu" data-bs-toggle="collapse" role="button" <a class="nav-link menu-link" href="#configurationMenu" data-bs-toggle="collapse" role="button"
aria-expanded="false" aria-controls="configurationMenu"> aria-expanded="false" aria-controls="configurationMenu">
<i class="ri-tools-line"></i> <span data-key="t-menu-config">Configuración</span> <i class="ri-tools-line"></i>
<span data-key="t-menu-config">Configuración</span>
</a> </a>
<div class="collapse menu-dropdown" id="configurationMenu"> <div class="collapse menu-dropdown" id="configurationMenu">
<ul class="nav nav-sm flex-column"> <ul class="nav nav-sm flex-column">
<li class="nav-item"> <li class="nav-item" sec:authorize="hasAuthority('USERS:VIEW')">
<a href="/configuration/printers" class="nav-link" data-key="t-menu-config-impresioras"> <a href="/configuration/users" class="nav-link" data-key="t-menu-config-usuarios">
Impresoras </a> Usuarios
</a>
</li> </li>
<li class="nav-item" sec:authorize="hasAuthority('PRINTERS:VIEW')">
<a href="/configuration/printers" class="nav-link" data-key="t-menu-config-impresoras">
Impresoras
</a>
</li>
<li class="nav-item" sec:authorize="hasAuthority('CUSTOMERS:VIEW')">
<a href="/configuration/customers" class="nav-link" data-key="t-menu-config-clientes">
Clientes
</a>
</li>
<!-- Añadir más secciones según los permisos -->
</ul> </ul>
</div> </div>
</li> <!-- end Dashboard Menu --> </li>
</div> </div>

View File

@ -89,9 +89,11 @@
<span class="d-none d-xl-inline-block ms-1 fw-medium user-name-text" <span class="d-none d-xl-inline-block ms-1 fw-medium user-name-text"
th:text="${currentUser.firstName + ' ' + currentUser.lastName}"> th:text="${currentUser.firstName + ' ' + currentUser.lastName}">
</span> </span>
<span class="d-none d-xl-block ms-1 fs-12 text-muted user-name-sub-text" <span class="d-none d-xl-block ms-1 fs-12 text-muted user-name-sub-text">
th:attr="data-key=${'t-role.' + currentUser.role} ?: ${currentUser.role}" <span th:each="r : ${currentUser.roles}"
th:text="#{${'t-role.' + currentUser.role} ?: ${currentUser.role}}"> th:attr="data-key=${'t-role.' + r.name}"
th:text="#{${'t-role.' + r.name}} + ' '">
</span>
</span> </span>
</span> </span>
</span> </span>