diff --git a/RestroHub/.gitignore b/RestroHub/.gitignore new file mode 100644 index 0000000..d9ec3be --- /dev/null +++ b/RestroHub/.gitignore @@ -0,0 +1 @@ +logs/ diff --git a/RestroHub/build.gradle b/RestroHub/build.gradle index 49e56a0..dca39c8 100644 --- a/RestroHub/build.gradle +++ b/RestroHub/build.gradle @@ -81,6 +81,8 @@ dependencies { // Logging (JSON format for production) implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5' implementation 'ch.qos.logback.contrib:logback-jackson:0.1.5' + + implementation 'org.springframework.boot:spring-boot-starter-mail' } tasks.named('test') { diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/auth/controller/AuthController.java b/RestroHub/src/main/java/com/restroly/qrmenu/auth/controller/AuthController.java index fe7bacd..410941c 100644 --- a/RestroHub/src/main/java/com/restroly/qrmenu/auth/controller/AuthController.java +++ b/RestroHub/src/main/java/com/restroly/qrmenu/auth/controller/AuthController.java @@ -115,6 +115,8 @@ public ResponseEntity> login( return ResponseEntity.ok(ApiResponse.success(authResponse, "Login successful")); } + + @PostMapping("/refresh") @Operation( summary = "Refresh access token", @@ -147,7 +149,36 @@ public ResponseEntity> refreshToken( return ResponseEntity.ok(ApiResponse.success(authResponse, "Token refreshed successfully")); } - + @PostMapping("/forgot-password") +public ResponseEntity forgotPassword(@RequestParam String email) { + if(email == null || email.trim().isEmpty()) { + return ResponseEntity.badRequest().body("Email cannot be empty"); +} + +if(!email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) { + return ResponseEntity.badRequest().body("Invalid email format"); +} + authService.forgotPassword(email); + + return ResponseEntity.ok("Reset token generated successfully"); +} + +@PostMapping("/reset-password") +public ResponseEntity resetPassword( + @RequestParam String token, + @RequestParam String newPassword) { + if(newPassword == null || newPassword.trim().isEmpty()) { + return ResponseEntity.badRequest().body("Password cannot be empty"); +} + +if(newPassword.length() < 6) { + return ResponseEntity.badRequest().body("Password must be at least 6 characters"); +} + authService.resetPassword(token, newPassword); + + return ResponseEntity.ok("Password reset successful"); +} + @PostMapping("/logout") @Operation( summary = "Logout user", diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/AuthService.java b/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/AuthService.java index f11e5ba..d89833c 100644 --- a/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/AuthService.java +++ b/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/AuthService.java @@ -11,4 +11,8 @@ public interface AuthService { AuthResponse refreshToken(RefreshTokenRequest refreshTokenRequest); void logout(String token); + + void forgotPassword(String email); + + void resetPassword(String token, String newPassword); } \ No newline at end of file diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/AuthServiceImpl.java b/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/AuthServiceImpl.java index 4928eaf..5a26d6a 100644 --- a/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/AuthServiceImpl.java +++ b/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/AuthServiceImpl.java @@ -21,14 +21,25 @@ import java.util.List; import java.util.stream.Collectors; +import com.restroly.qrmenu.user.entity.User; +import com.restroly.qrmenu.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.UUID; +import org.springframework.security.crypto.password.PasswordEncoder; + @Service @RequiredArgsConstructor @Slf4j public class AuthServiceImpl implements AuthService { + private final AuthenticationManager authenticationManager; private final JwtTokenProvider jwtTokenProvider; private final UserDetailsService userDetailsService; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final EmailService emailService; @Override public AuthResponse login(LoginRequest loginRequest) { @@ -116,4 +127,39 @@ public void logout(String token) { SecurityContextHolder.clearContext(); log.info("User logged out successfully"); } + + @Override + public void forgotPassword(String email) { + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new RuntimeException("User not found")); + + String token = UUID.randomUUID().toString(); + + user.setResetToken(token); + user.setResetTokenExpiry(LocalDateTime.now().plusMinutes(30)); + + userRepository.save(user); + + emailService.sendResetEmail(user.getEmail(), token); + } + @Override +public void resetPassword(String token, String newPassword) { + + User user = userRepository.findByResetToken(token) + .orElseThrow(() -> new RuntimeException("Invalid reset token")); + + if (user.getResetTokenExpiry().isBefore(LocalDateTime.now())) { + throw new RuntimeException("Reset token expired"); + } + + user.setPassword(passwordEncoder.encode(newPassword)); + + user.setResetToken(null); + user.setResetTokenExpiry(null); + + userRepository.save(user); + + System.out.println("Password reset successful"); +} } \ No newline at end of file diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/EmailService.java b/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/EmailService.java new file mode 100644 index 0000000..2079b7e --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/auth/service/EmailService.java @@ -0,0 +1,31 @@ +package com.restroly.qrmenu.auth.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class EmailService { + + private final JavaMailSender mailSender; + + public void sendResetEmail(String toEmail, String token) { + + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(System.getenv("MAIL_USERNAME")); + message.setTo(toEmail); + message.setSubject("Password Reset Request"); + + message.setText( + "Click the link below to reset your password:\n\n" + + "https://example.com/reset-password?token=" + + token + ); + + mailSender.send(message); + + System.out.println("Reset email sent successfully"); + } +} \ No newline at end of file diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/config/SecurityConfig.java b/RestroHub/src/main/java/com/restroly/qrmenu/config/SecurityConfig.java index a08c841..89bf448 100644 --- a/RestroHub/src/main/java/com/restroly/qrmenu/config/SecurityConfig.java +++ b/RestroHub/src/main/java/com/restroly/qrmenu/config/SecurityConfig.java @@ -41,6 +41,8 @@ public class SecurityConfig { "/api/v1/users/register", // Public api endpoints "/public/api/v1/**" + + }; private static final String[] PUBLIC_GET_URLS = { diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/user/entity/User.java b/RestroHub/src/main/java/com/restroly/qrmenu/user/entity/User.java index 4f954f7..72fe598 100644 --- a/RestroHub/src/main/java/com/restroly/qrmenu/user/entity/User.java +++ b/RestroHub/src/main/java/com/restroly/qrmenu/user/entity/User.java @@ -29,6 +29,12 @@ public class User { @Column(name = "user_password", nullable = false) private String password; + @Column(name = "reset_token") + private String resetToken; + + @Column(name = "reset_token_expiry") + private LocalDateTime resetTokenExpiry; + @Column(name = "phone_number") private String phoneNumber; diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/user/repository/UserRepository.java b/RestroHub/src/main/java/com/restroly/qrmenu/user/repository/UserRepository.java index 768cb2e..e9f251a 100644 --- a/RestroHub/src/main/java/com/restroly/qrmenu/user/repository/UserRepository.java +++ b/RestroHub/src/main/java/com/restroly/qrmenu/user/repository/UserRepository.java @@ -16,6 +16,8 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + Optional findByResetToken(String resetToken); + boolean existsByEmail(String email); boolean existsByEmailAndUserIdNot(String email, Long userId); diff --git a/RestroHub/src/main/resources/application-dev.properties b/RestroHub/src/main/resources/application-dev.properties index 959e340..48e1c71 100644 --- a/RestroHub/src/main/resources/application-dev.properties +++ b/RestroHub/src/main/resources/application-dev.properties @@ -3,7 +3,7 @@ # =============================== spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/RestroHub_DB spring.datasource.username=${DB_USERNAME:postgres} -spring.datasource.password=${DB_PASSWORD:postgres} +spring.datasource.password=${DB_PASSWORD:${DB_PASSWORD}} spring.datasource.driver-class-name=org.postgresql.Driver # =============================== # HikariCP @@ -52,4 +52,4 @@ spring.servlet.multipart.max-request-size=10MB security.jwt.secret=${JWT_SECRET:your-256-bit-secret-key-here-change-in-production} security.jwt.expiration=${JWT_EXPIRATION:86400000} security.jwt.refresh-expiration=${JWT_REFRESH_EXPIRATION:604800000} -security.cors.allowed-origins=http://localhost:5173,http://localhost:3000,http://localhost:3002 \ No newline at end of file +security.cors.allowed-origins=http://localhost:5173,http://localhost:3000,http://localhost:3002