mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-29 23:28:51 +00:00
implementado el soft-delete
This commit is contained in:
@ -1,10 +1,16 @@
|
|||||||
package com.imprimelibros.erp.config;
|
package com.imprimelibros.erp.config;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
@ -15,11 +21,19 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
|
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
|
||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
|
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||||
|
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
|
||||||
import com.imprimelibros.erp.users.User;
|
|
||||||
import com.imprimelibros.erp.users.UserDao;
|
import com.imprimelibros.erp.users.UserDao;
|
||||||
import com.imprimelibros.erp.users.UserDetailsImpl;
|
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
@ -32,12 +46,16 @@ public class SecurityConfig {
|
|||||||
// ========== Beans base ==========
|
// ========== Beans base ==========
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public UserDetailsService userDetailsService(UserDao repo) {
|
public UserDetailsService userDetailsService(UserDao repo, MessageSource messages) {
|
||||||
return username -> {
|
return username -> repo
|
||||||
User u = repo.findByUserNameAndEnabledTrue(username);
|
.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(username) // <-- @EntityGraph aplicado
|
||||||
if (u == null) throw new UsernameNotFoundException("No existe: " + username);
|
.map(UserDetailsImpl::new) // dentro del ctor precalculamos authorities
|
||||||
return new UserDetailsImpl(u);
|
.orElseThrow(() -> {
|
||||||
};
|
var locale = org.springframework.context.i18n.LocaleContextHolder.getLocale();
|
||||||
|
String msg = messages.getMessage("usuarios.error.no-activo", null,
|
||||||
|
"Usuario no activo o no existe", locale);
|
||||||
|
return new org.springframework.security.core.userdetails.UsernameNotFoundException(msg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ -45,84 +63,131 @@ public class SecurityConfig {
|
|||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remember-me (tabla persistent_logins)
|
||||||
|
@Bean
|
||||||
|
public PersistentTokenRepository persistentTokenRepository() {
|
||||||
|
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
|
||||||
|
repo.setDataSource(dataSource);
|
||||||
|
// repo.setCreateTableOnStartup(true); // solo 1ª vez si necesitas crear la
|
||||||
|
// tabla
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
|
||||||
// Provider que soporta UsernamePasswordAuthenticationToken
|
// Provider que soporta UsernamePasswordAuthenticationToken
|
||||||
@Bean
|
@Bean
|
||||||
public AuthenticationProvider daoAuthenticationProvider(
|
public AuthenticationProvider daoAuthenticationProvider(
|
||||||
UserDetailsService userDetailsService,
|
UserDetailsService userDetailsService,
|
||||||
PasswordEncoder passwordEncoder) {
|
PasswordEncoder passwordEncoder) {
|
||||||
|
|
||||||
DaoAuthenticationProvider p = new DaoAuthenticationProvider();
|
// ✅ constructor recomendado (sin deprecations)
|
||||||
p.setUserDetailsService(userDetailsService);
|
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
|
||||||
p.setPasswordEncoder(passwordEncoder);
|
provider.setPasswordEncoder(passwordEncoder); // este setter NO está deprecado
|
||||||
return p;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remember-me (tabla persistent_logins)
|
// Provider que soporta UsernamePasswordAuthenticationToken
|
||||||
@Bean
|
private static RequestMatcher pathStartsWith(String... prefixes) {
|
||||||
public PersistentTokenRepository persistentTokenRepository() {
|
return new RequestMatcher() {
|
||||||
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
|
@Override
|
||||||
repo.setDataSource(dataSource);
|
public boolean matches(HttpServletRequest request) {
|
||||||
// repo.setCreateTableOnStartup(true); // solo 1ª vez si necesitas crear la tabla
|
String uri = request.getRequestURI();
|
||||||
return repo;
|
if (uri == null)
|
||||||
|
return false;
|
||||||
|
for (String p : prefixes) {
|
||||||
|
if (uri.startsWith(p))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Filtro de seguridad ==========
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(
|
public SecurityFilterChain securityFilterChain(
|
||||||
HttpSecurity http,
|
HttpSecurity http,
|
||||||
@Value("${security.rememberme.key}") String keyRememberMe,
|
@Value("${security.rememberme.key}") String keyRememberMe,
|
||||||
UserDetailsService userDetailsService,
|
UserDetailsService userDetailsService,
|
||||||
PersistentTokenRepository tokenRepo,
|
PersistentTokenRepository tokenRepo,
|
||||||
AuthenticationProvider daoAuthenticationProvider
|
AuthenticationProvider daoAuthenticationProvider) throws Exception {
|
||||||
) throws Exception {
|
|
||||||
|
|
||||||
http
|
http
|
||||||
// Registra explícitamente el provider para Username/Password
|
.authenticationProvider(daoAuthenticationProvider)
|
||||||
.authenticationProvider(daoAuthenticationProvider)
|
|
||||||
|
|
||||||
.sessionManagement(session -> session
|
.sessionManagement(session -> session
|
||||||
.invalidSessionUrl("/login?expired")
|
.invalidSessionUrl("/login?expired")
|
||||||
.maximumSessions(1)
|
.maximumSessions(1))
|
||||||
)
|
|
||||||
|
|
||||||
.csrf(csrf -> csrf
|
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
|
||||||
.ignoringRequestMatchers("/presupuesto/public/**")
|
.csrf(csrf -> csrf
|
||||||
)
|
.ignoringRequestMatchers(pathStartsWith("/presupuesto/public/")))
|
||||||
|
|
||||||
.authorizeHttpRequests(auth -> auth
|
// ====== RequestCache: sólo navegaciones HTML reales ======
|
||||||
.requestMatchers("/", "/login",
|
.requestCache(rc -> {
|
||||||
"/assets/**", "/css/**", "/js/**", "/images/**",
|
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
||||||
"/public/**", "/presupuesto/public/**",
|
|
||||||
"/error", "/favicon.ico").permitAll()
|
|
||||||
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
)
|
|
||||||
|
|
||||||
.formLogin(login -> login
|
// Navegación HTML (por tipo o por cabecera Accept)
|
||||||
.loginPage("/login").permitAll()
|
RequestMatcher htmlPage = new OrRequestMatcher(
|
||||||
.loginProcessingUrl("/login")
|
new MediaTypeRequestMatcher(MediaType.TEXT_HTML),
|
||||||
.usernameParameter("username")
|
new MediaTypeRequestMatcher(MediaType.APPLICATION_XHTML_XML),
|
||||||
.passwordParameter("password")
|
new RequestHeaderRequestMatcher("Accept", "text/html"));
|
||||||
.defaultSuccessUrl("/", false)
|
|
||||||
.failureUrl("/login?error") // útil para diagnosticar
|
|
||||||
)
|
|
||||||
|
|
||||||
.rememberMe(rm -> rm
|
// No AJAX
|
||||||
.key(keyRememberMe)
|
RequestMatcher nonAjax = new NegatedRequestMatcher(
|
||||||
.rememberMeParameter("remember-me")
|
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
|
||||||
.rememberMeCookieName("IMPRIMELIBROS_REMEMBER")
|
|
||||||
.tokenValiditySeconds(60 * 60 * 24 * 2)
|
|
||||||
.userDetailsService(userDetailsService)
|
|
||||||
.tokenRepository(tokenRepo)
|
|
||||||
)
|
|
||||||
|
|
||||||
.logout(logout -> logout
|
// Excluir sondas .well-known
|
||||||
.logoutUrl("/logout")
|
RequestMatcher notWellKnown = new NegatedRequestMatcher(pathStartsWith("/.well-known/"));
|
||||||
.logoutSuccessUrl("/")
|
|
||||||
.invalidateHttpSession(true)
|
// Excluir estáticos: comunes + tu /assets/**
|
||||||
.deleteCookies("JSESSIONID", "IMPRIMELIBROS_REMEMBER")
|
RequestMatcher notStatic = new AndRequestMatcher(
|
||||||
.permitAll()
|
new NegatedRequestMatcher(PathRequest.toStaticResources().atCommonLocations()),
|
||||||
);
|
new NegatedRequestMatcher(pathStartsWith("/assets/")));
|
||||||
|
|
||||||
|
cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic, notWellKnown));
|
||||||
|
rc.requestCache(cache);
|
||||||
|
})
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
// Aquí usa patrones String (no deprecados)
|
||||||
|
.requestMatchers(
|
||||||
|
"/",
|
||||||
|
"/login",
|
||||||
|
"/assets/**",
|
||||||
|
"/css/**",
|
||||||
|
"/js/**",
|
||||||
|
"/images/**",
|
||||||
|
"/public/**",
|
||||||
|
"/presupuesto/public/**",
|
||||||
|
"/error",
|
||||||
|
"/favicon.ico",
|
||||||
|
"/.well-known/**" // opcional
|
||||||
|
).permitAll()
|
||||||
|
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
||||||
|
.anyRequest().authenticated())
|
||||||
|
|
||||||
|
.formLogin(login -> login
|
||||||
|
.loginPage("/login").permitAll()
|
||||||
|
.loginProcessingUrl("/login")
|
||||||
|
.usernameParameter("username")
|
||||||
|
.passwordParameter("password")
|
||||||
|
.defaultSuccessUrl("/", false) // respeta SavedRequest (ya filtrada)
|
||||||
|
.failureUrl("/login?error"))
|
||||||
|
|
||||||
|
.rememberMe(rm -> rm
|
||||||
|
.key(keyRememberMe)
|
||||||
|
.rememberMeParameter("remember-me")
|
||||||
|
.rememberMeCookieName("IMPRIMELIBROS_REMEMBER")
|
||||||
|
.tokenValiditySeconds(60 * 60 * 24 * 2)
|
||||||
|
.userDetailsService(userDetailsService)
|
||||||
|
.tokenRepository(tokenRepo))
|
||||||
|
|
||||||
|
.logout(logout -> logout
|
||||||
|
.logoutUrl("/logout")
|
||||||
|
.logoutSuccessUrl("/")
|
||||||
|
.invalidateHttpSession(true)
|
||||||
|
.deleteCookies("JSESSIONID", "IMPRIMELIBROS_REMEMBER")
|
||||||
|
.permitAll());
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,72 +0,0 @@
|
|||||||
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 (var o : dt.order) {
|
|
||||||
var col = dt.columns.get(o.column);
|
|
||||||
String field = col != null ? col.name : null;
|
|
||||||
|
|
||||||
// Acepta solo columnas válidas: no vacías, marcadas como orderable y en la
|
|
||||||
// whitelist "searchable"
|
|
||||||
if (field == null || field.isBlank())
|
|
||||||
continue;
|
|
||||||
if (!col.orderable)
|
|
||||||
continue;
|
|
||||||
if (!searchableFields.contains(field))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
orders.add(new Sort.Order(
|
|
||||||
"desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC,
|
|
||||||
field));
|
|
||||||
}
|
|
||||||
if (!orders.isEmpty()) {
|
|
||||||
sort = Sort.by(orders);
|
|
||||||
} else {
|
|
||||||
// Fallback: primera columna de dt.columns que sea orderable y esté en la
|
|
||||||
// whitelist
|
|
||||||
for (var c : dt.columns) {
|
|
||||||
if (c != null && c.orderable && c.name != null && !c.name.isBlank()
|
|
||||||
&& searchableFields.contains(c.name)) {
|
|
||||||
sort = Sort.by(c.name);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Si no hay ninguna válida, sort se queda UNSORTED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,6 +7,14 @@ import jakarta.persistence.GenerationType;
|
|||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "roles")
|
@Table(name = "roles")
|
||||||
public class Role {
|
public class Role {
|
||||||
@ -19,6 +27,11 @@ public class Role {
|
|||||||
@Column(name = "name")
|
@Column(name = "name")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@SQLRestriction("deleted = false")
|
||||||
|
@OneToMany(mappedBy = "role", fetch = FetchType.LAZY)
|
||||||
|
private Set<UserRole> usersLink = new HashSet<>();
|
||||||
|
|
||||||
public Role() {
|
public Role() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,6 +55,14 @@ public class Role {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<UserRole> getUsersLink() {
|
||||||
|
return usersLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsersLink(Set<UserRole> usersLink) {
|
||||||
|
this.usersLink = usersLink;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Role{" + "id=" + id + ", name='" + name + '\'' + '}';
|
return "Role{" + "id=" + id + ", name='" + name + '\'' + '}';
|
||||||
|
|||||||
@ -8,10 +8,23 @@ import java.util.Set;
|
|||||||
|
|
||||||
import org.hibernate.annotations.Formula;
|
import org.hibernate.annotations.Formula;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "users", uniqueConstraints = {
|
@Table(name = "users", uniqueConstraints = {
|
||||||
@UniqueConstraint(name = "uk_users_username", columnNames = "username")
|
@UniqueConstraint(name = "uk_users_username", columnNames = "username")
|
||||||
})
|
})
|
||||||
|
@SQLRestriction("deleted = false")
|
||||||
public class User {
|
public class User {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@ -35,32 +48,42 @@ public class User {
|
|||||||
@Column(name = "enabled")
|
@Column(name = "enabled")
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
|
|
||||||
@ManyToMany(fetch = FetchType.EAGER)
|
@Column(name = "deleted", nullable = false)
|
||||||
@JoinTable(name = "users_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
|
private boolean deleted = false;
|
||||||
private Set<Role> roles = new java.util.HashSet<>();
|
|
||||||
|
@Column(name = "deleted_at")
|
||||||
|
private LocalDateTime deletedAt;
|
||||||
|
|
||||||
|
@Column(name = "deleted_by")
|
||||||
|
private Long deletedBy;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true)
|
||||||
|
@SQLRestriction("deleted = false")
|
||||||
|
@JsonIgnore
|
||||||
|
private Set<UserRole> rolesLink = new HashSet<>();
|
||||||
|
|
||||||
// SUPERADMIN=3, ADMIN=2, USER=1 (ajusta a tus nombres reales)
|
// SUPERADMIN=3, ADMIN=2, USER=1 (ajusta a tus nombres reales)
|
||||||
@Formula("""
|
@Formula("""
|
||||||
(
|
(
|
||||||
select coalesce(max(
|
select coalesce(max(
|
||||||
case r.name
|
case r.name
|
||||||
when 'SUPERADMIN' then 3
|
when 'SUPERADMIN' then 3
|
||||||
when 'ADMIN' then 2
|
when 'ADMIN' then 2
|
||||||
else 1
|
else 1
|
||||||
end
|
end
|
||||||
), 0)
|
), 0)
|
||||||
from users_roles ur
|
from users_roles ur
|
||||||
join roles r on r.id = ur.role_id
|
join roles r on r.id = ur.role_id
|
||||||
where ur.user_id = id
|
where ur.user_id = id
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
private Integer roleRank;
|
private Integer roleRank;
|
||||||
|
|
||||||
@Formula("""
|
@Formula("""
|
||||||
(select group_concat(lower(r.name) order by r.name separator ', ')
|
(select group_concat(lower(r.name) order by r.name separator ', ')
|
||||||
from users_roles ur join roles r on r.id = ur.role_id
|
from users_roles ur join roles r on r.id = ur.role_id
|
||||||
where ur.user_id = id)
|
where ur.user_id = id)
|
||||||
""")
|
""")
|
||||||
private String rolesConcat;
|
private String rolesConcat;
|
||||||
|
|
||||||
/* Constructors */
|
/* Constructors */
|
||||||
@ -75,12 +98,12 @@ public class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public User(String fullName, String userName, String password, boolean enabled,
|
public User(String fullName, String userName, String password, boolean enabled,
|
||||||
Set<Role> roles) {
|
Set<UserRole> roles) {
|
||||||
this.fullName = fullName;
|
this.fullName = fullName;
|
||||||
this.userName = userName;
|
this.userName = userName;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
this.roles = roles;
|
this.rolesLink = roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Getters and Setters */
|
/* Getters and Setters */
|
||||||
@ -125,12 +148,63 @@ public class User {
|
|||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
public Set<Role> getRoles() {
|
public Set<Role> getRoles() {
|
||||||
return roles;
|
return rolesLink.stream()
|
||||||
|
.filter(ur -> !ur.isDeleted())
|
||||||
|
.map(UserRole::getRole)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRoles(Set<Role> roles) {
|
@JsonProperty("roles")
|
||||||
this.roles = roles;
|
public List<String> getRoleNames() {
|
||||||
|
return this.getRoles().stream()
|
||||||
|
.map(Role::getName)
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.map(String::trim)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoles(Set<Role> desired) {
|
||||||
|
if (desired == null)
|
||||||
|
desired = Collections.emptySet();
|
||||||
|
|
||||||
|
// 1) ids deseados
|
||||||
|
Set<Long> desiredIds = desired.stream()
|
||||||
|
.map(Role::getId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 2) Soft-delete de vínculos activos que ya no se desean
|
||||||
|
this.rolesLink.stream()
|
||||||
|
.filter(ur -> !ur.isDeleted() && !desiredIds.contains(ur.getRole().getId()))
|
||||||
|
.forEach(UserRole::softDelete);
|
||||||
|
|
||||||
|
// 3) Para cada rol deseado: si hay vínculo borrado => reactivar; si no existe
|
||||||
|
// => crear
|
||||||
|
for (Role role : desired) {
|
||||||
|
// ya activo
|
||||||
|
boolean activeExists = this.rolesLink.stream()
|
||||||
|
.anyMatch(ur -> !ur.isDeleted() && ur.getRole().getId().equals(role.getId()));
|
||||||
|
if (activeExists)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// existe borrado => reactivar
|
||||||
|
Optional<UserRole> deletedLink = this.rolesLink.stream()
|
||||||
|
.filter(ur -> ur.isDeleted() && ur.getRole().getId().equals(role.getId()))
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
if (deletedLink.isPresent()) {
|
||||||
|
UserRole ur = deletedLink.get();
|
||||||
|
ur.setDeleted(false);
|
||||||
|
ur.setDeletedAt(null);
|
||||||
|
} else {
|
||||||
|
// crear nuevo vínculo
|
||||||
|
UserRole ur = new UserRole(this, role);
|
||||||
|
this.rolesLink.add(ur);
|
||||||
|
// si tienes la colección inversa en Role:
|
||||||
|
role.getUsersLink().add(ur);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getRoleRank() {
|
public Integer getRoleRank() {
|
||||||
@ -141,15 +215,46 @@ public class User {
|
|||||||
return rolesConcat;
|
return rolesConcat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isDeleted() {
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeleted(boolean deleted) {
|
||||||
|
this.deleted = deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getDeletedAt() {
|
||||||
|
return deletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||||
|
this.deletedAt = deletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getDeletedBy() {
|
||||||
|
return deletedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeletedBy(Long deletedBy) {
|
||||||
|
this.deletedBy = deletedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<UserRole> getRolesLink() {
|
||||||
|
return rolesLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRolesLink(Set<UserRole> rolesLink) {
|
||||||
|
this.rolesLink = rolesLink;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "User{" +
|
return "User{" +
|
||||||
"id=" + id +
|
"id=" + id +
|
||||||
", fullName='" + fullName + '\'' +
|
", fullName='" + fullName + '\'' +
|
||||||
", userName='" + userName + '\'' +
|
", userName='" + userName + '\'' +
|
||||||
", password='" + password + '\'' +
|
|
||||||
", enabled=" + enabled +
|
", enabled=" + enabled +
|
||||||
", roles=" + roles +
|
", roles=" + getRoles() +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import org.springframework.data.jpa.domain.Specification;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
@ -33,6 +34,7 @@ import com.imprimelibros.erp.datatables.DataTable;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
@ -66,18 +68,17 @@ public class UserController {
|
|||||||
public String list(Model model, Authentication authentication, Locale locale) {
|
public String list(Model model, Authentication authentication, Locale locale) {
|
||||||
|
|
||||||
List<String> keys = List.of(
|
List<String> keys = List.of(
|
||||||
"usuarios.delete.title",
|
"usuarios.delete.title",
|
||||||
"usuarios.delete.text",
|
"usuarios.delete.text",
|
||||||
"usuarios.eliminar",
|
"usuarios.eliminar",
|
||||||
"usuarios.delete.button",
|
"usuarios.delete.button",
|
||||||
"app.yes",
|
"app.yes",
|
||||||
"app.cancelar",
|
"app.cancelar",
|
||||||
"usuarios.delete.ok.title",
|
"usuarios.delete.ok.title",
|
||||||
"usuarios.delete.ok.text"
|
"usuarios.delete.ok.text");
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
model.addAttribute("languageBundle", translations);
|
model.addAttribute("languageBundle", translations);
|
||||||
|
|
||||||
return "imprimelibros/users/users-list";
|
return "imprimelibros/users/users-list";
|
||||||
}
|
}
|
||||||
@ -86,7 +87,8 @@ public class UserController {
|
|||||||
// método con @ResponseBody.
|
// método con @ResponseBody.
|
||||||
@GetMapping(value = "/datatable", produces = "application/json")
|
@GetMapping(value = "/datatable", produces = "application/json")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public DataTablesResponse<Map<String, Object>> datatable(HttpServletRequest request, Locale locale) {
|
public DataTablesResponse<Map<String, Object>> datatable(HttpServletRequest request, Authentication authentication,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
DataTablesRequest dt = DataTablesParser.from(request); //
|
DataTablesRequest dt = DataTablesParser.from(request); //
|
||||||
|
|
||||||
@ -94,8 +96,9 @@ public class UserController {
|
|||||||
// Si 'role' es relación, sácalo de aquí:
|
// Si 'role' es relación, sácalo de aquí:
|
||||||
List<String> searchable = List.of("fullName", "userName", "enabled", "rolesConcat"); // <- busca por roles de
|
List<String> searchable = List.of("fullName", "userName", "enabled", "rolesConcat"); // <- busca por roles de
|
||||||
// verdad
|
// verdad
|
||||||
List<String> orderable = List.of("id", "fullName", "userName", "enabled", "roleRank"); // <- permite ordenar por estas
|
List<String> orderable = List.of("id", "fullName", "userName", "enabled", "roleRank"); // <- permite ordenar por
|
||||||
// columnas
|
// estas
|
||||||
|
// columnas
|
||||||
|
|
||||||
Specification<User> base = (root, query, cb) -> cb.conjunction();
|
Specification<User> base = (root, query, cb) -> cb.conjunction();
|
||||||
long total = repo.count();
|
long total = repo.count();
|
||||||
@ -121,12 +124,27 @@ public class UserController {
|
|||||||
messageSource.getMessage("usuarios.rol." + rol, null, locale) + "</span>")
|
messageSource.getMessage("usuarios.rol." + rol, null, locale) + "</span>")
|
||||||
.collect(Collectors.joining(" ")))
|
.collect(Collectors.joining(" ")))
|
||||||
.add("actions", (user) -> {
|
.add("actions", (user) -> {
|
||||||
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
|
||||||
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
boolean isSuperAdmin = authentication.getAuthorities().stream()
|
||||||
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n" +
|
.anyMatch(a -> a.getAuthority().equals("ROLE_SUPERADMIN"));
|
||||||
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
|
||||||
+ "\" class=\"link-danger btn-delete-user fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>\n" +
|
if (!isSuperAdmin) {
|
||||||
" </div>";
|
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
||||||
|
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
||||||
|
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
|
||||||
|
+
|
||||||
|
" </div>";
|
||||||
|
} else {
|
||||||
|
// Admin editando otro admin o usuario normal: puede editarse y eliminarse
|
||||||
|
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
||||||
|
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
||||||
|
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
|
||||||
|
+
|
||||||
|
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
||||||
|
+ "\" class=\"link-danger btn-delete-user fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>\n"
|
||||||
|
+
|
||||||
|
" </div>";
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.where(base)
|
.where(base)
|
||||||
// Filtros custom:
|
// Filtros custom:
|
||||||
@ -295,28 +313,38 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public ResponseEntity<?> delete(@PathVariable Long id, Authentication authentication, Locale locale) {
|
@Transactional
|
||||||
|
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
|
||||||
return repo.findById(id).map(u -> {
|
return repo.findById(id).map(u -> {
|
||||||
|
if (auth != null && u.getUserName().equalsIgnoreCase(auth.getName())) {
|
||||||
if (authentication != null && u.getUserName().equalsIgnoreCase(authentication.getName())) {
|
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
.body(Map.of("message", messageSource.getMessage("usuarios.error.delete-self", null, locale)));
|
.body(Map.of("message", messageSource.getMessage("usuarios.error.delete-self", null, locale)));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
repo.delete(u);
|
Long currentUserId = null;
|
||||||
return ResponseEntity.status(HttpStatus.OK).body(
|
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||||
Map.of("message", messageSource.getMessage("usuarios.exito.eliminado", null, locale))
|
currentUserId = udi.getId();
|
||||||
);
|
} else if (auth != null) {
|
||||||
} catch (DataIntegrityViolationException dive) {
|
currentUserId = repo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null); // fallback
|
||||||
// Restricción FK / dependencias
|
}
|
||||||
return ResponseEntity.status(HttpStatus.CONFLICT)
|
|
||||||
.body(Map.of("message", messageSource.getMessage("usuarios.error.delete-relational-data", null, locale)));
|
u.setDeleted(true);
|
||||||
|
u.setDeletedAt(LocalDateTime.now());
|
||||||
|
u.setDeletedBy(currentUserId);
|
||||||
|
|
||||||
|
// Soft-delete de los vínculos (si usas cascade REMOVE + @SQLDelete en UserRole,
|
||||||
|
// podrías omitir este foreach y dejar que JPA lo haga)
|
||||||
|
u.getRolesLink().forEach(UserRole::softDelete);
|
||||||
|
|
||||||
|
repo.save(u); // ← NO delete(); guardamos el soft delete con deleted_by relleno
|
||||||
|
return ResponseEntity.ok(Map.of("message",
|
||||||
|
messageSource.getMessage("usuarios.exito.eliminado", null, locale)));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
.body(Map.of("message", messageSource.getMessage("usuarios.error.delete-internal-error", null, locale)));
|
.body(Map.of("message",
|
||||||
|
messageSource.getMessage("usuarios.error.delete-internal-error", null, locale)));
|
||||||
}
|
}
|
||||||
}).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
|
}).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||||
.body(Map.of("message", messageSource.getMessage("usuarios.error.delete-not-found", null, locale))));
|
.body(Map.of("message", messageSource.getMessage("usuarios.error.not-found", null, locale))));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,16 +2,47 @@ package com.imprimelibros.erp.users;
|
|||||||
|
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import org.springframework.data.jpa.repository.EntityGraph;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface UserDao extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
|
public interface UserDao extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
|
||||||
|
|
||||||
User findByUserNameAndEnabledTrue(String userName);
|
// Aplicamos EntityGraph a la versión con Specification+Pageable
|
||||||
|
@Override
|
||||||
|
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||||
|
@NonNull
|
||||||
|
Page<User> findAll(@Nullable Specification<User> spec, @NonNull Pageable pageable);
|
||||||
|
|
||||||
Optional<User> findByUserNameIgnoreCase(String userName);
|
Optional<User> findByUserNameIgnoreCase(String userName);
|
||||||
|
|
||||||
boolean existsByUserNameIgnoreCase(String userName);
|
boolean existsByUserNameIgnoreCase(String userName);
|
||||||
|
|
||||||
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
||||||
|
|
||||||
|
// Nuevo: para login/negocio "activo"
|
||||||
|
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||||
|
Optional<User> findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName);
|
||||||
|
|
||||||
|
// Para poder restaurar, necesitas leer ignorando @Where (native):
|
||||||
|
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
|
||||||
|
Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
|
||||||
|
|
||||||
|
@Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true)
|
||||||
|
List<User> findAllDeleted();
|
||||||
|
|
||||||
|
@Query("select u.id from User u where lower(u.userName) = lower(:userName)")
|
||||||
|
Optional<Long> findIdByUserNameIgnoreCase(@Param("userName") String userName);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,7 @@ 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.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adaptador de la entidad User a Spring Security.
|
* Adaptador de la entidad User a Spring Security.
|
||||||
@ -15,21 +12,22 @@ import java.util.stream.Collectors;
|
|||||||
public class UserDetailsImpl implements UserDetails {
|
public class UserDetailsImpl implements UserDetails {
|
||||||
|
|
||||||
private final User user;
|
private final User user;
|
||||||
|
private final java.util.Collection<? extends GrantedAuthority> authorities;
|
||||||
|
|
||||||
public UserDetailsImpl(User user) {
|
public UserDetailsImpl(User user) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
this.authorities = user.getRoles().stream()
|
||||||
|
.map(Role::getName)
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.map(String::trim)
|
||||||
|
.map(String::toUpperCase)
|
||||||
|
.map(name -> new SimpleGrantedAuthority("ROLE_" + name))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
public java.util.Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
// Si tu User tiene un Set<Role>:
|
return authorities; // no volvemos a tocar user.getRoles() fuera de sesión
|
||||||
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
|
@Override
|
||||||
@ -47,7 +45,6 @@ public class UserDetailsImpl implements UserDetails {
|
|||||||
return user.getFullName();
|
return user.getFullName();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👇 si en la vista usas principal.role, añade este también
|
|
||||||
public String getRole() {
|
public String getRole() {
|
||||||
return user.getRoles().stream()
|
return user.getRoles().stream()
|
||||||
.map(r -> r.getName()) // "ADMIN", "USER", ...
|
.map(r -> r.getName()) // "ADMIN", "USER", ...
|
||||||
@ -79,10 +76,15 @@ public class UserDetailsImpl implements UserDetails {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return user.isEnabled();
|
return user.isEnabled() && !user.isDeleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
public User getUser() {
|
public User getUser() {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return user.getId();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
121
src/main/java/com/imprimelibros/erp/users/UserRole.java
Normal file
121
src/main/java/com/imprimelibros/erp/users/UserRole.java
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import org.hibernate.annotations.SQLDelete;
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "users_roles", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(name = "ux_users_roles_active", columnNames = { "user_id", "role_id", "deleted" })
|
||||||
|
})
|
||||||
|
@SQLDelete(sql = "UPDATE users_roles SET deleted = TRUE, deleted_at = NOW() WHERE id = ?")
|
||||||
|
@SQLRestriction("deleted = false")
|
||||||
|
public class UserRole {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
// FK a users
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "user_id", nullable = false)
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
// FK a roles
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "role_id", nullable = false)
|
||||||
|
private Role role;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean deleted = false;
|
||||||
|
|
||||||
|
@Column(name = "deleted_at")
|
||||||
|
private LocalDateTime deletedAt;
|
||||||
|
|
||||||
|
protected UserRole() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserRole(User user, Role role) {
|
||||||
|
this.user = user;
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- helpers ---- */
|
||||||
|
public void softDelete() {
|
||||||
|
this.deleted = true;
|
||||||
|
this.deletedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o)
|
||||||
|
return true;
|
||||||
|
if (!(o instanceof UserRole other))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Long u1 = this.getUser() != null ? this.getUser().getId() : null;
|
||||||
|
Long r1 = this.getRole() != null ? this.getRole().getId() : null;
|
||||||
|
Long u2 = other.getUser() != null ? other.getUser().getId() : null;
|
||||||
|
Long r2 = other.getRole() != null ? other.getRole().getId() : null;
|
||||||
|
|
||||||
|
// igualdad por clave lógica (user_id, role_id) cuando existen
|
||||||
|
if (u1 != null && r1 != null && u2 != null && r2 != null) {
|
||||||
|
return u1.equals(u2) && r1.equals(r2);
|
||||||
|
}
|
||||||
|
// fallback: identidad por id si está asignado
|
||||||
|
if (this.getId() != null && other.getId() != null) {
|
||||||
|
return this.getId().equals(other.getId());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
Long u = this.getUser() != null ? this.getUser().getId() : null;
|
||||||
|
Long r = this.getRole() != null ? this.getRole().getId() : null;
|
||||||
|
if (u != null && r != null) {
|
||||||
|
return java.util.Objects.hash(u, r);
|
||||||
|
}
|
||||||
|
return java.util.Objects.hash(getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- getters/setters ---- */
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(User user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Role getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRole(Role role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDeleted() {
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeleted(boolean deleted) {
|
||||||
|
this.deleted = deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getDeletedAt() {
|
||||||
|
return deletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||||
|
this.deletedAt = deletedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,5 +3,5 @@ package com.imprimelibros.erp.users;
|
|||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
|
||||||
public interface UserService extends UserDetailsService {
|
public interface UserService extends UserDetailsService {
|
||||||
public User findByUserName(String userName);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,44 +1,22 @@
|
|||||||
package com.imprimelibros.erp.users;
|
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.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserServiceImpl implements UserService {
|
public class UserServiceImpl implements UserService {
|
||||||
|
|
||||||
private UserDao userDao;
|
private UserDao userDao;
|
||||||
private RoleDao roleDao;
|
|
||||||
|
|
||||||
public UserServiceImpl(UserDao userDao, RoleDao roleDao) {
|
public UserServiceImpl(UserDao userDao) {
|
||||||
this.userDao = userDao;
|
this.userDao = userDao;
|
||||||
this.roleDao = roleDao;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public User findByUserName(String userName) {
|
public UserDetails loadUserByUsername(String username) {
|
||||||
// check the database if the user already exists
|
User user = userDao.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(username)
|
||||||
return userDao.findByUserNameAndEnabledTrue(userName);
|
.orElseThrow(() -> new UsernameNotFoundException("No existe usuario activo: " + 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);
|
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,8 @@ usuarios.error.delete-internal-error=No se puede eliminar: error interno.
|
|||||||
usuarios.error.delete-not-found=No se puede eliminar: usuario no encontrado.
|
usuarios.error.delete-not-found=No se puede eliminar: usuario no encontrado.
|
||||||
usuarios.error.delete-self=No se puede eliminar a sí mismo.
|
usuarios.error.delete-self=No se puede eliminar a sí mismo.
|
||||||
|
|
||||||
|
usuarios.error.no-activo=No existe un usuario activo con este correo electrónico.
|
||||||
|
|
||||||
usuarios.exito.creado=Usuario creado con éxito.
|
usuarios.exito.creado=Usuario creado con éxito.
|
||||||
usuarios.exito.actualizado=Usuario actualizado con éxito.
|
usuarios.exito.actualizado=Usuario actualizado con éxito.
|
||||||
usuarios.exito.eliminado=Usuario eliminado con éxito.
|
usuarios.exito.eliminado=Usuario eliminado con éxito.
|
||||||
|
|||||||
Reference in New Issue
Block a user