Merge branch 'main' of gitlab.com:etc404/software-engineering-project

This commit is contained in:
kaipher7
2026-02-26 17:52:30 -07:00
9 changed files with 238 additions and 7 deletions
@@ -1,5 +1,50 @@
package com.example.demo.config; 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 { 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 youll likely switch to JWT or session login.
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
} }
@@ -1,5 +1,28 @@
package com.example.demo.controller; 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 { 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());
}
} }
@@ -1,5 +1,18 @@
package com.example.demo.dto; package com.example.demo.dto;
import jakarta.validation.constraints.NotBlank;
public class LoginRequest { 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; }
} }
@@ -1,5 +1,53 @@
package com.example.demo.dto; 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 { 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<IngredientDto> ingredients;
@NotNull(message = "steps list is required")
private List<StepDto> 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<IngredientDto> getIngredients() { return ingredients; }
public void setIngredients(List<IngredientDto> ingredients) { this.ingredients = ingredients; }
public List<StepDto> getSteps() { return steps; }
public void setSteps(List<StepDto> steps) { this.steps = steps; }
} }
@@ -1,5 +1,29 @@
package com.example.demo.dto; package com.example.demo.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class RegisterRequest { 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; }
} }
@@ -1,5 +1,8 @@
package com.example.demo.exception; package com.example.demo.exception;
public class BadRequestException { @SuppressWarnings("serial")
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
} }
@@ -1,5 +1,25 @@
package com.example.demo.exception; 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; }
} }
@@ -1,5 +1,57 @@
package com.example.demo.exception; 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 { public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException ex, HttpServletRequest request) {
return buildError(HttpStatus.NOT_FOUND, ex.getMessage(), request.getRequestURI());
}
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> 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);
}
} }
@@ -1,5 +1,8 @@
package com.example.demo.exception; package com.example.demo.exception;
public class NotFoundException { @SuppressWarnings("serial")
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
} }