AuthController.java
package com.stocks.stockease.security.web;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
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;
import com.stocks.stockease.shared.ApiResponse;
import com.stocks.stockease.security.User;
import com.stocks.stockease.security.internal.UserRepository;
import com.stocks.stockease.security.JwtUtil;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
/**
* REST controller for user authentication.
*
* <p>Exposes a single login endpoint that validates credentials via Spring Security
* and issues a signed JWT for subsequent API requests. Contract defined in
* {@code docs/api/paths/auth-login.yaml}.
*/
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
/**
* Authenticates the supplied credentials and returns a signed JWT.
*
* <p>Behavior is defined in {@code docs/api/paths/auth-login.yaml},
* operation {@code loginUser}.
*
* @param loginRequest username and password payload
* @return {@link ApiResponse} wrapping the JWT string on success (HTTP 200),
* or an error message on failure (HTTP 400, 401, or 500)
* @throws org.springframework.security.authentication.BadCredentialsException
* if the password does not match — caught and mapped to HTTP 401
* @throws UsernameNotFoundException if the account does not exist — mapped to HTTP 401
*/
@PostMapping("/login")
public ResponseEntity<ApiResponse<String>> login(@Valid @RequestBody LoginRequest loginRequest) {
try {
if (loginRequest.getUsername().isBlank() || loginRequest.getPassword().isBlank()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ApiResponse<>(false, "Username and password cannot be blank", null));
}
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
User user = userRepository.findByUsername(loginRequest.getUsername())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
String token = jwtUtil.generateToken(user.getUsername(), user.getRole());
return ResponseEntity.ok(new ApiResponse<>(true, "Login successful", token));
} catch (UsernameNotFoundException e) {
// Generic message prevents user enumeration (do not reveal whether the username exists)
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ApiResponse<>(false, e.getMessage(), null));
} catch (org.springframework.security.authentication.BadCredentialsException e) {
// Generic message prevents user enumeration (do not reveal which field was wrong)
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ApiResponse<>(false, "Invalid username or password", null));
} catch (RuntimeException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ApiResponse<>(false, "An unexpected error occurred", null));
}
}
}