Changeset ae83647
- Timestamp:
- 06/26/26 16:32:12 (42 hours ago)
- Branches:
- master
- Parents:
- fa32d0f
- Files:
-
- 4 added
- 9 edited
-
petify-backend/pom.xml (modified) (1 diff)
-
petify-backend/src/main/java/com/petify/petify/api/AdminClinicApplicationsController.java (modified) (3 diffs)
-
petify-backend/src/main/java/com/petify/petify/api/AuthController.java (modified) (2 diffs)
-
petify-backend/src/main/java/com/petify/petify/dto/ChangePasswordRequest.java (added)
-
petify-backend/src/main/java/com/petify/petify/dto/ForgotPasswordRequest.java (added)
-
petify-backend/src/main/java/com/petify/petify/repo/VetClinicRepository.java (modified) (1 diff)
-
petify-backend/src/main/java/com/petify/petify/service/AuthService.java (modified) (5 diffs)
-
petify-backend/src/main/java/com/petify/petify/service/ClinicApprovalService.java (added)
-
petify-backend/src/main/java/com/petify/petify/service/ClinicCredentialsEmailService.java (added)
-
petify-backend/src/main/resources/application.properties (modified) (1 diff)
-
petify-frontend/src/api/auth.ts (modified) (2 diffs)
-
petify-frontend/src/views/LoginView.vue (modified) (9 diffs)
-
petify-frontend/src/views/ProfileView.vue (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
petify-backend/pom.xml
rfa32d0f rae83647 46 46 <groupId>org.springframework.boot</groupId> 47 47 <artifactId>spring-boot-starter-webmvc</artifactId> 48 </dependency> 49 <dependency> 50 <groupId>org.springframework.boot</groupId> 51 <artifactId>spring-boot-starter-mail</artifactId> 48 52 </dependency> 49 53 -
petify-backend/src/main/java/com/petify/petify/api/AdminClinicApplicationsController.java
rfa32d0f rae83647 1 1 package com.petify.petify.api; 2 2 3 import com.petify.petify.domain.VetClinic;4 3 import com.petify.petify.domain.VetClinicApplication; 5 4 import com.petify.petify.dto.VetClinicApplicationDTO; 6 5 import com.petify.petify.repo.AdminRepository; 7 6 import com.petify.petify.repo.VetClinicApplicationRepository; 8 import com.petify.petify. repo.VetClinicRepository;7 import com.petify.petify.service.ClinicApprovalService; 9 8 import org.springframework.http.HttpStatus; 10 9 import org.springframework.http.ResponseEntity; … … 20 19 21 20 private final VetClinicApplicationRepository applicationRepository; 22 private final VetClinicRepository clinicRepository;23 21 private final AdminRepository adminRepository; 22 private final ClinicApprovalService clinicApprovalService; 24 23 25 24 public AdminClinicApplicationsController(VetClinicApplicationRepository applicationRepository, 26 VetClinicRepository clinicRepository,27 AdminRepository adminRepository) {25 AdminRepository adminRepository, 26 ClinicApprovalService clinicApprovalService) { 28 27 this.applicationRepository = applicationRepository; 29 this.clinicRepository = clinicRepository;30 28 this.adminRepository = adminRepository; 29 this.clinicApprovalService = clinicApprovalService; 31 30 } 32 31 … … 56 55 .body(Map.of("error", "Admin access required")); 57 56 } 58 VetClinicApplication application = applicationRepository.findById(applicationId) 59 .orElseThrow(() -> new RuntimeException("Application not found")); 60 61 application.setStatus("APPROVED"); 62 application.setReviewedAt(LocalDateTime.now()); 63 application.setReviewedBy(adminUserId); 64 application.setDenialReason(null); 65 VetClinicApplication saved = applicationRepository.save(application); 66 67 boolean clinicExists = clinicRepository.findAll().stream() 68 .anyMatch(clinic -> applicationId.equals(clinic.getApplicationId())); 69 if (!clinicExists) { 70 VetClinic clinic = new VetClinic(); 71 clinic.setApplicationId(application.getApplicationId()); 72 clinic.setName(application.getName()); 73 clinic.setEmail(application.getEmail()); 74 clinic.setPhone(application.getPhone()); 75 clinic.setCity(application.getCity()); 76 clinic.setAddress(application.getAddress()); 77 clinicRepository.save(clinic); 78 } 57 VetClinicApplication saved = clinicApprovalService.approveApplication(applicationId, adminUserId); 79 58 80 59 return ResponseEntity.ok(new VetClinicApplicationDTO(saved)); -
petify-backend/src/main/java/com/petify/petify/api/AuthController.java
rfa32d0f rae83647 2 2 3 3 import com.petify.petify.dto.AuthResponse; 4 import com.petify.petify.dto.ChangePasswordRequest; 5 import com.petify.petify.dto.ForgotPasswordRequest; 4 6 import com.petify.petify.dto.LoginRequest; 5 7 import com.petify.petify.dto.SignUpRequest; … … 47 49 } 48 50 51 @PostMapping("/change-password") 52 public ResponseEntity<?> changePassword(@RequestBody ChangePasswordRequest request) { 53 try { 54 authService.changePassword(request.getUserId(), request.getCurrentPassword(), request.getNewPassword()); 55 return ResponseEntity.ok(Map.of("message", "Password changed successfully")); 56 } catch (RuntimeException e) { 57 return ResponseEntity.badRequest() 58 .body(Map.of("message", e.getMessage())); 59 } 60 } 61 62 @PostMapping("/forgot-password") 63 public ResponseEntity<?> forgotPassword(@RequestBody ForgotPasswordRequest request) { 64 try { 65 authService.sendForgotPasswordEmail(request.getIdentifier()); 66 return ResponseEntity.ok(Map.of("message", "If an account matches that username or email, a temporary password has been sent.")); 67 } catch (RuntimeException e) { 68 return ResponseEntity.badRequest() 69 .body(Map.of("message", e.getMessage())); 70 } 71 } 72 49 73 50 74 @GetMapping("/users") -
petify-backend/src/main/java/com/petify/petify/repo/VetClinicRepository.java
rfa32d0f rae83647 10 10 public interface VetClinicRepository extends JpaRepository<VetClinic, Long> { 11 11 java.util.List<VetClinic> findAllByOrderByNameAsc(); 12 Optional<VetClinic> findByApplicationId(Long applicationId); 12 13 Optional<VetClinic> findByUserId(Long userId); 13 14 boolean existsByUserId(Long userId); -
petify-backend/src/main/java/com/petify/petify/service/AuthService.java
rfa32d0f rae83647 22 22 import org.springframework.transaction.annotation.Transactional; 23 23 24 import java.security.SecureRandom; 24 25 import java.time.LocalDateTime; 25 26 import java.util.List; … … 31 32 32 33 private static final Logger logger = LoggerFactory.getLogger(AuthService.class); 34 private static final String PASSWORD_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%"; 35 private static final SecureRandom SECURE_RANDOM = new SecureRandom(); 33 36 34 37 private final UserRepository userRepository; … … 39 42 private final AnalyticsRepository analyticsRepository; 40 43 private final VetClinicRepository vetClinicRepository; 44 private final ClinicCredentialsEmailService credentialsEmailService; 41 45 42 46 public AuthService(UserRepository userRepository, ClientRepository clientRepository, 43 47 OwnerRepository ownerRepository, AdminRepository adminRepository, 44 48 PasswordEncoder passwordEncoder, AnalyticsRepository analyticsRepository, 45 VetClinicRepository vetClinicRepository) { 49 VetClinicRepository vetClinicRepository, 50 ClinicCredentialsEmailService credentialsEmailService) { 46 51 this.userRepository = userRepository; 47 52 this.clientRepository = clientRepository; … … 51 56 this.analyticsRepository = analyticsRepository; 52 57 this.vetClinicRepository = vetClinicRepository; 58 this.credentialsEmailService = credentialsEmailService; 53 59 } 54 60 … … 177 183 isVerified 178 184 ); 185 } 186 187 @Transactional 188 public void changePassword(Long userId, String currentPassword, String newPassword) { 189 if (userId == null) { 190 throw new RuntimeException("User is required"); 191 } 192 if (currentPassword == null || currentPassword.isBlank()) { 193 throw new RuntimeException("Current password is required"); 194 } 195 if (newPassword == null || newPassword.isBlank()) { 196 throw new RuntimeException("New password is required"); 197 } 198 if (newPassword.length() < 8) { 199 throw new RuntimeException("New password must be at least 8 characters long"); 200 } 201 if (newPassword.equals(currentPassword)) { 202 throw new RuntimeException("New password must be different from the current password"); 203 } 204 205 User user = userRepository.findById(userId) 206 .orElseThrow(() -> new RuntimeException("User not found")); 207 208 boolean passwordMatches = passwordEncoder.matches(currentPassword, user.getPassword()); 209 if (!passwordMatches && !currentPassword.equals(user.getPassword())) { 210 throw new RuntimeException("Current password is incorrect"); 211 } 212 213 user.setPassword(passwordEncoder.encode(newPassword)); 214 userRepository.save(user); 215 logger.info("Password changed successfully for user: {}", user.getUsername()); 216 } 217 218 @Transactional 219 public void sendForgotPasswordEmail(String identifier) { 220 if (identifier == null || identifier.isBlank()) { 221 throw new RuntimeException("Username or email is required"); 222 } 223 224 Optional<User> user = userRepository.findByUsernameOrEmail(identifier.trim(), identifier.trim()); 225 if (user.isEmpty()) { 226 logger.info("Password reset requested for unknown identifier: {}", identifier); 227 return; 228 } 229 230 User foundUser = user.get(); 231 String temporaryPassword = generateTemporaryPassword(); 232 foundUser.setPassword(passwordEncoder.encode(temporaryPassword)); 233 userRepository.save(foundUser); 234 235 credentialsEmailService.sendTemporaryPassword( 236 foundUser.getEmail(), 237 foundUser.getFirstName(), 238 foundUser.getUsername(), 239 temporaryPassword 240 ); 241 logger.info("Temporary password sent for user: {}", foundUser.getUsername()); 242 } 243 244 private String generateTemporaryPassword() { 245 StringBuilder password = new StringBuilder(); 246 for (int i = 0; i < 14; i++) { 247 password.append(PASSWORD_CHARS.charAt(SECURE_RANDOM.nextInt(PASSWORD_CHARS.length()))); 248 } 249 return password.toString(); 179 250 } 180 251 -
petify-backend/src/main/resources/application.properties
rfa32d0f rae83647 33 33 spring.flyway.baseline-version=0 34 34 35 # Mail 36 spring.mail.host=${MAIL_HOST:smtp.gmail.com} 37 spring.mail.port=${MAIL_PORT:587} 38 spring.mail.username=${MAIL_USERNAME:} 39 spring.mail.password=${MAIL_PASSWORD:} 40 spring.mail.properties.mail.smtp.auth=true 41 spring.mail.properties.mail.smtp.starttls.enable=true 42 spring.mail.properties.mail.smtp.starttls.required=true 43 management.health.mail.enabled=false 44 35 45 spring.profiles.active=local 36 spring.config.import=optional:file:.env.properties 46 spring.config.import=optional:file:.env.properties,optional:file:petify-backend/.env.properties 37 47 38 48 -
petify-frontend/src/api/auth.ts
rfa32d0f rae83647 24 24 } 25 25 message?: string 26 } 27 28 export interface ChangePasswordRequest { 29 userId: number 30 currentPassword: string 31 newPassword: string 32 } 33 34 export interface ForgotPasswordRequest { 35 identifier: string 26 36 } 27 37 … … 141 151 return { message: data.message || "Registration successful" } 142 152 } 153 154 export async function changePassword(payload: ChangePasswordRequest, options?: { signal?: AbortSignal }): Promise<void> { 155 await postJson<{ message?: string }>('/api/auth/change-password', payload, options) 156 } 157 158 export async function forgotPassword(payload: ForgotPasswordRequest, options?: { signal?: AbortSignal }): Promise<string> { 159 const data = await postJson<{ message?: string }>('/api/auth/forgot-password', payload, options) 160 return data.message || 'If an account matches that username or email, a temporary password has been sent.' 161 } -
petify-frontend/src/views/LoginView.vue
rfa32d0f rae83647 29 29 <div v-if="error" class="alert alert-danger auth-alert" role="alert"> 30 30 <strong class="me-1">Oops.</strong>{{ error }} 31 </div> 32 <div v-if="forgotSuccess" class="alert alert-success auth-alert" role="alert"> 33 {{ forgotSuccess }} 31 34 </div> 32 35 … … 113 116 <!-- Remember me --> 114 117 <div class="d-flex justify-content-between align-items-center mt-2 mb-3"> 115 116 117 <!-- enable later if you implement route -->118 < !-- <RouterLink class="link small accent" to="/forgot">Forgot password?</RouterLink> -->118 <span></span> 119 <button class="link-button small accent" type="button" @click="toggleForgotPassword"> 120 Forgot password? 121 </button> 119 122 </div> 120 123 … … 123 126 <span v-if="loading" class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span> 124 127 {{ loading ? 'Logging in…' : 'Log in' }} 128 </button> 129 </form> 130 131 <form v-if="showForgotPassword" class="forgot-panel mt-4" @submit.prevent="submitForgotPassword"> 132 <label class="form-label" for="forgot-identifier">Username or email</label> 133 <div class="input-group auth-input"> 134 <span class="input-group-text"> 135 <svg width="18" height="18" viewBox="0 0 24 24" fill="none"> 136 <path 137 d="M4 6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 4 17.5v-11Zm2.5-.5a.5.5 0 0 0-.5.5v.8l6 3.7 6-3.7v-.8a.5.5 0 0 0-.5-.5h-11Zm11.5 3.6-5.5 3.4a1 1 0 0 1-1 0L6 9.6v7.9a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9.6Z" 138 fill="currentColor" 139 opacity=".85" 140 /> 141 </svg> 142 </span> 143 <input 144 id="forgot-identifier" 145 v-model.trim="forgotIdentifier" 146 class="form-control" 147 type="text" 148 autocomplete="username" 149 required 150 placeholder="username or email" 151 /> 152 </div> 153 <div v-if="forgotError" class="text-danger small mt-2">{{ forgotError }}</div> 154 <button class="btn btn-outline-primary w-100 mt-3" type="submit" :disabled="forgotLoading"> 155 <span v-if="forgotLoading" class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span> 156 {{ forgotLoading ? 'Sending...' : 'Send temporary password' }} 125 157 </button> 126 158 </form> … … 144 176 import { useRoute, useRouter } from 'vue-router' 145 177 import { useAuthStore } from '../stores/auth' 178 import { forgotPassword } from '../api/auth' 146 179 147 180 const auth = useAuthStore() … … 153 186 const loading = ref(false) 154 187 const error = ref<string | null>(null) 188 const forgotLoading = ref(false) 189 const forgotError = ref<string | null>(null) 190 const forgotSuccess = ref<string | null>(null) 191 const showForgotPassword = ref(false) 192 const forgotIdentifier = ref('') 155 193 156 194 const showPassword = ref(false) … … 159 197 function togglePassword() { 160 198 showPassword.value = !showPassword.value 199 } 200 201 function toggleForgotPassword() { 202 showForgotPassword.value = !showForgotPassword.value 203 forgotError.value = null 204 forgotSuccess.value = null 205 if (showForgotPassword.value && !forgotIdentifier.value) { 206 forgotIdentifier.value = username.value 207 } 161 208 } 162 209 … … 175 222 } finally { 176 223 loading.value = false 224 } 225 } 226 227 async function submitForgotPassword() { 228 forgotError.value = null 229 forgotSuccess.value = null 230 forgotLoading.value = true 231 try { 232 forgotSuccess.value = await forgotPassword({ identifier: forgotIdentifier.value }) 233 showForgotPassword.value = false 234 } catch (e) { 235 forgotError.value = e instanceof Error ? e.message : String(e) 236 } finally { 237 forgotLoading.value = false 177 238 } 178 239 } … … 276 337 } 277 338 339 .forgot-panel { 340 border-top: 1px solid rgba(31, 41, 55, 0.1); 341 padding-top: 1rem; 342 } 343 278 344 /* Input styling */ 279 345 .auth-input .input-group-text { … … 371 437 } 372 438 439 .link-button { 440 border: 0; 441 background: transparent; 442 padding: 0; 443 text-decoration: none; 444 } 445 446 .link-button.accent { 447 color: #ff7a18; 448 font-weight: 600; 449 } 450 451 .link-button.accent:hover { 452 color: #e76610; 453 } 454 373 455 /* Respect reduced motion */ 374 456 @media (prefers-reduced-motion: reduce) { -
petify-frontend/src/views/ProfileView.vue
rfa32d0f rae83647 94 94 </button> 95 95 </li> 96 <li class="nav-item" role="presentation"> 97 <button 98 class="nav-link" 99 :class="{ active: activeTab === 'account' }" 100 @click="activeTab = 'account'" 101 type="button" 102 role="tab" 103 > 104 <i class="bi bi-shield-lock-fill"></i> Account 105 </button> 106 </li> 96 107 </ul> 108 109 <!-- Account Tab --> 110 <div v-if="activeTab === 'account'" class="tab-content-section"> 111 <h2 class="section-title">Account</h2> 112 <form class="password-form" @submit.prevent="submitPasswordChange"> 113 <div v-if="passwordSuccess" class="alert alert-success" role="alert"> 114 {{ passwordSuccess }} 115 </div> 116 <div v-if="passwordError" class="alert alert-danger" role="alert"> 117 {{ passwordError }} 118 </div> 119 120 <div class="mb-3"> 121 <label class="form-label" for="current-password">Current password</label> 122 <input 123 id="current-password" 124 v-model="passwordForm.currentPassword" 125 class="form-control" 126 type="password" 127 autocomplete="current-password" 128 required 129 /> 130 </div> 131 132 <div class="mb-3"> 133 <label class="form-label" for="new-password">New password</label> 134 <input 135 id="new-password" 136 v-model="passwordForm.newPassword" 137 class="form-control" 138 type="password" 139 autocomplete="new-password" 140 minlength="8" 141 required 142 /> 143 </div> 144 145 <div class="mb-4"> 146 <label class="form-label" for="confirm-new-password">Confirm new password</label> 147 <input 148 id="confirm-new-password" 149 v-model="passwordForm.confirmPassword" 150 class="form-control" 151 type="password" 152 autocomplete="new-password" 153 minlength="8" 154 required 155 /> 156 </div> 157 158 <button class="btn btn-primary" type="submit" :disabled="isPasswordSubmitting"> 159 <span v-if="isPasswordSubmitting" class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span> 160 {{ isPasswordSubmitting ? 'Changing...' : 'Change password' }} 161 </button> 162 </form> 163 </div> 97 164 98 165 <!-- Listings Tab --> … … 746 813 type Review, 747 814 } from '../api/reviews' 815 import { changePassword } from '../api/auth' 748 816 749 817 const router = useRouter() 750 818 const auth = useAuthStore() 751 819 752 const activeTab = ref<'listings' | 'pets' | 'create-listing' | 'favorites' | 'appointments' >('listings')820 const activeTab = ref<'listings' | 'pets' | 'create-listing' | 'favorites' | 'appointments' | 'account'>('listings') 753 821 const listings = ref<any[]>([]) 754 822 const pets = ref<any[]>([]) … … 782 850 const petPhotoFile = ref<File | null>(null) 783 851 const petPhotoPreview = ref('') 852 const isPasswordSubmitting = ref(false) 853 const passwordError = ref('') 854 const passwordSuccess = ref('') 855 856 const passwordForm = ref({ 857 currentPassword: '', 858 newPassword: '', 859 confirmPassword: '', 860 }) 784 861 785 862 const newListing = ref({ … … 859 936 // Fall back to petNameMap (from pets list) 860 937 return petNameMap.value[animalId] || 'Unknown Pet' 938 } 939 940 async function submitPasswordChange() { 941 passwordError.value = '' 942 passwordSuccess.value = '' 943 944 if (!auth.user?.userId) { 945 passwordError.value = 'You need to be logged in to change your password.' 946 return 947 } 948 if (passwordForm.value.newPassword.length < 8) { 949 passwordError.value = 'New password must be at least 8 characters long.' 950 return 951 } 952 if (passwordForm.value.newPassword !== passwordForm.value.confirmPassword) { 953 passwordError.value = 'New passwords do not match.' 954 return 955 } 956 957 isPasswordSubmitting.value = true 958 try { 959 await changePassword({ 960 userId: auth.user.userId, 961 currentPassword: passwordForm.value.currentPassword, 962 newPassword: passwordForm.value.newPassword, 963 }) 964 passwordForm.value = { 965 currentPassword: '', 966 newPassword: '', 967 confirmPassword: '', 968 } 969 passwordSuccess.value = 'Password changed successfully.' 970 } catch (error: any) { 971 passwordError.value = error?.message || 'Failed to change password.' 972 } finally { 973 isPasswordSubmitting.value = false 974 } 861 975 } 862 976 … … 1670 1784 } 1671 1785 1786 .password-form { 1787 max-width: 520px; 1788 } 1789 1672 1790 @keyframes fadeIn { 1673 1791 from {
Note:
See TracChangeset
for help on using the changeset viewer.
