mirror of
https://git.imnavajas.es/jjimenez/printhub.git
synced 2026-01-12 08:28:48 +00:00
implementado el sistema de roles
This commit is contained in:
4
pom.xml
4
pom.xml
@ -51,6 +51,10 @@
|
||||
<groupId>nz.net.ultraq.thymeleaf</groupId>
|
||||
<artifactId>thymeleaf-layout-dialect</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
|
||||
@ -19,25 +19,27 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/login", "/register", "/assets/**", "/api/lang", "/api/lang/**").permitAll()
|
||||
.anyRequest().authenticated())
|
||||
.userDetailsService(userService)
|
||||
.formLogin(login -> login
|
||||
.loginPage("/login")
|
||||
.loginProcessingUrl("/login")
|
||||
.defaultSuccessUrl("/", true)
|
||||
.failureUrl("/login?error=true")
|
||||
.permitAll())
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/logout")
|
||||
.logoutSuccessHandler((request, response, authentication) -> {
|
||||
String lang = request.getParameter("lang");
|
||||
if (lang == null || lang.isBlank()) lang = "es";
|
||||
response.sendRedirect("/login?logout&lang=" + lang);
|
||||
}));
|
||||
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/login", "/register", "/assets/**", "/api/lang", "/api/lang/**").permitAll()
|
||||
.anyRequest().authenticated())
|
||||
.userDetailsService(userService)
|
||||
.formLogin(login -> login
|
||||
.loginPage("/login")
|
||||
.loginProcessingUrl("/login")
|
||||
.defaultSuccessUrl("/", true)
|
||||
.failureUrl("/login?error=true")
|
||||
.permitAll())
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/logout")
|
||||
.logoutSuccessHandler((request, response, authentication) -> {
|
||||
String lang = request.getParameter("lang");
|
||||
if (lang == null || lang.isBlank()) lang = "es";
|
||||
response.sendRedirect("/login?logout&lang=" + lang);
|
||||
}))
|
||||
.exceptionHandling(ex -> ex
|
||||
.accessDeniedPage("/error/403") // ✅ vista personalizada para acceso denegado
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package com.printhub.printhub.controller.configuration;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@ -46,6 +47,7 @@ public class PrintersController {
|
||||
return "printhub/configuration/printers";
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('PRINTERS:VIEW')")
|
||||
@GetMapping("/datatable")
|
||||
@ResponseBody
|
||||
public Map<String, Object> datatable(@RequestParam Map<String, String> params) {
|
||||
|
||||
@ -2,6 +2,12 @@ package com.printhub.printhub.entity.configuration;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
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
|
||||
@Table(name = "users")
|
||||
@ -17,9 +23,6 @@ public class User {
|
||||
@Column(nullable = false)
|
||||
private String password;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String role; // Ej: ROLE_USER, ROLE_ADMIN
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean enabled = true;
|
||||
|
||||
@ -40,57 +43,109 @@ public class User {
|
||||
@JoinColumn(name = "customer_id")
|
||||
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
|
||||
|
||||
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 Customer getCustomer() { return customer; }
|
||||
|
||||
public void setCustomer(Customer customer) { this.customer = customer; }
|
||||
public void setCustomer(Customer customer) {
|
||||
this.customer = customer;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,13 @@ package com.printhub.printhub.repository.configuration;
|
||||
|
||||
import com.printhub.printhub.entity.configuration.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
@EntityGraph(attributePaths = { "roles", "roles.permissions", "roles.permissions.module" })
|
||||
Optional<User> findByUsername(String username);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
package com.printhub.printhub.security;
|
||||
|
||||
import com.printhub.printhub.entity.configuration.User;
|
||||
import com.printhub.printhub.repository.configuration.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
|
||||
@ -2,14 +2,17 @@ package com.printhub.printhub.service.configuration;
|
||||
|
||||
import com.printhub.printhub.entity.configuration.User;
|
||||
import com.printhub.printhub.repository.configuration.UserRepository;
|
||||
|
||||
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.userdetails.*;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class UserService implements UserDetailsService {
|
||||
@ -20,31 +23,43 @@ public class UserService implements UserDetailsService {
|
||||
@Autowired
|
||||
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.setUsername(username);
|
||||
user.setPassword(passwordEncoder.encode(password));
|
||||
user.setRole("ROLE_" + role.toUpperCase());
|
||||
user.setEnabled(true);
|
||||
// NOTA: aquí deberías asociar roles desde RoleRepository, lo dejamos en blanco
|
||||
// por ahora
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||
if (userOpt.isEmpty() || userOpt.get().getDeletedAt() != null) {
|
||||
throw new UsernameNotFoundException("Usuario no encontrado o eliminado");
|
||||
}
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado"));
|
||||
|
||||
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(
|
||||
user.getUsername(),
|
||||
user.getPassword(),
|
||||
user.isEnabled(),
|
||||
true,
|
||||
true,
|
||||
user.getDeletedAt() == null,
|
||||
Collections.singletonList(new SimpleGrantedAuthority(user.getRole()))
|
||||
);
|
||||
user.getUsername(),
|
||||
user.getPassword(),
|
||||
user.isEnabled(),
|
||||
true, true, true,
|
||||
authorities);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.printhub.printhub.utils.datatables;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
t-menu=Menu
|
||||
t-menu-config=Configuration
|
||||
t-menu-config-impresioras=Printers
|
||||
t-menu-config-impresoras=Users
|
||||
t-menu-config-clientes=Customers
|
||||
t-menu-config-impresoras=Printers
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
t-paginas=Páginas
|
||||
|
||||
# Roles
|
||||
t-role.ROLE_ADMIN=Administrador
|
||||
t-role.ROLE_USER=Usuario
|
||||
t-role.admin=Administrador
|
||||
t-role.user=Usuario
|
||||
|
||||
# Topbar
|
||||
t-topbar-logout=Cerrar sesión
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
t-menu=Menú
|
||||
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
|
||||
27
src/main/resources/templates/printhub/error/403.html
Normal file
27
src/main/resources/templates/printhub/error/403.html
Normal 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>
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<!-- start page title -->
|
||||
<!-- start page title -->
|
||||
<div th:replace="~{printhub/partials/page-title :: page-title(${title},'Pages')}"></div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<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"
|
||||
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>
|
||||
|
||||
<style>
|
||||
|
||||
@ -1,16 +1,29 @@
|
||||
<div th:fragment="configuration" th:remove="tag">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link menu-link" href="#configurationMenu" data-bs-toggle="collapse" role="button"
|
||||
aria-expanded="false" aria-controls="configurationMenu">
|
||||
<i class="ri-tools-line"></i> <span data-key="t-menu-config">Configuración</span>
|
||||
aria-expanded="false" aria-controls="configurationMenu">
|
||||
<i class="ri-tools-line"></i>
|
||||
<span data-key="t-menu-config">Configuración</span>
|
||||
</a>
|
||||
<div class="collapse menu-dropdown" id="configurationMenu">
|
||||
<ul class="nav nav-sm flex-column">
|
||||
<li class="nav-item">
|
||||
<a href="/configuration/printers" class="nav-link" data-key="t-menu-config-impresioras">
|
||||
Impresoras </a>
|
||||
<li class="nav-item" sec:authorize="hasAuthority('USERS:VIEW')">
|
||||
<a href="/configuration/users" class="nav-link" data-key="t-menu-config-usuarios">
|
||||
Usuarios
|
||||
</a>
|
||||
</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>
|
||||
</div>
|
||||
</li> <!-- end Dashboard Menu -->
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
@ -89,9 +89,11 @@
|
||||
<span class="d-none d-xl-inline-block ms-1 fw-medium user-name-text"
|
||||
th:text="${currentUser.firstName + ' ' + currentUser.lastName}">
|
||||
</span>
|
||||
<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}"
|
||||
th:text="#{${'t-role.' + currentUser.role} ?: ${currentUser.role}}">
|
||||
<span class="d-none d-xl-block ms-1 fs-12 text-muted user-name-sub-text">
|
||||
<span th:each="r : ${currentUser.roles}"
|
||||
th:attr="data-key=${'t-role.' + r.name}"
|
||||
th:text="#{${'t-role.' + r.name}} + ' '">
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user