Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions RestroHub-FrontEnd/src/pages/public/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,18 +170,29 @@ const Login = () => {
const result = res.data;

if (result.success) {
const { accessToken, refreshToken, roles } = result.data;
const {
accessToken,
refreshToken,
roles,
isResetRequire
} = result.data;

localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
localStorage.setItem("roles", JSON.stringify(roles));

if (isResetRequire) {
toast.error("Password expired. Please reset your password.");
navigate("/forgot-password");
return;
}

axios.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`;

toast.success("Login successful!");

navigate("/admin/dashboard");
} else {
}else {
toast.error(result.message || "Login failed");
}
} catch (err) {
Expand Down
1 change: 1 addition & 0 deletions RestroHub/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
logs/
2 changes: 2 additions & 0 deletions RestroHub/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,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') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,39 @@ public ResponseEntity<ApiResponse<AuthResponse>> refreshToken(

return ResponseEntity.ok(ApiResponse.success(authResponse, "Token refreshed successfully"));
}
@PostMapping("/forgot-password")
public ResponseEntity<String> 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<String> resetPassword(
@RequestParam String email,
@RequestParam(required = false) 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");
}
Comment thread
ARYAN-MISHRA-2006 marked this conversation as resolved.
if(token != null && token.trim().isEmpty()) {
return ResponseEntity.badRequest().body("Invalid token");
}
authService.resetPassword(email, token, newPassword);
return ResponseEntity.ok("Password reset successful");
}

@PostMapping("/logout")
@Operation(
summary = "Logout user",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public class AuthResponse {

@Schema(description = "User roles", example = "[\"ROLE_ADMIN\"]")
private List<String> roles;

@Schema(description = "Indicates whether password reset is required")
private Boolean isResetRequire;

@Schema(description = "Token issue timestamp")
@Builder.Default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ public interface AuthService {
AuthResponse refreshToken(RefreshTokenRequest refreshTokenRequest);

void logout(String token);

void forgotPassword(String email);

void resetPassword(String email, String token, String newPassword);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,52 @@
import java.util.Collections;
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.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;

@Service
@RequiredArgsConstructor
@Slf4j
public class AuthServiceImpl implements AuthService {
private static class TokenData {
private final String token;
private final LocalDateTime expiryTime;

private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
public TokenData(String token, LocalDateTime expiryTime) {
this.token = token;
this.expiryTime = expiryTime;
}

public String getToken() {
return token;
}

public LocalDateTime getExpiryTime() {
return expiryTime;
}
}

private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final EmailService emailService;

private final Map<String, TokenData> resetTokenCache = new ConcurrentHashMap<>();

@Value("${reset.expiry.threshold.days:90}")
private int resetExpiryThresholdDays;

@Override
public AuthResponse login(LoginRequest loginRequest) {
Expand All @@ -57,6 +92,16 @@ public AuthResponse login(LoginRequest loginRequest) {
SecurityContextHolder.getContext().setAuthentication(authentication);

UserDetails userDetails = (UserDetails) authentication.getPrincipal();
User user = userRepository.findByEmail(userDetails.getUsername())
.orElseThrow(() -> new RuntimeException("User not found"));

boolean isResetRequired = false;

if (user.getResetPassExpiryDate() != null &&
LocalDateTime.now().isAfter(user.getResetPassExpiryDate())) {

isResetRequired = true;
}
String accessToken = jwtTokenProvider.generateAccessToken(userDetails);
String refreshToken = jwtTokenProvider.generateRefreshToken(userDetails);

Expand All @@ -73,6 +118,7 @@ public AuthResponse login(LoginRequest loginRequest) {
.expiresIn(jwtTokenProvider.getExpirationInSeconds())
.username(userDetails.getUsername())
.roles(roles)
.isResetRequire(isResetRequired)
.build();

} catch (BadCredentialsException ex) {
Expand Down Expand Up @@ -134,28 +180,65 @@ public void logout(String token) {
@Override
public AuthResponse register(RegisterRequest registerRequest) {

if (userRepository.findByEmail(registerRequest.getEmail()).isPresent()) {
throw new DuplicateResourceException("User already exists with this email");
}
User user = userRepository.findByEmail(email)
.orElse(null);
if (user == null) {
return;
}

User user = User.builder()
.name(registerRequest.getFirstName() + " " + registerRequest.getLastName())
.email(registerRequest.getEmail())
.password(passwordEncoder.encode(registerRequest.getPassword()))
.isActive(true)
.isLocked(false)
.authProvider("LOCAL")
.build();
String token = String.format("%06d", new Random().nextInt(999999));

Role customerRole = roleRepository.findByName("CUSTOMER")
.orElseThrow(() -> new RuntimeException("Default CUSTOMER role not found"));
LocalDateTime expiryTime = LocalDateTime.now().plusMinutes(10);

resetTokenCache.put(
email,
new TokenData(token, expiryTime)
);

user.setRoles(new ArrayList<>(Collections.singletonList(customerRole)));

userRepository.save(user);

return AuthResponse.builder()
.username(user.getEmail())
.build();
}
@Override
public void resetPassword(String email, String token, String newPassword) {

User user = userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("User not found"));

if (passwordEncoder.matches(newPassword, user.getPassword())) {
throw new RuntimeException("New password cannot be same as current password");
}

if (token != null && !token.trim().isEmpty()) {

TokenData tokenData = resetTokenCache.get(email);

if (tokenData == null) {
throw new RuntimeException("Invalid or expired token");
}

if (LocalDateTime.now().isAfter(tokenData.getExpiryTime())) {
resetTokenCache.remove(email);
throw new RuntimeException("Token expired, please regenerate token");
}

if (!tokenData.getToken().equals(token)) {
throw new RuntimeException("Invalid token");
}

resetTokenCache.remove(email);
}

user.setPassword(passwordEncoder.encode(newPassword));

user.setResetPassExpiryDate(
LocalDateTime.now().plusDays(resetExpiryThresholdDays)
);

userRepository.save(user);

System.out.println("Password reset successful");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.restroly.qrmenu.auth.service;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
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;

@Value("${spring.mail.username}")
private String mailUsername;

public void sendResetEmail(String toEmail, String token) {

SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(mailUsername);
message.setTo(toEmail);
message.setSubject("Password Reset Request");
Comment thread
ARYAN-MISHRA-2006 marked this conversation as resolved.

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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public class User {
@Column(name = "user_password", nullable = false)
private String password;

@Column(name = "reset_pass_expiry_date")
private LocalDateTime resetPassExpiryDate;

@Column(name = "phone_number")
private String phoneNumber;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByEmail(String email);

boolean existsByEmail(String email);

boolean existsByEmailAndUserIdNot(String email, Long userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import org.springframework.beans.factory.annotation.Value;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -42,6 +45,9 @@ public class UserServiceImpl implements UserService {
private final RestaurantRepository restaurantRepository;
private final PasswordEncoder passwordEncoder;

@Value("${reset.expiry.threshold.days:90}")
private int resetExpiryThresholdDays;

// =============================
// REGISTER USER
// =============================
Expand All @@ -66,6 +72,9 @@ public UserResponse registerUser(UserRequest request) {
.phoneNumber(request.getPhone())
.isActive(request.getIsActive() != null ? request.getIsActive() : true)
.isLocked(false)
.resetPassExpiryDate(
LocalDateTime.now().plusDays(resetExpiryThresholdDays)
)
.build();

if (request.getRoleIds() != null && !request.getRoleIds().isEmpty()) {
Expand Down
2 changes: 1 addition & 1 deletion RestroHub/src/main/resources/application-dev.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions RestroHub/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,4 @@ spring.servlet.multipart.max-request-size=10MB
# Service Requests Configuration
# ===============================
service.request.types=CALL_WAITER,REQUEST_BILL
reset.expiry.threshold.days=90