Changeset ae83647


Ignore:
Timestamp:
06/26/26 16:32:12 (42 hours ago)
Author:
veronika-ils <ilioskaveronika@…>
Branches:
master
Parents:
fa32d0f
Message:

add functionality so that users can change passwords

Files:
4 added
9 edited

Legend:

Unmodified
Added
Removed
  • petify-backend/pom.xml

    rfa32d0f rae83647  
    4646            <groupId>org.springframework.boot</groupId>
    4747            <artifactId>spring-boot-starter-webmvc</artifactId>
     48        </dependency>
     49        <dependency>
     50            <groupId>org.springframework.boot</groupId>
     51            <artifactId>spring-boot-starter-mail</artifactId>
    4852        </dependency>
    4953
  • petify-backend/src/main/java/com/petify/petify/api/AdminClinicApplicationsController.java

    rfa32d0f rae83647  
    11package com.petify.petify.api;
    22
    3 import com.petify.petify.domain.VetClinic;
    43import com.petify.petify.domain.VetClinicApplication;
    54import com.petify.petify.dto.VetClinicApplicationDTO;
    65import com.petify.petify.repo.AdminRepository;
    76import com.petify.petify.repo.VetClinicApplicationRepository;
    8 import com.petify.petify.repo.VetClinicRepository;
     7import com.petify.petify.service.ClinicApprovalService;
    98import org.springframework.http.HttpStatus;
    109import org.springframework.http.ResponseEntity;
     
    2019
    2120    private final VetClinicApplicationRepository applicationRepository;
    22     private final VetClinicRepository clinicRepository;
    2321    private final AdminRepository adminRepository;
     22    private final ClinicApprovalService clinicApprovalService;
    2423
    2524    public AdminClinicApplicationsController(VetClinicApplicationRepository applicationRepository,
    26                                              VetClinicRepository clinicRepository,
    27                                              AdminRepository adminRepository) {
     25                                             AdminRepository adminRepository,
     26                                             ClinicApprovalService clinicApprovalService) {
    2827        this.applicationRepository = applicationRepository;
    29         this.clinicRepository = clinicRepository;
    3028        this.adminRepository = adminRepository;
     29        this.clinicApprovalService = clinicApprovalService;
    3130    }
    3231
     
    5655                        .body(Map.of("error", "Admin access required"));
    5756            }
    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);
    7958
    8059            return ResponseEntity.ok(new VetClinicApplicationDTO(saved));
  • petify-backend/src/main/java/com/petify/petify/api/AuthController.java

    rfa32d0f rae83647  
    22
    33import com.petify.petify.dto.AuthResponse;
     4import com.petify.petify.dto.ChangePasswordRequest;
     5import com.petify.petify.dto.ForgotPasswordRequest;
    46import com.petify.petify.dto.LoginRequest;
    57import com.petify.petify.dto.SignUpRequest;
     
    4749    }
    4850
     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
    4973
    5074    @GetMapping("/users")
  • petify-backend/src/main/java/com/petify/petify/repo/VetClinicRepository.java

    rfa32d0f rae83647  
    1010public interface VetClinicRepository extends JpaRepository<VetClinic, Long> {
    1111    java.util.List<VetClinic> findAllByOrderByNameAsc();
     12    Optional<VetClinic> findByApplicationId(Long applicationId);
    1213    Optional<VetClinic> findByUserId(Long userId);
    1314    boolean existsByUserId(Long userId);
  • petify-backend/src/main/java/com/petify/petify/service/AuthService.java

    rfa32d0f rae83647  
    2222import org.springframework.transaction.annotation.Transactional;
    2323
     24import java.security.SecureRandom;
    2425import java.time.LocalDateTime;
    2526import java.util.List;
     
    3132
    3233    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();
    3336
    3437    private final UserRepository userRepository;
     
    3942    private final AnalyticsRepository analyticsRepository;
    4043    private final VetClinicRepository vetClinicRepository;
     44    private final ClinicCredentialsEmailService credentialsEmailService;
    4145
    4246    public AuthService(UserRepository userRepository, ClientRepository clientRepository,
    4347                      OwnerRepository ownerRepository, AdminRepository adminRepository,
    4448                      PasswordEncoder passwordEncoder, AnalyticsRepository analyticsRepository,
    45                       VetClinicRepository vetClinicRepository) {
     49                      VetClinicRepository vetClinicRepository,
     50                      ClinicCredentialsEmailService credentialsEmailService) {
    4651        this.userRepository = userRepository;
    4752        this.clientRepository = clientRepository;
     
    5156        this.analyticsRepository = analyticsRepository;
    5257        this.vetClinicRepository = vetClinicRepository;
     58        this.credentialsEmailService = credentialsEmailService;
    5359    }
    5460
     
    177183            isVerified
    178184        );
     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();
    179250    }
    180251
  • petify-backend/src/main/resources/application.properties

    rfa32d0f rae83647  
    3333spring.flyway.baseline-version=0
    3434
     35# Mail
     36spring.mail.host=${MAIL_HOST:smtp.gmail.com}
     37spring.mail.port=${MAIL_PORT:587}
     38spring.mail.username=${MAIL_USERNAME:}
     39spring.mail.password=${MAIL_PASSWORD:}
     40spring.mail.properties.mail.smtp.auth=true
     41spring.mail.properties.mail.smtp.starttls.enable=true
     42spring.mail.properties.mail.smtp.starttls.required=true
     43management.health.mail.enabled=false
     44
    3545spring.profiles.active=local
    36 spring.config.import=optional:file:.env.properties
     46spring.config.import=optional:file:.env.properties,optional:file:petify-backend/.env.properties
    3747
    3848
  • petify-frontend/src/api/auth.ts

    rfa32d0f rae83647  
    2424  }
    2525  message?: string
     26}
     27
     28export interface ChangePasswordRequest {
     29  userId: number
     30  currentPassword: string
     31  newPassword: string
     32}
     33
     34export interface ForgotPasswordRequest {
     35  identifier: string
    2636}
    2737
     
    141151  return { message: data.message || "Registration successful" }
    142152}
     153
     154export async function changePassword(payload: ChangePasswordRequest, options?: { signal?: AbortSignal }): Promise<void> {
     155  await postJson<{ message?: string }>('/api/auth/change-password', payload, options)
     156}
     157
     158export 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  
    2929        <div v-if="error" class="alert alert-danger auth-alert" role="alert">
    3030          <strong class="me-1">Oops.</strong>{{ error }}
     31        </div>
     32        <div v-if="forgotSuccess" class="alert alert-success auth-alert" role="alert">
     33          {{ forgotSuccess }}
    3134        </div>
    3235
     
    113116          <!-- Remember me -->
    114117          <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>
    119122          </div>
    120123
     
    123126            <span v-if="loading" class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span>
    124127            {{ 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' }}
    125157          </button>
    126158        </form>
     
    144176import { useRoute, useRouter } from 'vue-router'
    145177import { useAuthStore } from '../stores/auth'
     178import { forgotPassword } from '../api/auth'
    146179
    147180const auth = useAuthStore()
     
    153186const loading = ref(false)
    154187const error = ref<string | null>(null)
     188const forgotLoading = ref(false)
     189const forgotError = ref<string | null>(null)
     190const forgotSuccess = ref<string | null>(null)
     191const showForgotPassword = ref(false)
     192const forgotIdentifier = ref('')
    155193
    156194const showPassword = ref(false)
     
    159197function togglePassword() {
    160198  showPassword.value = !showPassword.value
     199}
     200
     201function 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  }
    161208}
    162209
     
    175222  } finally {
    176223    loading.value = false
     224  }
     225}
     226
     227async 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
    177238  }
    178239}
     
    276337}
    277338
     339.forgot-panel {
     340  border-top: 1px solid rgba(31, 41, 55, 0.1);
     341  padding-top: 1rem;
     342}
     343
    278344/* Input styling */
    279345.auth-input .input-group-text {
     
    371437}
    372438
     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
    373455/* Respect reduced motion */
    374456@media (prefers-reduced-motion: reduce) {
  • petify-frontend/src/views/ProfileView.vue

    rfa32d0f rae83647  
    9494              </button>
    9595            </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>
    96107          </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>
    97164
    98165          <!-- Listings Tab -->
     
    746813  type Review,
    747814} from '../api/reviews'
     815import { changePassword } from '../api/auth'
    748816
    749817const router = useRouter()
    750818const auth = useAuthStore()
    751819
    752 const activeTab = ref<'listings' | 'pets' | 'create-listing' | 'favorites' | 'appointments'>('listings')
     820const activeTab = ref<'listings' | 'pets' | 'create-listing' | 'favorites' | 'appointments' | 'account'>('listings')
    753821const listings = ref<any[]>([])
    754822const pets = ref<any[]>([])
     
    782850const petPhotoFile = ref<File | null>(null)
    783851const petPhotoPreview = ref('')
     852const isPasswordSubmitting = ref(false)
     853const passwordError = ref('')
     854const passwordSuccess = ref('')
     855
     856const passwordForm = ref({
     857  currentPassword: '',
     858  newPassword: '',
     859  confirmPassword: '',
     860})
    784861
    785862const newListing = ref({
     
    859936  // Fall back to petNameMap (from pets list)
    860937  return petNameMap.value[animalId] || 'Unknown Pet'
     938}
     939
     940async 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  }
    861975}
    862976
     
    16701784}
    16711785
     1786.password-form {
     1787  max-width: 520px;
     1788}
     1789
    16721790@keyframes fadeIn {
    16731791  from {
Note: See TracChangeset for help on using the changeset viewer.