Explorar el Código

add change password api (#5)

Daniel Bohry hace 1 mes
padre
commit
2a12c8d6b9

+ 21 - 0
src/main/java/com/danielbohry/authservice/api/UserController.java

@@ -1,7 +1,10 @@
 package com.danielbohry.authservice.api;
 
+import com.danielbohry.authservice.api.dto.AuthenticationResponse;
+import com.danielbohry.authservice.api.dto.PasswordChangeRequest;
 import com.danielbohry.authservice.api.dto.UserResponse;
 import com.danielbohry.authservice.domain.ApplicationUser;
+import com.danielbohry.authservice.service.auth.AuthService;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
@@ -10,6 +13,8 @@ import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.CrossOrigin;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -20,6 +25,8 @@ import org.springframework.web.bind.annotation.RestController;
 @RequestMapping("api/users")
 public class UserController {
 
+    private final AuthService authService;
+
     @GetMapping("current")
     public ResponseEntity<UserResponse> get() {
         SecurityContext context = SecurityContextHolder.getContext();
@@ -30,4 +37,18 @@ public class UserController {
 
         return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
     }
+
+    @PostMapping("change-password")
+    public ResponseEntity<AuthenticationResponse> changePassword(@RequestBody PasswordChangeRequest request) {
+        SecurityContext context = SecurityContextHolder.getContext();
+        Object principal = context.getAuthentication().getPrincipal();
+
+        if (principal instanceof ApplicationUser user) {
+            log.info("Changing password for user [{}]", user.getUsername());
+            var response = authService.changePassword(user.getId(), request.getCurrentPassword(), request.getNewPassword());
+            return ResponseEntity.ok(response);
+        }
+
+        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
+    }
 }

+ 15 - 0
src/main/java/com/danielbohry/authservice/api/dto/PasswordChangeRequest.java

@@ -0,0 +1,15 @@
+package com.danielbohry.authservice.api.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class PasswordChangeRequest {
+
+    private String currentPassword;
+    private String newPassword;
+
+}

+ 15 - 1
src/main/java/com/danielbohry/authservice/service/auth/AuthService.java

@@ -5,6 +5,7 @@ import com.danielbohry.authservice.api.dto.AuthenticationResponse;
 import com.danielbohry.authservice.domain.ApplicationUser;
 import com.danielbohry.authservice.service.user.UserService;
 import lombok.AllArgsConstructor;
+import org.checkerframework.checker.nullness.qual.NonNull;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -35,7 +36,7 @@ public class AuthService implements UserDetailsService {
     }
 
     public AuthenticationResponse signup(AuthenticationRequest request) {
-        UserDetails user = User.builder().username(request.getUsername()).password(passwordEncoder.encode(request.getPassword())).build();
+        UserDetails user = buildUserDetails(request);
         ApplicationUser saved = service.create(convert(user));
         Authentication authentication = jwtService.generateToken(saved);
         return buildResponse(saved.getId(), authentication);
@@ -50,6 +51,19 @@ public class AuthService implements UserDetailsService {
         return buildResponse(user.getId(), authentication);
     }
 
+    public AuthenticationResponse changePassword(String userId, String currentPassword, String newPassword) {
+        ApplicationUser user = service.changePassword(userId, currentPassword, newPassword, passwordEncoder);
+        Authentication authentication = jwtService.generateToken(user);
+        return buildResponse(user.getId(), authentication);
+    }
+
+    private UserDetails buildUserDetails(AuthenticationRequest request) {
+        return User.builder()
+                .username(request.getUsername())
+                .password(passwordEncoder.encode(request.getPassword()))
+                .build();
+    }
+
     private static AuthenticationResponse buildResponse(String id, Authentication authentication) {
         return AuthenticationResponse.builder()
             .id(id)

+ 13 - 0
src/main/java/com/danielbohry/authservice/service/user/UserService.java

@@ -6,6 +6,7 @@ import com.danielbohry.authservice.exceptions.NotFoundException;
 import com.danielbohry.authservice.repository.UserRepository;
 import lombok.AllArgsConstructor;
 import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
@@ -45,6 +46,18 @@ public class UserService {
         return repository.save(applicationUser);
     }
 
+    public ApplicationUser changePassword(String userId, String currentPassword, String newPassword, PasswordEncoder passwordEncoder) {
+        ApplicationUser user = repository.findById(userId)
+            .orElseThrow(() -> new NotFoundException("User not found"));
+
+        if (!passwordEncoder.matches(currentPassword, user.getPassword())) {
+            throw new BadRequestException("Current password is incorrect");
+        }
+
+        user.setPassword(passwordEncoder.encode(newPassword));
+        return repository.save(user);
+    }
+
     private void validateUsername(ApplicationUser applicationUser) {
         boolean exists = repository.existsByUsername(applicationUser.getUsername());
 

+ 203 - 4
src/main/resources/static/index.html

@@ -180,6 +180,70 @@
             color: #666;
         }
 
+        .change-password-section {
+            background: white;
+            padding: 20px;
+            border-radius: 8px;
+            margin-bottom: 15px;
+        }
+
+        .change-password-section h3 {
+            color: #333;
+            margin-bottom: 15px;
+            font-size: 1.1rem;
+        }
+
+        .action-buttons {
+            margin-bottom: 15px;
+        }
+
+        .action-btn {
+            width: 100%;
+            padding: 12px;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            border: none;
+            border-radius: 8px;
+            font-size: 0.95rem;
+            font-weight: 500;
+            cursor: pointer;
+            transition: all 0.3s ease;
+        }
+
+        .action-btn:hover {
+            transform: translateY(-1px);
+            box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+        }
+
+        .form-buttons {
+            display: flex;
+            gap: 10px;
+            margin-top: 15px;
+        }
+
+        .form-buttons .submit-btn {
+            flex: 1;
+            margin: 0;
+        }
+
+        .cancel-btn {
+            flex: 1;
+            padding: 12px;
+            background: #6c757d;
+            color: white;
+            border: none;
+            border-radius: 8px;
+            font-size: 1rem;
+            font-weight: 600;
+            cursor: pointer;
+            transition: all 0.3s ease;
+        }
+
+        .cancel-btn:hover {
+            background: #5a6268;
+            transform: translateY(-1px);
+        }
+
         .logout-btn {
             width: 100%;
             padding: 10px;
@@ -260,6 +324,33 @@
                 <p><strong>Roles:</strong> <span id="currentRoles"></span></p>
                 <p><strong>Token Expires:</strong> <span id="tokenExpiration"></span></p>
             </div>
+
+            <div class="action-buttons">
+                <button class="action-btn" id="showChangePasswordBtn" onclick="toggleChangePasswordForm()">Change Password</button>
+            </div>
+
+            <div class="change-password-section" id="changePasswordSection" style="display: none;">
+                <h3>Change Password</h3>
+                <form id="changePasswordForm">
+                    <div class="form-group">
+                        <label for="currentPassword">Current Password</label>
+                        <input type="password" id="currentPassword" name="currentPassword" required>
+                    </div>
+                    <div class="form-group">
+                        <label for="newPassword">New Password</label>
+                        <input type="password" id="newPassword" name="newPassword" required>
+                    </div>
+                    <div class="form-group">
+                        <label for="confirmPassword">Confirm New Password</label>
+                        <input type="password" id="confirmPassword" name="confirmPassword" required>
+                    </div>
+                    <div class="form-buttons">
+                        <button type="submit" class="submit-btn" id="changePasswordBtn">Change Password</button>
+                    </div>
+                    <div id="changePasswordMessage" class="message" style="display: none;"></div>
+                </form>
+            </div>
+
             <button class="logout-btn" onclick="logout()">Logout</button>
         </div>
     </div>
@@ -314,7 +405,13 @@
                 button.innerHTML = '<span class="loading"></span>Processing...';
                 button.disabled = true;
             } else {
-                button.innerHTML = buttonId.includes('login') ? 'Sign In' : 'Create Account';
+                if (buttonId.includes('login')) {
+                    button.innerHTML = 'Sign In';
+                } else if (buttonId.includes('register')) {
+                    button.innerHTML = 'Create Account';
+                } else if (buttonId.includes('changePassword')) {
+                    button.innerHTML = 'Change Password';
+                }
                 button.disabled = false;
             }
         }
@@ -397,10 +494,65 @@
             }
         });
 
-        function showUserSection() {
-            document.getElementById('authSection').style.display = 'none';
-            document.getElementById('userSection').classList.add('active');
+        document.getElementById('changePasswordForm').addEventListener('submit', async function(e) {
+            e.preventDefault();
+
+            const currentPassword = document.getElementById('currentPassword').value;
+            const newPassword = document.getElementById('newPassword').value;
+            const confirmPassword = document.getElementById('confirmPassword').value;
+
+            // Validate password confirmation
+            if (newPassword !== confirmPassword) {
+                showMessage('changePasswordMessage', 'New passwords do not match.', 'error');
+                return;
+            }
+
+            // Validate password length
+            if (newPassword.length < 6) {
+                showMessage('changePasswordMessage', 'New password must be at least 6 characters long.', 'error');
+                return;
+            }
+
+            setButtonLoading('changePasswordBtn', true);
+            clearPasswordChangeMessages();
+
+            try {
+                const response = await fetch(`${API_BASE_URL}/users/change-password`, {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        'Authorization': `Bearer ${localStorage.getItem('authToken')}`
+                    },
+                    body: JSON.stringify({ currentPassword, newPassword })
+                });
+
+                if (response.ok) {
+                    const data = await response.json();
+
+                    showMessage('changePasswordMessage', 'Password changed successfully! Please log in with your new password.', 'success');
+
+                    // Clear the form and force logout after a brief delay
+                    document.getElementById('changePasswordForm').reset();
+                    logout();
+                } else {
+                    const errorText = await response.text();
+                    showMessage('changePasswordMessage', errorText || 'Failed to change password. Please try again.', 'error');
+                }
+            } catch (error) {
+                console.error('Change password error:', error);
+                showMessage('changePasswordMessage', 'Network error. Please check if the auth service is running.', 'error');
+            } finally {
+                setButtonLoading('changePasswordBtn', false);
+            }
+        });
+
+        function clearPasswordChangeMessages() {
+            const messageElement = document.getElementById('changePasswordMessage');
+            messageElement.style.display = 'none';
+            messageElement.textContent = '';
+        }
 
+        function updateUserDisplay() {
             document.getElementById('currentUsername').textContent = currentUser.username;
             document.getElementById('currentUserId').textContent = maskUserId(currentUser.id);
             document.getElementById('currentRoles').textContent = currentUser.roles.join(', ');
@@ -409,6 +561,12 @@
             document.getElementById('tokenExpiration').textContent = expirationDate.toLocaleString();
         }
 
+        function showUserSection() {
+            document.getElementById('authSection').style.display = 'none';
+            document.getElementById('userSection').classList.add('active');
+            updateUserDisplay();
+        }
+
         function maskUserId(id) {
             const parts = id.split("-");
             return parts[0] + "-****-" + parts[4];
@@ -424,10 +582,51 @@
 
             document.getElementById('loginForm').reset();
             document.getElementById('registerForm').reset();
+
+            // Reset password change form and hide it
+            hideChangePasswordForm();
+
             clearMessages();
 
             switchTab('login');
         }
+
+        function toggleChangePasswordForm() {
+            const formSection = document.getElementById('changePasswordSection');
+            const isVisible = formSection.style.display === 'block';
+
+            if (isVisible) {
+                hideChangePasswordForm();
+            } else {
+                showChangePasswordForm();
+            }
+        }
+
+        function showChangePasswordForm() {
+            const formSection = document.getElementById('changePasswordSection');
+            const actionBtn = document.getElementById('showChangePasswordBtn');
+
+            formSection.style.display = 'block';
+            actionBtn.textContent = 'Hide Change Password';
+
+            // Clear any previous messages
+            clearPasswordChangeMessages();
+
+            // Focus on first input
+            document.getElementById('currentPassword').focus();
+        }
+
+        function hideChangePasswordForm() {
+            const formSection = document.getElementById('changePasswordSection');
+            const actionBtn = document.getElementById('showChangePasswordBtn');
+
+            formSection.style.display = 'none';
+            actionBtn.textContent = 'Change Password';
+
+            // Clear form and messages
+            document.getElementById('changePasswordForm').reset();
+            clearPasswordChangeMessages();
+        }
     </script>
 </body>
 </html>