diff --git a/demo/src/main/java/com/example/demo/config/SecurityConfig.java b/demo/src/main/java/com/example/demo/config/SecurityConfig.java index 204584b..8e7a372 100644 --- a/demo/src/main/java/com/example/demo/config/SecurityConfig.java +++ b/demo/src/main/java/com/example/demo/config/SecurityConfig.java @@ -1,5 +1,50 @@ package com.example.demo.config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; + +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@EnableWebSecurity public class SecurityConfig { + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + + http + // Uses your CorsConfigurationSource bean from CorsConfig.java + .cors(Customizer.withDefaults()) + + // For now, disable CSRF so you can POST from a separate frontend easily. + // If you later use cookies/sessions, revisit CSRF. + .csrf(csrf -> csrf.disable()) + + // Auth rules + .authorizeHttpRequests(auth -> auth + // Allow health check + auth endpoints without login + .requestMatchers("/api/health").permitAll() + .requestMatchers("/api/auth/**").permitAll() + + // Everything else requires authentication (you can loosen this later) + .anyRequest().authenticated() + ) + + // Temporary: enables basic auth popup in browser tools. + // Later you’ll likely switch to JWT or session login. + .httpBasic(Customizer.withDefaults()); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } } diff --git a/demo/src/main/java/com/example/demo/controller/AuthController.java b/demo/src/main/java/com/example/demo/controller/AuthController.java index 1fdce90..2606b44 100644 --- a/demo/src/main/java/com/example/demo/controller/AuthController.java +++ b/demo/src/main/java/com/example/demo/controller/AuthController.java @@ -1,5 +1,28 @@ package com.example.demo.controller; +import com.example.demo.dto.LoginRequest; +import com.example.demo.dto.RegisterRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; + +@RestController +@RequestMapping("/api/auth") public class AuthController { + // TEMP: Register endpoint (service logic added later) + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + + // For now just return what was sent (test validation first) + return ResponseEntity.ok("User registered: " + request.getUsername()); + } + + // TEMP: Login endpoint + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + + return ResponseEntity.ok("Login attempt for: " + request.getUsernameOrEmail()); + } } \ No newline at end of file diff --git a/demo/src/main/java/com/example/demo/dto/LoginRequest.java b/demo/src/main/java/com/example/demo/dto/LoginRequest.java index 89d74f9..5371c7d 100644 --- a/demo/src/main/java/com/example/demo/dto/LoginRequest.java +++ b/demo/src/main/java/com/example/demo/dto/LoginRequest.java @@ -1,5 +1,18 @@ package com.example.demo.dto; +import jakarta.validation.constraints.NotBlank; + public class LoginRequest { + @NotBlank(message = "usernameOrEmail is required") + private String usernameOrEmail; + + @NotBlank(message = "password is required") + private String password; + + public String getUsernameOrEmail() { return usernameOrEmail; } + public void setUsernameOrEmail(String usernameOrEmail) { this.usernameOrEmail = usernameOrEmail; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } } diff --git a/demo/src/main/java/com/example/demo/dto/RecipeCreateRequest.java b/demo/src/main/java/com/example/demo/dto/RecipeCreateRequest.java index 00739e3..e7e3b73 100644 --- a/demo/src/main/java/com/example/demo/dto/RecipeCreateRequest.java +++ b/demo/src/main/java/com/example/demo/dto/RecipeCreateRequest.java @@ -1,5 +1,53 @@ package com.example.demo.dto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; +import java.util.List; + public class RecipeCreateRequest { + @NotBlank(message = "title is required") + @Size(max = 100, message = "title must be 100 characters or less") + private String title; + + @Size(max = 1000, message = "description must be 1000 characters or less") + private String description; + + @PositiveOrZero(message = "prepTimeMinutes must be 0 or greater") + private int prepTimeMinutes; + + @PositiveOrZero(message = "cookTimeMinutes must be 0 or greater") + private int cookTimeMinutes; + + @PositiveOrZero(message = "servings must be 0 or greater") + private int servings; + + @NotNull(message = "ingredients list is required") + private List ingredients; + + @NotNull(message = "steps list is required") + private List steps; + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public int getPrepTimeMinutes() { return prepTimeMinutes; } + public void setPrepTimeMinutes(int prepTimeMinutes) { this.prepTimeMinutes = prepTimeMinutes; } + + public int getCookTimeMinutes() { return cookTimeMinutes; } + public void setCookTimeMinutes(int cookTimeMinutes) { this.cookTimeMinutes = cookTimeMinutes; } + + public int getServings() { return servings; } + public void setServings(int servings) { this.servings = servings; } + + public List getIngredients() { return ingredients; } + public void setIngredients(List ingredients) { this.ingredients = ingredients; } + + public List getSteps() { return steps; } + public void setSteps(List steps) { this.steps = steps; } } diff --git a/demo/src/main/java/com/example/demo/dto/RegisterRequest.java b/demo/src/main/java/com/example/demo/dto/RegisterRequest.java index 3984882..c72f717 100644 --- a/demo/src/main/java/com/example/demo/dto/RegisterRequest.java +++ b/demo/src/main/java/com/example/demo/dto/RegisterRequest.java @@ -1,5 +1,29 @@ package com.example.demo.dto; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + public class RegisterRequest { + @NotBlank(message = "username is required") + @Size(min = 3, max = 30, message = "username must be 3-30 characters") + private String username; + + @NotBlank(message = "email is required") + @Email(message = "email must be valid") + private String email; + + @NotBlank(message = "password is required") + @Size(min = 8, max = 100, message = "password must be at least 8 characters") + private String password; + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } } diff --git a/demo/src/main/java/com/example/demo/exception/BadRequestException.java b/demo/src/main/java/com/example/demo/exception/BadRequestException.java index f96064c..3cc42bd 100644 --- a/demo/src/main/java/com/example/demo/exception/BadRequestException.java +++ b/demo/src/main/java/com/example/demo/exception/BadRequestException.java @@ -1,5 +1,8 @@ package com.example.demo.exception; -public class BadRequestException { - -} +@SuppressWarnings("serial") +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/demo/src/main/java/com/example/demo/exception/ErrorResponse.java b/demo/src/main/java/com/example/demo/exception/ErrorResponse.java index de0fe3a..4d88baa 100644 --- a/demo/src/main/java/com/example/demo/exception/ErrorResponse.java +++ b/demo/src/main/java/com/example/demo/exception/ErrorResponse.java @@ -1,5 +1,25 @@ package com.example.demo.exception; -public class ErrorResponse { +import java.time.LocalDateTime; -} +public class ErrorResponse { + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private String path; + + public ErrorResponse(LocalDateTime timestamp, int status, String error, String message, String path) { + this.timestamp = timestamp; + this.status = status; + this.error = error; + this.message = message; + this.path = path; + } + + public LocalDateTime getTimestamp() { return timestamp; } + public int getStatus() { return status; } + public String getError() { return error; } + public String getMessage() { return message; } + public String getPath() { return path; } +} \ No newline at end of file diff --git a/demo/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java b/demo/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java index 444e0cd..dab8ce6 100644 --- a/demo/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java +++ b/demo/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java @@ -1,5 +1,57 @@ package com.example.demo.exception; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.time.LocalDateTime; +import java.util.stream.Collectors; + +@ControllerAdvice public class GlobalExceptionHandler { + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFound(NotFoundException ex, HttpServletRequest request) { + return buildError(HttpStatus.NOT_FOUND, ex.getMessage(), request.getRequestURI()); + } + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequest(BadRequestException ex, HttpServletRequest request) { + return buildError(HttpStatus.BAD_REQUEST, ex.getMessage(), request.getRequestURI()); + } + + // Handles @Valid validation failures from DTOs + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidation(MethodArgumentNotValidException ex, + HttpServletRequest request) { + + String msg = ex.getBindingResult() + .getFieldErrors() + .stream() + .map(err -> err.getField() + ": " + err.getDefaultMessage()) + .collect(Collectors.joining(", ")); + + return buildError(HttpStatus.BAD_REQUEST, msg, request.getRequestURI()); + } + + // Fallback for anything you didn't explicitly handle + @ExceptionHandler(Exception.class) + public ResponseEntity handleGeneric(Exception ex, HttpServletRequest request) { + // In production you'd avoid returning raw exception messages. + return buildError(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error occurred", request.getRequestURI()); + } + + private ResponseEntity buildError(HttpStatus status, String message, String path) { + ErrorResponse body = new ErrorResponse( + LocalDateTime.now(), + status.value(), + status.getReasonPhrase(), + message, + path + ); + return ResponseEntity.status(status).body(body); + } } diff --git a/demo/src/main/java/com/example/demo/exception/NotFoundException.java b/demo/src/main/java/com/example/demo/exception/NotFoundException.java index 759cdf9..129f424 100644 --- a/demo/src/main/java/com/example/demo/exception/NotFoundException.java +++ b/demo/src/main/java/com/example/demo/exception/NotFoundException.java @@ -1,5 +1,8 @@ package com.example.demo.exception; -public class NotFoundException { - +@SuppressWarnings("serial") +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } }