mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-02-05 02:28:52 +00:00
Compare commits
6 Commits
b2026f1cab
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 06a3521f6b | |||
| ecf1472f58 | |||
| 48993a34c4 | |||
| a0bf8552f1 | |||
| 562dc2b231 | |||
| 9a49ccf6b8 |
13107
logs/erp.log
13107
logs/erp.log
File diff suppressed because one or more lines are too long
@ -149,6 +149,10 @@ public class SecurityConfig {
|
||||
"/pagos/redsys/**"
|
||||
)
|
||||
.permitAll()
|
||||
.requestMatchers("/impersonate/exit")
|
||||
.hasRole("PREVIOUS_ADMINISTRATOR")
|
||||
.requestMatchers("/impersonate")
|
||||
.hasAnyRole("SUPERADMIN", "ADMIN")
|
||||
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
||||
.anyRequest().authenticated())
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@ import java.util.Optional;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -63,6 +65,8 @@ import jakarta.validation.Valid;
|
||||
@RequestMapping("/presupuesto")
|
||||
public class PresupuestoController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PresupuestoController.class);
|
||||
|
||||
private final PresupuestoRepository presupuestoRepository;
|
||||
|
||||
@Autowired
|
||||
@ -824,6 +828,7 @@ public class PresupuestoController {
|
||||
return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"),
|
||||
"message", messageSource.getMessage("presupuesto.exito.guardado", null, locale)));
|
||||
} catch (Exception ex) {
|
||||
log.error("Error al guardar el presupuesto", ex);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.error.save-internal-error", null, locale),
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
package com.imprimelibros.erp.users;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
import com.imprimelibros.erp.config.Sanitizer;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
@Controller
|
||||
public class ImpersonationController {
|
||||
|
||||
private static final String PREVIOUS_ADMIN_ROLE = "ROLE_PREVIOUS_ADMINISTRATOR";
|
||||
private static final String SESSION_ATTR = "IMPERSONATOR_AUTH";
|
||||
|
||||
private final UserService userService;
|
||||
private final Sanitizer sanitizer;
|
||||
|
||||
public ImpersonationController(UserService userService, Sanitizer sanitizer) {
|
||||
this.userService = userService;
|
||||
this.sanitizer = sanitizer;
|
||||
}
|
||||
|
||||
@PostMapping("/impersonate")
|
||||
@PreAuthorize("hasRole('ADMIN') or hasRole('SUPERADMIN')")
|
||||
public String impersonate(
|
||||
@RequestParam("username") String username,
|
||||
Authentication authentication,
|
||||
HttpServletRequest request) {
|
||||
|
||||
if (authentication == null) {
|
||||
return "redirect:/login";
|
||||
}
|
||||
|
||||
if (hasRole(authentication, PREVIOUS_ADMIN_ROLE)) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
String normalized = sanitizer.plain(username);
|
||||
if (normalized == null || normalized.isBlank()) {
|
||||
return "redirect:/users";
|
||||
}
|
||||
normalized = normalized.trim().toLowerCase();
|
||||
|
||||
if (authentication.getName() != null
|
||||
&& authentication.getName().equalsIgnoreCase(normalized)) {
|
||||
return "redirect:/users";
|
||||
}
|
||||
|
||||
UserDetails target;
|
||||
try {
|
||||
target = userService.loadUserByUsername(normalized);
|
||||
} catch (UsernameNotFoundException ex) {
|
||||
throw new AccessDeniedException("No autorizado");
|
||||
}
|
||||
|
||||
boolean currentIsSuperAdmin = hasRole(authentication, "ROLE_SUPERADMIN");
|
||||
boolean targetIsSuperAdmin = target.getAuthorities().stream()
|
||||
.anyMatch(a -> "ROLE_SUPERADMIN".equals(a.getAuthority()));
|
||||
if (targetIsSuperAdmin && !currentIsSuperAdmin) {
|
||||
throw new AccessDeniedException("No autorizado");
|
||||
}
|
||||
|
||||
HttpSession session = request.getSession(true);
|
||||
if (session.getAttribute(SESSION_ATTR) == null) {
|
||||
session.setAttribute(SESSION_ATTR, authentication);
|
||||
}
|
||||
|
||||
List<GrantedAuthority> authorities = new ArrayList<>(target.getAuthorities());
|
||||
authorities.add(new SimpleGrantedAuthority(PREVIOUS_ADMIN_ROLE));
|
||||
|
||||
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(
|
||||
target, target.getPassword(), authorities);
|
||||
newAuth.setDetails(authentication.getDetails());
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(newAuth);
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@PostMapping("/impersonate/exit")
|
||||
@PreAuthorize("hasRole('PREVIOUS_ADMINISTRATOR')")
|
||||
public String exit(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
Object previous = session.getAttribute(SESSION_ATTR);
|
||||
if (previous instanceof Authentication previousAuth) {
|
||||
SecurityContextHolder.getContext().setAuthentication(previousAuth);
|
||||
} else {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
session.removeAttribute(SESSION_ATTR);
|
||||
}
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
private static boolean hasRole(Authentication auth, String role) {
|
||||
return auth != null
|
||||
&& auth.getAuthorities().stream()
|
||||
.anyMatch(a -> role.equals(a.getAuthority()));
|
||||
}
|
||||
}
|
||||
155
src/main/java/com/imprimelibros/erp/users/ProfileController.java
Normal file
155
src/main/java/com/imprimelibros/erp/users/ProfileController.java
Normal file
@ -0,0 +1,155 @@
|
||||
package com.imprimelibros.erp.users;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import com.imprimelibros.erp.config.Sanitizer;
|
||||
import com.imprimelibros.erp.users.validation.ProfileForm;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/pages-profile")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public class ProfileController {
|
||||
|
||||
private final UserDao userDao;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final MessageSource messageSource;
|
||||
private final Sanitizer sanitizer;
|
||||
|
||||
public ProfileController(UserDao userDao, PasswordEncoder passwordEncoder,
|
||||
MessageSource messageSource, Sanitizer sanitizer) {
|
||||
this.userDao = userDao;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.messageSource = messageSource;
|
||||
this.sanitizer = sanitizer;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String view(
|
||||
Authentication authentication,
|
||||
@RequestParam(name = "success", required = false) String success,
|
||||
Model model,
|
||||
Locale locale) {
|
||||
|
||||
if (authentication == null) {
|
||||
return "redirect:/login";
|
||||
}
|
||||
|
||||
User user = userDao.findByUserNameIgnoreCase(authentication.getName()).orElse(null);
|
||||
if (user == null) {
|
||||
return "redirect:/login";
|
||||
}
|
||||
|
||||
ProfileForm form = new ProfileForm();
|
||||
form.setId(user.getId());
|
||||
form.setFullName(user.getFullName());
|
||||
form.setUserName(user.getUserName());
|
||||
|
||||
model.addAttribute("user", form);
|
||||
model.addAttribute("success", success != null);
|
||||
return "imprimelibros/users/profile";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String update(
|
||||
Authentication authentication,
|
||||
@Validated @ModelAttribute("user") ProfileForm form,
|
||||
BindingResult binding,
|
||||
Model model,
|
||||
RedirectAttributes redirectAttributes,
|
||||
Locale locale) {
|
||||
|
||||
if (authentication == null) {
|
||||
return "redirect:/login";
|
||||
}
|
||||
|
||||
User user = userDao.findByUserNameIgnoreCase(authentication.getName()).orElse(null);
|
||||
if (user == null) {
|
||||
return "redirect:/login";
|
||||
}
|
||||
|
||||
String normalized = sanitizer.plain(form.getUserName());
|
||||
if (normalized != null) {
|
||||
normalized = normalized.trim().toLowerCase();
|
||||
}
|
||||
|
||||
if (normalized == null || normalized.isBlank()) {
|
||||
binding.rejectValue("userName", "usuarios.error.email",
|
||||
messageSource.getMessage("usuarios.error.email", null, locale));
|
||||
} else if (userDao.existsByUserNameIgnoreCaseAndIdNot(normalized, user.getId())) {
|
||||
binding.rejectValue("userName", "usuarios.error.duplicado",
|
||||
messageSource.getMessage("usuarios.error.duplicado", null, locale));
|
||||
}
|
||||
|
||||
String cleanName = sanitizer.plain(form.getFullName());
|
||||
if (cleanName == null || cleanName.isBlank()) {
|
||||
binding.rejectValue("fullName", "usuarios.error.nombre",
|
||||
messageSource.getMessage("usuarios.error.nombre", null, locale));
|
||||
}
|
||||
|
||||
boolean wantsPasswordChange = hasText(form.getCurrentPassword())
|
||||
|| hasText(form.getNewPassword())
|
||||
|| hasText(form.getConfirmPassword());
|
||||
|
||||
if (wantsPasswordChange) {
|
||||
if (!hasText(form.getCurrentPassword())) {
|
||||
binding.rejectValue("currentPassword", "usuarios.error.password.actual",
|
||||
messageSource.getMessage("usuarios.error.password.actual", null, locale));
|
||||
} else if (!passwordEncoder.matches(form.getCurrentPassword(), user.getPassword())) {
|
||||
binding.rejectValue("currentPassword", "usuarios.error.password.actual.incorrecta",
|
||||
messageSource.getMessage("usuarios.error.password.actual.incorrecta", null, locale));
|
||||
}
|
||||
|
||||
if (!hasText(form.getNewPassword())) {
|
||||
binding.rejectValue("newPassword", "usuarios.error.password.nueva.requerida",
|
||||
messageSource.getMessage("usuarios.error.password.nueva.requerida", null, locale));
|
||||
} else if (form.getNewPassword().length() < 6) {
|
||||
binding.rejectValue("newPassword", "usuarios.error.password.min",
|
||||
messageSource.getMessage("usuarios.error.password.min", null, locale));
|
||||
}
|
||||
|
||||
if (!hasText(form.getConfirmPassword())) {
|
||||
binding.rejectValue("confirmPassword", "usuarios.error.confirmPassword.requerida",
|
||||
messageSource.getMessage("usuarios.error.confirmPassword.requerida", null, locale));
|
||||
} else if (hasText(form.getNewPassword()) && !form.getNewPassword().equals(form.getConfirmPassword())) {
|
||||
binding.rejectValue("confirmPassword", "usuarios.error.password-coinciden",
|
||||
messageSource.getMessage("usuarios.error.password-coinciden", null, locale));
|
||||
}
|
||||
}
|
||||
|
||||
if (binding.hasErrors()) {
|
||||
model.addAttribute("success", false);
|
||||
return "imprimelibros/users/profile";
|
||||
}
|
||||
|
||||
user.setFullName(cleanName.trim());
|
||||
user.setUserName(normalized);
|
||||
|
||||
if (wantsPasswordChange) {
|
||||
user.setPassword(passwordEncoder.encode(form.getNewPassword()));
|
||||
}
|
||||
|
||||
userDao.save(user);
|
||||
|
||||
redirectAttributes.addAttribute("success", "1");
|
||||
return "redirect:/pages-profile";
|
||||
}
|
||||
|
||||
private static boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
}
|
||||
@ -81,6 +81,9 @@ public class UserController {
|
||||
"usuarios.delete.button",
|
||||
"app.yes",
|
||||
"app.cancelar",
|
||||
"usuarios.impersonate.title",
|
||||
"usuarios.impersonate.text",
|
||||
"usuarios.impersonate.button",
|
||||
"usuarios.delete.ok.title",
|
||||
"usuarios.delete.ok.text");
|
||||
|
||||
@ -132,26 +135,36 @@ public class UserController {
|
||||
.collect(Collectors.joining(" ")))
|
||||
.add("actions", (user) -> {
|
||||
|
||||
boolean isSuperAdmin = authentication.getAuthorities().stream()
|
||||
boolean isSuperAdmin = authentication != null && authentication.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_SUPERADMIN"));
|
||||
|
||||
if (!isSuperAdmin) {
|
||||
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>";
|
||||
boolean isSelf = authentication != null
|
||||
&& authentication.getName() != null
|
||||
&& authentication.getName().equalsIgnoreCase(user.getUserName());
|
||||
|
||||
boolean targetIsSuperAdmin = user.getRoles().stream()
|
||||
.anyMatch(r -> "SUPERADMIN".equalsIgnoreCase(r.getName()));
|
||||
|
||||
StringBuilder actions = new StringBuilder();
|
||||
actions.append("<div class=\"hstack gap-3 flex-wrap\">");
|
||||
actions.append("<a href=\"javascript:void(0);\" data-id=\"")
|
||||
.append(user.getId())
|
||||
.append("\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>");
|
||||
|
||||
if (!isSelf && (isSuperAdmin || !targetIsSuperAdmin)) {
|
||||
actions.append("<a href=\"javascript:void(0);\" data-username=\"")
|
||||
.append(user.getUserName())
|
||||
.append("\" class=\"link-info btn-impersonate-user fs-15\"><i class=\"ri-user-shared-line\"></i></a>");
|
||||
}
|
||||
|
||||
if (isSuperAdmin) {
|
||||
actions.append("<a href=\"javascript:void(0);\" data-id=\"")
|
||||
.append(user.getId())
|
||||
.append("\" class=\"link-danger btn-delete-user fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>");
|
||||
}
|
||||
|
||||
actions.append("</div>");
|
||||
return actions.toString();
|
||||
})
|
||||
.where(base)
|
||||
// Filtros custom:
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
package com.imprimelibros.erp.users.validation;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class ProfileForm {
|
||||
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "{usuarios.error.nombre}")
|
||||
private String fullName;
|
||||
|
||||
@NotBlank(message = "{usuarios.error.email}")
|
||||
@Email(message = "{usuarios.error.email.formato}")
|
||||
private String userName;
|
||||
|
||||
private String currentPassword;
|
||||
private String newPassword;
|
||||
private String confirmPassword;
|
||||
|
||||
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 getCurrentPassword() {
|
||||
return currentPassword;
|
||||
}
|
||||
|
||||
public void setCurrentPassword(String currentPassword) {
|
||||
this.currentPassword = currentPassword;
|
||||
}
|
||||
|
||||
public String getNewPassword() {
|
||||
return newPassword;
|
||||
}
|
||||
|
||||
public void setNewPassword(String newPassword) {
|
||||
this.newPassword = newPassword;
|
||||
}
|
||||
|
||||
public String getConfirmPassword() {
|
||||
return confirmPassword;
|
||||
}
|
||||
|
||||
public void setConfirmPassword(String confirmPassword) {
|
||||
this.confirmPassword = confirmPassword;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
spring.application.name=erp
|
||||
# Active profile
|
||||
#spring.profiles.active=dev
|
||||
spring.profiles.active=test
|
||||
spring.profiles.active=dev
|
||||
#spring.profiles.active=test
|
||||
#spring.profiles.active=prod
|
||||
|
||||
|
||||
|
||||
@ -7,3 +7,4 @@ app.guardar=Save
|
||||
app.editar=Edit
|
||||
app.eliminar=Delete
|
||||
app.imprimir=Print
|
||||
app.impersonate.exit=Return to my user
|
||||
|
||||
@ -33,3 +33,4 @@ app.sidebar.gestion-pagos=Gestión de Pagos
|
||||
app.errors.403=No tienes permiso para acceder a esta página.
|
||||
|
||||
app.validation.required=Campo obligatorio
|
||||
app.impersonate.exit=Volver a mi usuario
|
||||
|
||||
@ -1 +1,23 @@
|
||||
usuarios.form.nombre=Full name
|
||||
usuarios.form.email=Email
|
||||
usuarios.form.confirmarPassword=Confirm password
|
||||
usuarios.form.password.actual=Current password
|
||||
usuarios.form.password.nueva=New password
|
||||
usuarios.form.password.nota=You can only change the password if you provide the current one.
|
||||
|
||||
usuarios.error.nombre=Name is required.
|
||||
usuarios.error.email=Email is required.
|
||||
usuarios.error.email.formato=Email is not valid.
|
||||
usuarios.error.password.min=Password must be at least 6 characters.
|
||||
usuarios.error.password.actual=Current password is required.
|
||||
usuarios.error.password.actual.incorrecta=Current password is not correct.
|
||||
usuarios.error.password.nueva.requerida=New password is required.
|
||||
usuarios.error.confirmPassword.requerida=Password confirmation is required.
|
||||
usuarios.error.password-coinciden=Passwords do not match.
|
||||
usuarios.error.duplicado=There is already a user with that email.
|
||||
|
||||
usuarios.impersonate.title=Sign in as user
|
||||
usuarios.impersonate.text=You are about to sign in as <b>{0}</b>. You can return to your user from the menu.
|
||||
usuarios.impersonate.button=Continue
|
||||
usuarios.profile.title=Edit profile
|
||||
usuarios.profile.success=Profile updated successfully.
|
||||
|
||||
@ -20,6 +20,9 @@ usuarios.form.nombre=Nombre completo
|
||||
usuarios.form.email=Correo electrónico
|
||||
usuarios.form.password=Contraseña
|
||||
usuarios.form.confirmarPassword=Confirmar contraseña
|
||||
usuarios.form.password.actual=Contraseña actual
|
||||
usuarios.form.password.nueva=Nueva contraseña
|
||||
usuarios.form.password.nota=Solo podrás cambiar la contraseña si indicas la actual.
|
||||
usuarios.form.rol=Rol
|
||||
usuarios.form.estado=Estado
|
||||
|
||||
@ -37,6 +40,9 @@ usuarios.error.email.formato=El correo electrónico no es válido.
|
||||
usuarios.error.rol=El rol seleccionado no es válido.
|
||||
usuarios.error.password.requerida=La contraseña es obligatoria.
|
||||
usuarios.error.password.min=La contraseña debe tener al menos 6 caracteres.
|
||||
usuarios.error.password.actual=La contraseña actual es obligatoria.
|
||||
usuarios.error.password.actual.incorrecta=La contraseña actual no es correcta.
|
||||
usuarios.error.password.nueva.requerida=La nueva contraseña es obligatoria.
|
||||
usuarios.error.confirmPassword.requerida=La confirmación de la contraseña es obligatoria.
|
||||
usuarios.error.password-coinciden=Las contraseñas no coinciden.
|
||||
usuarios.error.delete-relational-data=No se puede eliminar el usuario porque tiene datos relacionados.
|
||||
@ -54,3 +60,8 @@ usuarios.delete.button=Si, ELIMINAR
|
||||
usuarios.delete.text=¿Está seguro de que desea eliminar al usuario?<br>Esta acción no se puede deshacer.
|
||||
usuarios.delete.ok.title=Usuario eliminado
|
||||
usuarios.delete.ok.text=El usuario ha sido eliminado con éxito.
|
||||
usuarios.impersonate.title=Entrar como usuario
|
||||
usuarios.impersonate.text=Vas a iniciar sesión como <b>{0}</b>. Podrás volver a tu usuario desde el menú.
|
||||
usuarios.impersonate.button=Entrar
|
||||
usuarios.profile.title=Editar perfil
|
||||
usuarios.profile.success=Perfil actualizado correctamente.
|
||||
|
||||
@ -147,6 +147,55 @@ $(() => {
|
||||
});
|
||||
});
|
||||
|
||||
// Botón "Entrar como"
|
||||
$(document).on('click', '.btn-impersonate-user', function (e) {
|
||||
e.preventDefault();
|
||||
const username = $(this).data('username');
|
||||
|
||||
const title = window.languageBundle.get(['usuarios.impersonate.title']) || 'Entrar como usuario';
|
||||
const textTpl = window.languageBundle.get(['usuarios.impersonate.text'])
|
||||
|| 'Vas a iniciar sesión como <b>{0}</b>.';
|
||||
const confirmText = window.languageBundle.get(['usuarios.impersonate.button']) || 'Entrar';
|
||||
|
||||
Swal.fire({
|
||||
title,
|
||||
html: textTpl.replace('{0}', username),
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-info w-xs mt-2',
|
||||
cancelButton: 'btn btn-light w-xs mt-2'
|
||||
},
|
||||
confirmButtonText: confirmText,
|
||||
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
$.ajax({
|
||||
url: '/impersonate',
|
||||
type: 'POST',
|
||||
data: { username },
|
||||
success: function () {
|
||||
window.location.href = '/';
|
||||
},
|
||||
error: function (xhr) {
|
||||
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||
|| 'No se pudo iniciar sesión como ese usuario.';
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'No se pudo suplantar',
|
||||
text: msg,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Submit del form en el modal
|
||||
$(document).on('submit', '#userForm', function (e) {
|
||||
|
||||
@ -100,9 +100,14 @@
|
||||
<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" 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" th:text="#{app.mensajes}">Mensajes</span></a>
|
||||
<div sec:authorize="hasRole('PREVIOUS_ADMINISTRATOR')">
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#"
|
||||
onclick="document.getElementById('exitImpersonationForm').submit(); return false;">
|
||||
<i class="mdi mdi-account-switch text-muted fs-16 align-middle me-1"></i>
|
||||
<span class="align-middle" th:text="#{app.impersonate.exit}">Volver a mi usuario</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#"
|
||||
onclick="document.getElementById('logoutForm').submit(); return false;">
|
||||
@ -127,6 +132,9 @@
|
||||
<form id="logoutForm" th:action="@{/logout}" method="post" class="d-none">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
</form>
|
||||
<form id="exitImpersonationForm" th:action="@{/impersonate/exit}" method="post" class="d-none">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
</form>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
107
src/main/resources/templates/imprimelibros/users/profile.html
Normal file
107
src/main/resources/templates/imprimelibros/users/profile.html
Normal file
@ -0,0 +1,107 @@
|
||||
<!doctype html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{imprimelibros/layout}">
|
||||
|
||||
<head>
|
||||
<th:block layout:fragment="pagetitle" />
|
||||
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
|
||||
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}" />
|
||||
|
||||
<th:block layout:fragment="content">
|
||||
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page" th:text="#{app.perfil}">Perfil</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0" th:text="#{usuarios.profile.title}">Editar perfil</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<div th:if="${success}" class="alert alert-success"
|
||||
th:text="#{usuarios.profile.success}">Perfil actualizado.</div>
|
||||
|
||||
<form id="profileForm" novalidate th:action="@{/pages-profile}" th:object="${user}"
|
||||
method="post">
|
||||
|
||||
<div th:if="${#fields.hasGlobalErrors()}" class="alert alert-danger">
|
||||
<div th:each="e : ${#fields.globalErrors()}" th:text="${e}"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label th:text="#{usuarios.form.nombre}" for="fullName">Nombre</label>
|
||||
<input type="text" class="form-control" id="fullName" th:field="*{fullName}"
|
||||
th:classappend="${#fields.hasErrors('fullName')} ? ' is-invalid'" required>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('fullName')}"
|
||||
th:errors="*{fullName}">Error</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label th:text="#{usuarios.form.email}" for="userName">Correo electrónico</label>
|
||||
<input type="email" class="form-control" id="userName" th:field="*{userName}"
|
||||
th:classappend="${#fields.hasErrors('userName')} ? ' is-invalid'" required>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('userName')}"
|
||||
th:errors="*{userName}">Error</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="mb-3">
|
||||
<label th:text="#{usuarios.form.password.actual}" for="currentPassword">Contraseña actual</label>
|
||||
<input type="password" class="form-control" id="currentPassword"
|
||||
th:field="*{currentPassword}"
|
||||
th:classappend="${#fields.hasErrors('currentPassword')} ? ' is-invalid'">
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('currentPassword')}"
|
||||
th:errors="*{currentPassword}">Error</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label th:text="#{usuarios.form.password.nueva}" for="newPassword">Nueva contraseña</label>
|
||||
<input type="password" class="form-control" id="newPassword"
|
||||
th:field="*{newPassword}"
|
||||
th:classappend="${#fields.hasErrors('newPassword')} ? ' is-invalid'">
|
||||
<div class="text-muted" th:text="#{usuarios.form.password.nota}">
|
||||
Solo podrás cambiar la contraseña si indicas la actual.
|
||||
</div>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('newPassword')}"
|
||||
th:errors="*{newPassword}">Error</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label th:text="#{usuarios.form.confirmarPassword}" for="confirmPassword">Confirmar contraseña</label>
|
||||
<input type="password" class="form-control" id="confirmPassword"
|
||||
th:field="*{confirmPassword}"
|
||||
th:classappend="${#fields.hasErrors('confirmPassword')} ? ' is-invalid'">
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('confirmPassword')}"
|
||||
th:errors="*{confirmPassword}">Error</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-secondary" th:text="#{app.guardar}">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="modal" />
|
||||
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user