mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Add admin password reset option for users (#5180)
## Summary - add backend support for admins to reset user passwords and optionally email notifications when SMTP is enabled - surface mail capability in admin settings data for the UI - add a shared change-password modal hooked into People and Team user actions with random password generation and email options ## Testing - not run (not requested) ------ [Codex Task](https://chatgpt.com/codex/tasks/task_b_6934b978fe3c83289b5b95dec79b3d38) --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent
c980ee10c0
commit
9c03914edd
+3
-1
@@ -328,6 +328,7 @@ public class ProprietaryUIDataController {
|
||||
data.setGrandfatheredUserCount(grandfatheredCount);
|
||||
data.setLicenseMaxUsers(licenseMaxUsers);
|
||||
data.setPremiumEnabled(premiumEnabled);
|
||||
data.setMailEnabled(applicationProperties.getMail().isEnabled());
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
@@ -376,7 +377,7 @@ public class ProprietaryUIDataController {
|
||||
data.setUsername(username);
|
||||
data.setRole(user.get().getRolesAsString());
|
||||
data.setSettings(settingsJson);
|
||||
data.setChangeCredsFlag(user.get().isFirstLogin());
|
||||
data.setChangeCredsFlag(user.get().isFirstLogin() || user.get().isForcePasswordChange());
|
||||
data.setOAuth2Login(isOAuth2Login);
|
||||
data.setSaml2Login(isSaml2Login);
|
||||
|
||||
@@ -510,6 +511,7 @@ public class ProprietaryUIDataController {
|
||||
private int grandfatheredUserCount;
|
||||
private int licenseMaxUsers;
|
||||
private boolean premiumEnabled;
|
||||
private boolean mailEnabled;
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
+77
@@ -7,6 +7,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -18,6 +19,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.transaction.Transactional;
|
||||
@@ -236,6 +238,8 @@ public class UserController {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(Map.of("error", "incorrectPassword", "message", "Incorrect password"));
|
||||
}
|
||||
// Set flags before changing password so they're saved together
|
||||
user.setForcePasswordChange(false);
|
||||
userService.changePassword(user, newPassword);
|
||||
userService.changeFirstUse(user, false);
|
||||
// Logout using Spring's utility
|
||||
@@ -584,6 +588,79 @@ public class UserController {
|
||||
return ResponseEntity.ok(Map.of("message", "User role updated successfully"));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@PostMapping("/admin/changePasswordForUser")
|
||||
public ResponseEntity<?> changePasswordForUser(
|
||||
@RequestParam(name = "username") String username,
|
||||
@RequestParam(name = "newPassword", required = false) String newPassword,
|
||||
@RequestParam(name = "generateRandom", defaultValue = "false") boolean generateRandom,
|
||||
@RequestParam(name = "sendEmail", defaultValue = "false") boolean sendEmail,
|
||||
@RequestParam(name = "includePassword", defaultValue = "false") boolean includePassword,
|
||||
@RequestParam(name = "forcePasswordChange", defaultValue = "false")
|
||||
boolean forcePasswordChange,
|
||||
HttpServletRequest request,
|
||||
Authentication authentication)
|
||||
throws SQLException, UnsupportedProviderException, MessagingException {
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("error", "User not found."));
|
||||
}
|
||||
|
||||
String currentUsername = authentication.getName();
|
||||
if (currentUsername.equalsIgnoreCase(username)) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Map.of("error", "Cannot change your own password."));
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
String finalPassword = newPassword;
|
||||
if (generateRandom) {
|
||||
finalPassword = UUID.randomUUID().toString().replace("-", "").substring(0, 12);
|
||||
}
|
||||
|
||||
if (finalPassword == null || finalPassword.trim().isEmpty()) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Map.of("error", "New password is required."));
|
||||
}
|
||||
|
||||
// Set force password change flag before changing password so both are saved together
|
||||
user.setForcePasswordChange(forcePasswordChange);
|
||||
userService.changePassword(user, finalPassword);
|
||||
|
||||
// Invalidate all active sessions to force reauthentication
|
||||
userService.invalidateUserSessions(username);
|
||||
|
||||
if (sendEmail) {
|
||||
if (emailService.isEmpty() || !applicationProperties.getMail().isEnabled()) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Map.of("error", "Email is not configured."));
|
||||
}
|
||||
|
||||
String userEmail = user.getUsername();
|
||||
// Check if username is a valid email format
|
||||
if (userEmail == null || userEmail.isBlank() || !userEmail.contains("@")) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(
|
||||
Map.of(
|
||||
"error",
|
||||
"User's email is not a valid email address. Notifications are disabled."));
|
||||
}
|
||||
|
||||
String loginUrl = buildLoginUrl(request);
|
||||
emailService
|
||||
.get()
|
||||
.sendPasswordChangedNotification(
|
||||
userEmail,
|
||||
user.getUsername(),
|
||||
includePassword ? finalPassword : null,
|
||||
loginUrl);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of("message", "User password updated successfully"));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@PostMapping("/admin/changeUserEnabled/{username}")
|
||||
public ResponseEntity<?> changeUserEnabled(
|
||||
|
||||
@@ -59,6 +59,9 @@ public class User implements UserDetails, Serializable {
|
||||
@Column(name = "hasCompletedInitialSetup")
|
||||
private Boolean hasCompletedInitialSetup = false;
|
||||
|
||||
@Column(name = "forcePasswordChange")
|
||||
private Boolean forcePasswordChange = false;
|
||||
|
||||
@Column(name = "roleName")
|
||||
private String roleName;
|
||||
|
||||
@@ -117,6 +120,14 @@ public class User implements UserDetails, Serializable {
|
||||
this.hasCompletedInitialSetup = hasCompletedInitialSetup;
|
||||
}
|
||||
|
||||
public boolean isForcePasswordChange() {
|
||||
return forcePasswordChange != null && forcePasswordChange;
|
||||
}
|
||||
|
||||
public void setForcePasswordChange(boolean forcePasswordChange) {
|
||||
this.forcePasswordChange = forcePasswordChange;
|
||||
}
|
||||
|
||||
public void setAuthenticationType(AuthenticationType authenticationType) {
|
||||
this.authenticationType = authenticationType.toString().toLowerCase();
|
||||
}
|
||||
|
||||
+49
@@ -223,4 +223,53 @@ public class EmailService {
|
||||
|
||||
sendPlainEmail(to, subject, body, true);
|
||||
}
|
||||
|
||||
@Async
|
||||
public void sendPasswordChangedNotification(
|
||||
String to, String username, String newPassword, String loginUrl) throws MessagingException {
|
||||
String subject = "Your Stirling PDF password has been updated";
|
||||
|
||||
String passwordSection =
|
||||
newPassword == null
|
||||
? ""
|
||||
: """
|
||||
<div style=\"background-color: #f8f9fa; border-left: 4px solid #007bff; padding: 15px; margin: 20px 0; border-radius: 4px;\">
|
||||
<p style=\"margin: 0;\"><strong>Temporary Password:</strong> %s</p>
|
||||
</div>
|
||||
"""
|
||||
.formatted(newPassword);
|
||||
|
||||
String body =
|
||||
"""
|
||||
<html><body style=\"margin: 0; padding: 0;\">
|
||||
<div style=\"font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px;\">
|
||||
<div style=\"max-width: 600px; margin: auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; border: 1px solid #e0e0e0;\">
|
||||
<div style=\"text-align: center; padding: 20px; background-color: #222;\">
|
||||
<img src=\"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling-transparent.svg\" alt=\"Stirling PDF\" style=\"max-height: 60px;\">
|
||||
</div>
|
||||
<div style=\"padding: 30px; color: #333;\">
|
||||
<h2 style=\"color: #222; margin-top: 0;\">Your password was changed</h2>
|
||||
<p>Hello %s,</p>
|
||||
<p>An administrator has updated the password for your Stirling PDF account.</p>
|
||||
%s
|
||||
<p>If you did not expect this change, please contact your administrator immediately.</p>
|
||||
<div style=\"text-align: center; margin: 30px 0;\">
|
||||
<a href=\"%s\" style=\"display: inline-block; background-color: #007bff; color: #ffffff; padding: 14px 28px; text-decoration: none; border-radius: 5px; font-weight: bold;\">Go to Stirling PDF</a>
|
||||
</div>
|
||||
<p style=\"font-size: 14px; color: #666;\">Or copy and paste this link in your browser:</p>
|
||||
<div style=\"background-color: #f8f9fa; padding: 12px; margin: 15px 0; border-radius: 4px; word-break: break-all; font-size: 13px; color: #555;\">
|
||||
%s
|
||||
</div>
|
||||
</div>
|
||||
<div style=\"text-align: center; padding: 15px; font-size: 12px; color: #777; background-color: #f0f0f0;\">
|
||||
© 2025 Stirling PDF. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
"""
|
||||
.formatted(username, passwordSection, loginUrl, loginUrl);
|
||||
|
||||
sendPlainEmail(to, subject, body, true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user