From 58077801f4124b6a39611c7e1b213bfcbacb76dd Mon Sep 17 00:00:00 2001 From: mks257 <138339735+mks257@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:29:02 -0500 Subject: [PATCH 01/25] update User entity for login support --- .../coffeecodesyndicate/api/models/User.java | 72 ++++++++++++++----- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/coffeecodesyndicate/api/models/User.java b/src/main/java/com/coffeecodesyndicate/api/models/User.java index 7edaeb6..a94519b 100644 --- a/src/main/java/com/coffeecodesyndicate/api/models/User.java +++ b/src/main/java/com/coffeecodesyndicate/api/models/User.java @@ -1,51 +1,87 @@ package com.coffeecodesyndicate.api.models; import jakarta.persistence.*; - +import java.time.Instant; import java.util.Set; @Entity -@Table(name = "User") +@Table(name = "Users") public class User { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; + private Long id; + @Column(nullable = false, unique = true, length = 50) private String username; - private String password; + + @Column(nullable = false, unique = true, length = 100) private String email; - //isRegistered means they can adopt - //if isRegistered is false, can view only - private Boolean isRegistered; + // store only the hashed password (BCrypt), never the raw password + @Column(nullable = false, length = 255) + private String passwordHash; + + // isRegistered means they can adopt + // if isRegistered is false, can view only + private Boolean isRegistered = false; - //if true, user has admin privileges - private Boolean isAdmin; + // if true, user has admin privileges + private Boolean isAdmin = false; - //a user can have many applications + // account status flags for authentication + private Boolean enabled = true; // can log in + private Boolean locked = false; // locked after violations, etc. + + // audit fields + @Column(nullable = false, updatable = false) + private Instant createdAt = Instant.now(); + private Instant updatedAt = Instant.now(); + + // a user can have many applications @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private Set applications; - //a user can have many pets - //no cascade or orphanRemoval, so if user is deleted the associated pets aren't deleted either and can default back to being adoptable + // a user can have many pets + // no cascade or orphanRemoval, so if user is deleted the associated pets aren't deleted either and can default back to being adoptable @OneToMany(mappedBy = "owner") private Set pets; - public Integer getId() { return id; } - public void setId(Integer id) { this.id = id; } + /* ========= Getters and Setters ========= */ + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } - public String getPassword() { return password; } - public void setPassword(String password) { this.password = password; } - public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } - public boolean getIsRegistered() { return isRegistered; } + public String getPasswordHash() { return passwordHash; } + public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; } + + public Boolean getIsRegistered() { return isRegistered; } public void setIsRegistered(Boolean isRegistered) { this.isRegistered = isRegistered; } public Boolean getIsAdmin() { return isAdmin; } public void setIsAdmin(Boolean isAdmin) { this.isAdmin = isAdmin; } + + public Boolean getEnabled() { return enabled; } + public void setEnabled(Boolean enabled) { this.enabled = enabled; } + + public Boolean getLocked() { return locked; } + public void setLocked(Boolean locked) { this.locked = locked; } + + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } + + public Instant getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } + + public Set getApplications() { return applications; } + public void setApplications(Set applications) { this.applications = applications; } + + public Set getPets() { return pets; } + public void setPets(Set pets) { this.pets = pets; } } From fe6d2ead0c4f28c88613bbd16f736d700854e64d Mon Sep 17 00:00:00 2001 From: mks257 <138339735+mks257@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:32:50 -0500 Subject: [PATCH 02/25] update UserRepository with email lookup --- .../api/repositories/UserRepository.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/coffeecodesyndicate/api/repositories/UserRepository.java b/src/main/java/com/coffeecodesyndicate/api/repositories/UserRepository.java index be8fe2d..696d714 100644 --- a/src/main/java/com/coffeecodesyndicate/api/repositories/UserRepository.java +++ b/src/main/java/com/coffeecodesyndicate/api/repositories/UserRepository.java @@ -2,5 +2,16 @@ import com.coffeecodesyndicate.api.models.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; -public interface UserRepository extends JpaRepository {} +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + + // find user by email (case insensitive) for login + Optional findByEmailIgnoreCase(String email); + + // optional: find by username if you also want username-based login + Optional findByUsernameIgnoreCase(String username); +} From 38b161e2beae182095b3477678addc113d6c32ae Mon Sep 17 00:00:00 2001 From: mks257 <138339735+mks257@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:35:31 -0500 Subject: [PATCH 03/25] update UserService to hash passwords and use Long ids --- .../api/services/UserService.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/coffeecodesyndicate/api/services/UserService.java b/src/main/java/com/coffeecodesyndicate/api/services/UserService.java index 0d8c209..f6168bf 100644 --- a/src/main/java/com/coffeecodesyndicate/api/services/UserService.java +++ b/src/main/java/com/coffeecodesyndicate/api/services/UserService.java @@ -2,39 +2,52 @@ import com.coffeecodesyndicate.api.models.User; import com.coffeecodesyndicate.api.repositories.UserRepository; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; @Service public class UserService { + private final UserRepository repo; + private final PasswordEncoder passwordEncoder; - public UserService(UserRepository repo) { + public UserService(UserRepository repo, PasswordEncoder passwordEncoder) { this.repo = repo; + this.passwordEncoder = passwordEncoder; } public List findAll() { return repo.findAll(); } - public User findById(Integer id) { + public User findById(Long id) { return repo.findById(id).orElseThrow(() -> new RuntimeException("User not found")); } public User create(User u) { u.setId(null); // let JPA generate + // encode raw password if present + if (u.getPasswordHash() != null && !u.getPasswordHash().isBlank()) { + u.setPasswordHash(passwordEncoder.encode(u.getPasswordHash())); + } return repo.save(u); } - public User update(Integer id, User u) { - // ensure it exists (optional) - if (!repo.existsById(id)) throw new RuntimeException("User not found"); + public User update(Long id, User u) { + if (!repo.existsById(id)) { + throw new RuntimeException("User not found"); + } u.setId(id); + // encode raw password if it’s changed + if (u.getPasswordHash() != null && !u.getPasswordHash().isBlank()) { + u.setPasswordHash(passwordEncoder.encode(u.getPasswordHash())); + } return repo.save(u); } - public void delete(Integer id) { + public void delete(Long id) { repo.deleteById(id); } } From 5c6b5f7409afcc44e7296d7f5d2a5a0bb38c1357 Mon Sep 17 00:00:00 2001 From: mks257 <138339735+mks257@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:49:33 -0500 Subject: [PATCH 04/25] add SecurityConfig with JWT, CORS, and password encoder --- .../api/config/SecurityConfig.java | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java diff --git a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java new file mode 100644 index 0000000..371d513 --- /dev/null +++ b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java @@ -0,0 +1,107 @@ +package com.coffeecodesyndicate.api.config; + +import com.coffeecodesyndicate.api.config.security.JwtAuthFilter; // <- create this class (OncePerRequestFilter) +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + // Optional: If JwtAuthFilter bean exists, we’ll add it. If not, app still compiles/runs. + private final ObjectProvider jwtAuthFilterProvider; + + public SecurityConfig(ObjectProvider jwtAuthFilterProvider) { + this.jwtAuthFilterProvider = jwtAuthFilterProvider; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // REST APIs should be stateless + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource(null))) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + + // Authorization rules + .authorizeHttpRequests(auth -> auth + // public endpoints + .requestMatchers( + "/api/auth/**", + "/actuator/health", + "/error" + ).permitAll() + + // (optional) allow read-only access to docs + .requestMatchers( + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html" + ).permitAll() + + // allow CORS preflight + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + + // everything else requires auth + .anyRequest().authenticated() + ); + + // If a JwtAuthFilter bean is present, register it before UsernamePasswordAuthenticationFilter + JwtAuthFilter jwtFilter = jwtAuthFilterProvider.getIfAvailable(); + if (jwtFilter != null) { + http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); + } + + return http.build(); + } + + // BCrypt encoder used by UserService to hash passwords + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); + } + + /** + * CORS configuration + * Configure allowed origins via application.properties: + * app.cors.allowed-origins=http://localhost:5173,http://localhost:3000 + */ + @Bean + public CorsConfigurationSource corsConfigurationSource( + @Value("${app.cors.allowed-origins:*}") String allowedOriginsProp) { + + List allowedOrigins = ("*".equals(allowedOriginsProp)) + ? List.of("*") + : Arrays.stream(allowedOriginsProp.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toList(); + + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(allowedOrigins); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(!allowedOrigins.contains("*")); // only allow credentials when not wildcard + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} From a3472b54aafa42384119d8371e4badf771d5d3e3 Mon Sep 17 00:00:00 2001 From: mks257 <138339735+mks257@users.noreply.github.com> Date: Fri, 5 Sep 2025 16:03:42 -0500 Subject: [PATCH 05/25] add security/validation and JWT; remove jbcrypt --- pom.xml | 77 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/pom.xml b/pom.xml index 6aef70f..e61d334 100644 --- a/pom.xml +++ b/pom.xml @@ -2,72 +2,69 @@ 4.0.0 + org.springframework.boot spring-boot-starter-parent 3.5.5 - + + com.coffeecodesyndicate api 0.0.1-SNAPSHOT MV-API Demo project for Spring Boot - - - - - - - - - - - - - + 21 - + + org.springframework.boot spring-boot-starter-actuator + org.springframework.boot spring-boot-starter + org.springframework.boot - spring-boot-starter-test - test - - - com.h2database - h2 - runtime + spring-boot-starter-web + + org.springframework.boot spring-boot-starter-data-jpa + + - org.springframework.boot - spring-boot-starter-web + com.h2database + h2 + runtime + + - org.mindrot - jbcrypt - 0.4 + org.springframework.boot + spring-boot-starter-security + + org.springframework.boot - spring-boot-starter-oauth2-client + spring-boot-starter-validation + + io.jsonwebtoken jjwt-api @@ -85,6 +82,14 @@ 0.11.5 runtime + + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot spring-boot-starter-thymeleaf @@ -97,6 +102,21 @@ nz.net.ultraq.thymeleaf thymeleaf-layout-dialect + + + + org.springframework.boot + spring-boot-starter-test + test + + + @@ -107,5 +127,4 @@ - From 1a518648cec45f2d0cc6afc5c28849473a3004bc Mon Sep 17 00:00:00 2001 From: mks257 <138339735+mks257@users.noreply.github.com> Date: Fri, 5 Sep 2025 16:04:52 -0500 Subject: [PATCH 06/25] configure application.properties with JWT and CORS settings --- src/main/resources/application.properties | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 10e0a88..80deeed 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,10 @@ +# Application name spring.application.name=MV-API + +# JWT security settings +security.jwt.secret=REPLACE_WITH_32+_CHAR_SECRET +security.jwt.expMinutes=60 +security.jwt.refreshDays=14 + +# (optional) CORS configuration for frontend origins +app.cors.allowed-origins=http://localhost:5173,http://localhost:3000 From 794db57a019fcf0581c684f01ec1f61d72a0ce50 Mon Sep 17 00:00:00 2001 From: mks257 <138339735+mks257@users.noreply.github.com> Date: Fri, 5 Sep 2025 16:08:16 -0500 Subject: [PATCH 07/25] changed security password --- src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 80deeed..f287e56 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,7 +2,7 @@ spring.application.name=MV-API # JWT security settings -security.jwt.secret=REPLACE_WITH_32+_CHAR_SECRET +security.jwt.secret=mysupersecretkeymysupersecretkey123! security.jwt.expMinutes=60 security.jwt.refreshDays=14 From c9ea1b5c73c1b65a3c3e8bd427de789aa7c0eca4 Mon Sep 17 00:00:00 2001 From: Lauren Balch Date: Fri, 12 Sep 2025 11:52:31 -0500 Subject: [PATCH 08/25] dependencies added --- pom.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pom.xml b/pom.xml index 6aef70f..9586ab9 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,11 @@ jbcrypt 0.4 + + com.okta.spring + okta-spring-boot-starter + 3.0.5 + org.springframework.boot spring-boot-starter-oauth2-client @@ -93,6 +98,10 @@ org.thymeleaf.extras thymeleaf-extras-springsecurity6 + + org.springframework.boot + spring-boot-starter-security + nz.net.ultraq.thymeleaf thymeleaf-layout-dialect From 1b0b0f10a23dae0ac9a509843abf8f6de0269d68 Mon Sep 17 00:00:00 2001 From: Lauren Balch Date: Fri, 12 Sep 2025 12:31:06 -0500 Subject: [PATCH 09/25] Auth0 authentication & Auth2 authorization added --- .../com/coffeecodesyndicate/api/config/SecurityConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java index 8c643cd..fab2e65 100644 --- a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java +++ b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java @@ -15,10 +15,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/unregistered/**").permitAll() + .requestMatchers("/unregistered/**", "/login").permitAll() .anyRequest().authenticated() ) - .httpBasic(); + .oauth2Login(); return http.build(); } From bf0ac0c4ad277d2fc99b43ebcd27540e4543f1dc Mon Sep 17 00:00:00 2001 From: Lauren Balch Date: Fri, 12 Sep 2025 14:32:56 -0500 Subject: [PATCH 10/25] Add secure client connection --- .gitignore | 1 + src/main/resources/application.properties | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 667aaef..2d20004 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ target/ .settings .springBeans .sts4-cache +.env ### IntelliJ IDEA ### .idea diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 10e0a88..eedad99 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,5 @@ spring.application.name=MV-API +spring.security.oauth2.client.registration.auth0.client-id=${AUTH0_CLIENT_ID} +spring.security.oauth2.client.registration.auth0.client-secret=${AUTH0_CLIENT_SECRET} +spring.security.oauth2.client.registration.auth0.scope=openid, profile, email +spring.security.oauth2.client.provider.auth0.issuer-uri=dev-l2d4cal6tw36m07l.us.auth0.com From fcae0fc0bc763642a81af957f55c1d3717cb08b2 Mon Sep 17 00:00:00 2001 From: Lauren Balch Date: Fri, 12 Sep 2025 21:10:25 -0500 Subject: [PATCH 11/25] Additional environment variables pointed toward render --- .../com/coffeecodesyndicate/api/config/SecurityConfig.java | 2 +- src/main/resources/application.properties | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java index fab2e65..46b34df 100644 --- a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java +++ b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java @@ -18,7 +18,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/unregistered/**", "/login").permitAll() .anyRequest().authenticated() ) - .oauth2Login(); + .oauth2Login(oauth2 -> {}); return http.build(); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index eedad99..0daef91 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,4 +2,5 @@ spring.application.name=MV-API spring.security.oauth2.client.registration.auth0.client-id=${AUTH0_CLIENT_ID} spring.security.oauth2.client.registration.auth0.client-secret=${AUTH0_CLIENT_SECRET} spring.security.oauth2.client.registration.auth0.scope=openid, profile, email -spring.security.oauth2.client.provider.auth0.issuer-uri=dev-l2d4cal6tw36m07l.us.auth0.com +spring.security.oauth2.client.registration.auth0.redirect-uri=${RENDER_REDIRECT_URI} +spring.security.oauth2.client.provider.auth0.issuer-uri=${RENDER_ISSUER_URI} From 04324305c1527c96784406ff05a31566d6041633 Mon Sep 17 00:00:00 2001 From: Lauren Balch Date: Fri, 19 Sep 2025 13:07:35 -0500 Subject: [PATCH 12/25] Merged with Main --- .../com/coffeecodesyndicate/api/config/SecurityConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java index e85a614..3a991e5 100644 --- a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java +++ b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java @@ -29,11 +29,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/unregistered/register").permitAll() + .requestMatchers("/unregistered/register", "/login").permitAll() .requestMatchers("/unregistered/**").permitAll() .anyRequest().authenticated() ) - .httpBasic(); + .oauth2Login(oauth2 -> {}); return http.build(); } From de1cea5725dc3364d3ed8c4240882d634e70246b Mon Sep 17 00:00:00 2001 From: mks257 <138339735+mks257@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:42:34 -0500 Subject: [PATCH 13/25] user login files --- .../api/config/ConflictException.java | 16 +++ .../ExcludeBrokenControllersConfig.java | 17 +++ .../api/config/GlobalExceptionHandler.java | 90 +++++++++++++ .../api/config/JService.java | 72 +++++++++++ .../api/config/SecurityConfig.java | 118 ++++++++++++++++++ .../api/controllers/AdminController.java | 5 +- .../controllers/ApplicationController.java | 14 +-- .../api/controllers/AuthController.java | 74 +++++++++++ .../api/controllers/UserController.java | 33 ++--- .../coffeecodesyndicate/api/dto/AuthDTOS.java | 27 ++++ .../coffeecodesyndicate/api/models/User.java | 31 +++-- .../api/repositories/UserRepository.java | 17 +-- .../api/services/UserService.java | 82 +++++------- src/main/resources/application.properties | 15 ++- 14 files changed, 507 insertions(+), 104 deletions(-) create mode 100644 src/main/java/com/coffeecodesyndicate/api/config/ConflictException.java create mode 100644 src/main/java/com/coffeecodesyndicate/api/config/ExcludeBrokenControllersConfig.java create mode 100644 src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java create mode 100644 src/main/java/com/coffeecodesyndicate/api/config/JService.java create mode 100644 src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java create mode 100644 src/main/java/com/coffeecodesyndicate/api/controllers/AuthController.java create mode 100644 src/main/java/com/coffeecodesyndicate/api/dto/AuthDTOS.java diff --git a/src/main/java/com/coffeecodesyndicate/api/config/ConflictException.java b/src/main/java/com/coffeecodesyndicate/api/config/ConflictException.java new file mode 100644 index 0000000..18be09a --- /dev/null +++ b/src/main/java/com/coffeecodesyndicate/api/config/ConflictException.java @@ -0,0 +1,16 @@ +package com.coffeecodesyndicate.api.config; + +/** + * Custom exception to represent 409 Conflict errors, + * e.g., when a username or email is already in use. + */ +public class ConflictException extends RuntimeException { + + public ConflictException(String message) { + super(message); + } + + public ConflictException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/coffeecodesyndicate/api/config/ExcludeBrokenControllersConfig.java b/src/main/java/com/coffeecodesyndicate/api/config/ExcludeBrokenControllersConfig.java new file mode 100644 index 0000000..a02ca3c --- /dev/null +++ b/src/main/java/com/coffeecodesyndicate/api/config/ExcludeBrokenControllersConfig.java @@ -0,0 +1,17 @@ +// src/main/java/com/coffeecodesyndicate/api/config/ExcludeBrokenControllersConfig.java +package com.coffeecodesyndicate.api.config; + +import com.coffeecodesyndicate.api.controllers.AdminController; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; + +@Configuration +@ComponentScan( + basePackages = "com.coffeecodesyndicate.api", + excludeFilters = @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + classes = AdminController.class + ) +) +public class ExcludeBrokenControllersConfig {} diff --git a/src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java b/src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..118fc92 --- /dev/null +++ b/src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java @@ -0,0 +1,90 @@ +package com.coffeecodesyndicate.api.config; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + /* ---------- 409: Conflicts (duplicates, unique key, etc.) ---------- */ + @ExceptionHandler(ConflictException.class) + public ResponseEntity handleConflict(ConflictException ex, HttpServletRequest req) { + return build(HttpStatus.CONFLICT, "Conflict", ex.getMessage(), req); + } + + // If DB unique constraints fire (e.g., uk_users_email), map to 409 as well + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrity(DataIntegrityViolationException ex, HttpServletRequest req) { + String msg = "Data integrity violation"; + if (ex.getMostSpecificCause() != null && ex.getMostSpecificCause().getMessage() != null) { + msg = ex.getMostSpecificCause().getMessage(); + } + return build(HttpStatus.CONFLICT, "Conflict", msg, req); + } + + /* ---------- 401: Authentication failures ---------- */ + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity handleBadCredentials(BadCredentialsException ex, HttpServletRequest req) { + return build(HttpStatus.UNAUTHORIZED, "Unauthorized", "Invalid credentials", req); + } + + /* ---------- 400: Validation errors (@Valid) ---------- */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidation(MethodArgumentNotValidException ex, HttpServletRequest req) { + Map errors = new HashMap<>(); + for (FieldError fe : ex.getBindingResult().getFieldErrors()) { + errors.put(fe.getField(), fe.getDefaultMessage()); + } + String message = "Validation failed"; + return build(HttpStatus.BAD_REQUEST, "Bad Request", message, req, errors); + } + + /* ---------- 500: Fallback ---------- */ + @ExceptionHandler(Exception.class) + public ResponseEntity handleOther(Exception ex, HttpServletRequest req) { + return build(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", ex.getMessage(), req); + } + + /* ---------- Helpers ---------- */ + private ResponseEntity build(HttpStatus status, String error, String message, HttpServletRequest req) { + return ResponseEntity.status(status) + .body(new ErrorResponse(Instant.now(), status.value(), error, message, req.getRequestURI(), null)); + } + + private ResponseEntity build(HttpStatus status, String error, String message, + HttpServletRequest req, Map details) { + return ResponseEntity.status(status) + .body(new ErrorResponse(Instant.now(), status.value(), error, message, req.getRequestURI(), details)); + } + + /* Small JSON error payload */ + public static class ErrorResponse { + public final Instant timestamp; + public final int status; + public final String error; + public final String message; + public final String path; + public final Map details; // optional per-field errors + + public ErrorResponse(Instant timestamp, int status, String error, String message, + String path, Map details) { + this.timestamp = timestamp; + this.status = status; + this.error = error; + this.message = message; + this.path = path; + this.details = details; + } + } +} diff --git a/src/main/java/com/coffeecodesyndicate/api/config/JService.java b/src/main/java/com/coffeecodesyndicate/api/config/JService.java new file mode 100644 index 0000000..9549245 --- /dev/null +++ b/src/main/java/com/coffeecodesyndicate/api/config/JService.java @@ -0,0 +1,72 @@ +package com.coffeecodesyndicate.api.config; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.function.Function; + +/** + * Service for creating and validating JWT tokens. + */ +@Service +public class JService { + + private final Key signingKey; + private final long expirationMillis; + + public JService( + @Value("${security.jwt.secret}") String secret, + @Value("${security.jwt.expMinutes:60}") long expMinutes + ) { + this.signingKey = Keys.hmacShaKeyFor(secret.getBytes()); + this.expirationMillis = expMinutes * 60_000L; + } + + /** + * Generate a JWT token for the given subject (usually the user's email). + */ + public String generateToken(String subject) { + long now = System.currentTimeMillis(); + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date(now)) + .setExpiration(new Date(now + expirationMillis)) + .signWith(signingKey, SignatureAlgorithm.HS256) + .compact(); + } + + /** + * Extract the subject (e.g. email) from the token. + */ + public String extractSubject(String token) { + return extractClaim(token, Claims::getSubject); + } + + /** + * Validate token for subject and expiration. + */ + public boolean isTokenValid(String token, String expectedSubject) { + String subject = extractSubject(token); + return subject.equalsIgnoreCase(expectedSubject) && !isTokenExpired(token); + } + + /* ---------- Internal helpers ---------- */ + + private boolean isTokenExpired(String token) { + Date expiration = extractClaim(token, Claims::getExpiration); + return expiration.before(new Date()); + } + + private T extractClaim(String token, Function resolver) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(signingKey) + .build() + .parseClaimsJws(token) + .getBody(); + return resolver.apply(claims); + } +} diff --git a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java new file mode 100644 index 0000000..dd90ae1 --- /dev/null +++ b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java @@ -0,0 +1,118 @@ +package com.coffeecodesyndicate.api.config; + +import com.coffeecodesyndicate.api.repositories.UserRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; + +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; + +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.User; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +import org.springframework.security.web.SecurityFilterChain; + +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +@Configuration +@EnableMethodSecurity +public class SecurityConfig { + + @Bean + public UserDetailsService userDetailsService(UserRepository repo) { + // authenticate by EMAIL (case-insensitive) + return email -> repo.findByEmailIgnoreCase(email) + .map(u -> User.withUsername(u.getEmail()) + .password(u.getPasswordHash()) // BCrypt hash from DB + .accountLocked(Boolean.TRUE.equals(u.getLocked())) + .disabled(!Boolean.TRUE.equals(u.getEnabled())) + .roles(Boolean.TRUE.equals(u.getIsAdmin()) ? "ADMIN" : "USER") + .build()) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); // BCrypt + } + + @Bean + public AuthenticationProvider authenticationProvider( + UserDetailsService uds, PasswordEncoder encoder) { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(uds); + provider.setPasswordEncoder(encoder); + return provider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration cfg) throws Exception { + return cfg.getAuthenticationManager(); + } + + @Bean + public SecurityFilterChain securityFilterChain( + HttpSecurity http, + AuthenticationProvider authProvider, + CorsConfigurationSource corsConfigurationSource) throws Exception { + + http + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource)) + .authenticationProvider(authProvider) + .authorizeHttpRequests(auth -> auth + // keep your existing public routes + .requestMatchers("/unregistered/register").permitAll() + .requestMatchers("/unregistered/**").permitAll() + // add auth/register/login if you create them later + .requestMatchers("/api/auth/**").permitAll() + .anyRequest().authenticated() + ) + // ok to keep httpBasic() for now; you can remove when you switch to JWT filter + .httpBasic(basic -> {}) + .formLogin(form -> form.disable()) + .sessionManagement(sm -> sm.sessionCreationPolicy( + org.springframework.security.config.http.SessionCreationPolicy.STATELESS + )); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource( + @Value("${app.cors.allowed-origins:}") String allowedOriginsProp) { + + CorsConfiguration config = new CorsConfiguration(); + if (allowedOriginsProp != null && !allowedOriginsProp.isBlank()) { + List origins = Arrays.stream(allowedOriginsProp.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toList(); + config.setAllowedOrigins(origins); + } else { + config.setAllowedOrigins(List.of("*")); // dev fallback + } + config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} diff --git a/src/main/java/com/coffeecodesyndicate/api/controllers/AdminController.java b/src/main/java/com/coffeecodesyndicate/api/controllers/AdminController.java index 378018f..b845cbf 100644 --- a/src/main/java/com/coffeecodesyndicate/api/controllers/AdminController.java +++ b/src/main/java/com/coffeecodesyndicate/api/controllers/AdminController.java @@ -15,7 +15,7 @@ @RestController @RequestMapping("/admin/pets") -@PreAuthorize("hasRole('ADMIN')") +@PreAuthorize("hasRole('isAdmin')") public class AdminController { private final PetRepository PetRepository; @@ -82,8 +82,7 @@ public void deleteUser(@PathVariable Integer id) { UserRepository.deleteById(id); } - - + // create @PostMapping("/applications") public Application createApplication(@RequestBody Application application) { return ApplicationRepository.save(application); } diff --git a/src/main/java/com/coffeecodesyndicate/api/controllers/ApplicationController.java b/src/main/java/com/coffeecodesyndicate/api/controllers/ApplicationController.java index 205008f..dbe61b1 100644 --- a/src/main/java/com/coffeecodesyndicate/api/controllers/ApplicationController.java +++ b/src/main/java/com/coffeecodesyndicate/api/controllers/ApplicationController.java @@ -2,6 +2,7 @@ import com.coffeecodesyndicate.api.models.Application; import com.coffeecodesyndicate.api.models.ApplicationStatus; +import com.coffeecodesyndicate.api.models.User; import com.coffeecodesyndicate.api.services.ApplicationService; import com.coffeecodesyndicate.api.services.UserService; import org.springframework.http.HttpStatus; @@ -20,40 +21,37 @@ public ApplicationController(ApplicationService apps, UserService users) { this.users = users; } - // List all applications @GetMapping("/apps") public List all() { return apps.findAll(); } - // Get one application @GetMapping("/apps/{id}") public Application one(@PathVariable Integer id) { return apps.findById(id); } - // Create application (body already has user set or null) @PostMapping("/apps") @ResponseStatus(HttpStatus.CREATED) public Application create(@RequestBody Application a) { return apps.create(a); } - // Create application for a specific user + // NOTE: User IDs are Long (UserRepository) @PostMapping("/apps/user/{userId}") @ResponseStatus(HttpStatus.CREATED) - public Application createForUser(@PathVariable Integer userId, @RequestBody Application a) { - a.setUser(users.findById(userId)); + public Application createForUser(@PathVariable Long userId, @RequestBody Application a) { + User u = users.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found: " + userId)); + a.setUser(u); return apps.create(a); } - //update application's status, not the whole Application object @PutMapping("/apps/{id}/status") public Application updateStatus(@PathVariable Integer id, @RequestParam ApplicationStatus status) { return apps.update(id, status); } - // Delete application @DeleteMapping("/apps/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void delete(@PathVariable Integer id) { diff --git a/src/main/java/com/coffeecodesyndicate/api/controllers/AuthController.java b/src/main/java/com/coffeecodesyndicate/api/controllers/AuthController.java new file mode 100644 index 0000000..95b8b84 --- /dev/null +++ b/src/main/java/com/coffeecodesyndicate/api/controllers/AuthController.java @@ -0,0 +1,74 @@ +package com.coffeecodesyndicate.api.controllers; + +import com.coffeecodesyndicate.api.dto.AuthDTOS; +import com.coffeecodesyndicate.api.models.User; +import com.coffeecodesyndicate.api.services.UserService; +import com.coffeecodesyndicate.api.config.JService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + private final AuthenticationManager authenticationManager; + private final UserService userService; + private final JService jwt; + + public AuthController(AuthenticationManager authenticationManager, + UserService userService, + JService jwt) { + this.authenticationManager = authenticationManager; + this.userService = userService; + this.jwt = jwt; + } + + /** + * Register a new user. + * Expects AuthDTOS.RegisterRequest { username, email, password }. + * Returns AuthDTOS.AuthResponse { token, message }. + */ + @PostMapping("/register") + public ResponseEntity register(@RequestBody AuthDTOS.RegisterRequest req) { + // build the user model (no raw password inside entity) + User u = new User(); + u.setUsername(req.username()); + u.setEmail(req.email()); + + // service will hash the password and save + User created = userService.registerUser(u, req.password()); + + // issue a JWT for immediate login + String token = jwt.generateToken(created.getEmail()); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(new AuthDTOS.AuthResponse(token, "Registered")); + } + + /** + * Login with email + password. + * Expects AuthDTOS.LoginRequest { email, password }. + * Returns AuthDTOS.AuthResponse { token, message }. + */ + @PostMapping("/login") + public ResponseEntity login(@RequestBody AuthDTOS.LoginRequest req) { + try { + Authentication auth = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(req.email(), req.password()) + ); + // principal username is the email per SecurityConfig UserDetailsService + String email = auth.getName(); + String token = jwt.generateToken(email); + return ResponseEntity.ok(new AuthDTOS.AuthResponse(token, "OK")); + } catch (BadCredentialsException ex) { + // consistent 401 when bad creds + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(new AuthDTOS.AuthResponse(null, "Invalid credentials")); + } + } +} diff --git a/src/main/java/com/coffeecodesyndicate/api/controllers/UserController.java b/src/main/java/com/coffeecodesyndicate/api/controllers/UserController.java index 9b6c867..78d61be 100644 --- a/src/main/java/com/coffeecodesyndicate/api/controllers/UserController.java +++ b/src/main/java/com/coffeecodesyndicate/api/controllers/UserController.java @@ -1,49 +1,50 @@ package com.coffeecodesyndicate.api.controllers; +import com.coffeecodesyndicate.api.dto.AuthDTOS; import com.coffeecodesyndicate.api.models.Pet; import com.coffeecodesyndicate.api.models.User; import com.coffeecodesyndicate.api.repositories.PetRepository; import com.coffeecodesyndicate.api.services.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; - import java.util.List; +import java.util.List; @RestController @RequestMapping("/unregistered") public class UserController { - @Autowired - private UserService userService; + private final UserService userService; + private final PetRepository petRepository; - //Non-registered users can access pets and see specific pets (by id) - @Autowired - private PetRepository petRepository; + public UserController(UserService userService, PetRepository petRepository) { + this.userService = userService; + this.petRepository = petRepository; + } - //get all pets + // public pet endpoints @GetMapping("/pets") public List getAllPets() { return petRepository.findAll(); - }; + } - //get pet by id @GetMapping("/pets/{id}") public Pet getPetById(@PathVariable Integer id) { return petRepository.findById(id).orElse(null); - }; + } @GetMapping("/users") -// @PreAuthorize("hasRole('isAdmin')") //only admins can get all user info, uncomment this when login route is done public List getAllUsers() { return userService.findAll(); } + // Register using DTO -> hashes into passwordHash @PostMapping("/register") @ResponseStatus(HttpStatus.CREATED) - public User registerUser(@RequestBody User user) { - return userService.registerUser(user); + public User registerUser(@RequestBody AuthDTOS.RegisterRequest req) { + User u = new User(); + u.setUsername(req.username()); + u.setEmail(req.email()); + return userService.registerUser(u, req.password()); } - } diff --git a/src/main/java/com/coffeecodesyndicate/api/dto/AuthDTOS.java b/src/main/java/com/coffeecodesyndicate/api/dto/AuthDTOS.java new file mode 100644 index 0000000..1897bad --- /dev/null +++ b/src/main/java/com/coffeecodesyndicate/api/dto/AuthDTOS.java @@ -0,0 +1,27 @@ +package com.coffeecodesyndicate.api.dto; + +/** + * Collection of authentication-related DTOs. + * You can also split these into separate files if you prefer. + */ +public class AuthDTOS { + + // Request body for registration + public static record RegisterRequest( + String username, + String email, + String password + ) {} + + // Request body for login + public static record LoginRequest( + String email, + String password + ) {} + + // Response body after login or register (JWT token and optional message) + public static record AuthResponse( + String token, + String message + ) {} +} diff --git a/src/main/java/com/coffeecodesyndicate/api/models/User.java b/src/main/java/com/coffeecodesyndicate/api/models/User.java index c6b9776..5908dda 100644 --- a/src/main/java/com/coffeecodesyndicate/api/models/User.java +++ b/src/main/java/com/coffeecodesyndicate/api/models/User.java @@ -4,6 +4,7 @@ import java.time.Instant; import java.util.Set; + @Entity @Table( name = "Users", @@ -18,56 +19,60 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - // unique username (limit to 50) @Column(nullable = false, unique = true, length = 50) private String username; - // unique email (limit to 100) @Column(nullable = false, unique = true, length = 100) private String email; - // store only the hashed password (e.g., BCrypt ~60 chars) @Column(nullable = false, length = 255) private String passwordHash; - // registration status for adoption permissions private Boolean isRegistered = false; - - // admin flag private Boolean isAdmin = false; + private Boolean enabled = true; + private Boolean locked = false; - // account status flags - private Boolean enabled = true; // can log in - private Boolean locked = false; // locked after violations, etc. - - // audit fields @Column(nullable = false, updatable = false) private Instant createdAt; @Column(nullable = false) private Instant updatedAt; - // relationships @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private Set applications; - // if user deleted, pets remain (no cascade) @OneToMany(mappedBy = "owner") private Set pets; + /* ==== NEW: transient raw password field ==== */ + @Transient + private String password; // not stored in DB + + public String getPassword() { + return password; + } + public void setPassword(String password) { + this.password = password; + } + /* ==== Lifecycle hooks ==== */ @PrePersist protected void onCreate() { Instant now = Instant.now(); this.createdAt = now; this.updatedAt = now; + + hashPasswordIfNeeded(); } @PreUpdate protected void onUpdate() { this.updatedAt = Instant.now(); + hashPasswordIfNeeded(); } + private void hashPasswordIfNeeded() { } /* ========= Getters and Setters ========= */ public Long getId() { return id; } diff --git a/src/main/java/com/coffeecodesyndicate/api/repositories/UserRepository.java b/src/main/java/com/coffeecodesyndicate/api/repositories/UserRepository.java index 920c5c7..0bd25bd 100644 --- a/src/main/java/com/coffeecodesyndicate/api/repositories/UserRepository.java +++ b/src/main/java/com/coffeecodesyndicate/api/repositories/UserRepository.java @@ -6,17 +6,18 @@ import java.util.Optional; -<<<<<<< HEAD @Repository public interface UserRepository extends JpaRepository { - // find user by email (case insensitive) for login Optional findByEmailIgnoreCase(String email); - - // optional: find by username if you also want username-based login + boolean existsByEmailIgnoreCase(String email); Optional findByUsernameIgnoreCase(String username); -======= -public interface UserRepository extends JpaRepository { - Optional findUserByUsername(String username); ->>>>>>> 86d59849857ec25193912e67624af04cfa311fde + + // --- Compatibility for teammate's controller (Integer IDs) --- + default Optional findById(Integer id) { + return findById(id == null ? null : id.longValue()); + } + default void deleteById(Integer id) { + if (id != null) deleteById(id.longValue()); + } } diff --git a/src/main/java/com/coffeecodesyndicate/api/services/UserService.java b/src/main/java/com/coffeecodesyndicate/api/services/UserService.java index 8f33ef7..1a1c173 100644 --- a/src/main/java/com/coffeecodesyndicate/api/services/UserService.java +++ b/src/main/java/com/coffeecodesyndicate/api/services/UserService.java @@ -10,81 +10,57 @@ @Service public class UserService { -<<<<<<< HEAD - private final UserRepository repo; + private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; -======= - private final UserRepository repo; - private final PasswordEncoder passwordEncoder; - ->>>>>>> 86d59849857ec25193912e67624af04cfa311fde - public UserService(UserRepository repo, PasswordEncoder passwordEncoder) { - this.repo = repo; + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public List findAll() { - return repo.findAll(); + return userRepository.findAll(); } - public User findById(Long id) { - return repo.findById(id).orElseThrow(() -> new RuntimeException("User not found")); + public Optional findById(Long id) { + return userRepository.findById(id); } - public User create(User u) { - u.setId(null); // let JPA generate - // encode raw password if present - if (u.getPasswordHash() != null && !u.getPasswordHash().isBlank()) { - u.setPasswordHash(passwordEncoder.encode(u.getPasswordHash())); - } - return repo.save(u); + public Optional findByEmail(String email) { + return userRepository.findByEmailIgnoreCase(email); } - public User update(Long id, User u) { - if (!repo.existsById(id)) { - throw new RuntimeException("User not found"); - } - u.setId(id); - // encode raw password if it’s changed - if (u.getPasswordHash() != null && !u.getPasswordHash().isBlank()) { - u.setPasswordHash(passwordEncoder.encode(u.getPasswordHash())); - } - return repo.save(u); + public boolean emailExists(String email) { + return userRepository.existsByEmailIgnoreCase(email); } -<<<<<<< HEAD - public void delete(Long id) { -======= - public Optional findUserByUsername(String username) { - return repo.findUserByUsername(username); + public User save(User user) { + return userRepository.save(user); } - public User registerUser(User user) { - if (repo.findUserByUsername(user.getUsername()).isPresent()) { - throw new RuntimeException("Username already exists"); + /** Register a new user (hash raw password into passwordHash). */ + public User registerUser(User user, String rawPassword) { + if (userRepository.existsByEmailIgnoreCase(user.getEmail())) { + throw new IllegalArgumentException("Email already in use"); } - user.setPassword(passwordEncoder.encode(user.getPassword())); - user.setIsRegistered(true); //user is going to be isRegistered, but not isAdmin - user.setIsAdmin(false); - return repo.save(user); - } + // Prefer rawPassword param, fallback to transient user.getPassword() + String toHash = (rawPassword != null && !rawPassword.isBlank()) + ? rawPassword + : user.getPassword(); - public User registerAdminUser(User user) { - if (repo.findUserByUsername(user.getUsername()).isPresent()) { - throw new RuntimeException("Username already exists"); + if (toHash == null || toHash.isBlank()) { + throw new IllegalArgumentException("Password is required"); } - user.setPassword(passwordEncoder.encode(user.getPassword())); - user.setIsRegistered(true); //user is going to be isRegistered and isAdmin - user.setIsAdmin(true); - return repo.save(user); - } + user.setPasswordHash(passwordEncoder.encode(toHash)); + user.setPassword(null); // clear transient field + + if (user.getEnabled() == null) user.setEnabled(true); + if (user.getLocked() == null) user.setLocked(false); + if (user.getIsAdmin() == null) user.setIsAdmin(false); - public void delete(Integer id) { ->>>>>>> 86d59849857ec25193912e67624af04cfa311fde - repo.deleteById(id); + return userRepository.save(user); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1dd818d..3659fba 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,11 +1,20 @@ # Application name spring.application.name=MV-API +# ====================== # JWT security settings +# ====================== + +# Secret must be at least 32 characters for HS256 security.jwt.secret=mysupersecretkeymysupersecretkey123! -security.jwt.expMinutes=60 # access token expiry = 1 hour -security.jwt.refreshDays=14 # refresh token expiry = 14 days +# Access token expiry in minutes +security.jwt.expMinutes=60 + +# Refresh token expiry in days +security.jwt.refreshDays=14 -# (optional) CORS configuration for frontend origins +# ====================== +# (Optional) CORS configuration for frontend origins +# ====================== app.cors.allowed-origins=http://localhost:5173,http://localhost:3000 From db52b5d88221f3640c5f3decf37539b188d7e51b Mon Sep 17 00:00:00 2001 From: Lauren Balch Date: Fri, 19 Sep 2025 14:44:59 -0500 Subject: [PATCH 14/25] Updated to show only a user's pets --- .../java/com/coffeecodesyndicate/api/services/PetService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/coffeecodesyndicate/api/services/PetService.java b/src/main/java/com/coffeecodesyndicate/api/services/PetService.java index fefedbc..348cd17 100644 --- a/src/main/java/com/coffeecodesyndicate/api/services/PetService.java +++ b/src/main/java/com/coffeecodesyndicate/api/services/PetService.java @@ -1,6 +1,7 @@ package com.coffeecodesyndicate.api.services; import com.coffeecodesyndicate.api.models.Pet; +import com.coffeecodesyndicate.api.models.User; import com.coffeecodesyndicate.api.repositories.PetRepository; import org.springframework.stereotype.Service; @@ -14,7 +15,7 @@ public PetService(PetRepository repo) { this.repo = repo; } - public List findAll() { return repo.findAll(); } + public List findAllByOwner(User owner) { return repo.findAll(); } public Pet findById(Integer id) { return repo.findById(id).orElseThrow(() -> new RuntimeException("Pet not found")); From 90dbc19e27f41c135d902732bded3e1bd937617b Mon Sep 17 00:00:00 2001 From: Lauren Balch Date: Fri, 19 Sep 2025 14:46:37 -0500 Subject: [PATCH 15/25] Getters and Setters for owner --- src/main/java/com/coffeecodesyndicate/api/models/Pet.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/coffeecodesyndicate/api/models/Pet.java b/src/main/java/com/coffeecodesyndicate/api/models/Pet.java index 68f5f9d..8c34868 100644 --- a/src/main/java/com/coffeecodesyndicate/api/models/Pet.java +++ b/src/main/java/com/coffeecodesyndicate/api/models/Pet.java @@ -32,8 +32,11 @@ public class Pet { public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } + public User getOwner() { return owner; } + public void setOwner(User owner) { this.owner = owner; } + public String getDescription() { return description; } - public void setDescription(String description) { this.description = Pet.this.description; } + public void setDescription(String description) { this.description = description; } public Boolean getIsAdopted() { return isAdopted; } public void setIsAdopted(Boolean isAdopted) { this.isAdopted = isAdopted; } From 0360a7856fcf99aa2c590f903795e77d4aeee637 Mon Sep 17 00:00:00 2001 From: Lauren Balch Date: Fri, 19 Sep 2025 14:46:59 -0500 Subject: [PATCH 16/25] Seeded Data --- .../api/config/DataSeeder.java | 157 ++++++++++++++++++ .../api/controllers/AdminController.java | 2 +- 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/coffeecodesyndicate/api/config/DataSeeder.java diff --git a/src/main/java/com/coffeecodesyndicate/api/config/DataSeeder.java b/src/main/java/com/coffeecodesyndicate/api/config/DataSeeder.java new file mode 100644 index 0000000..d149902 --- /dev/null +++ b/src/main/java/com/coffeecodesyndicate/api/config/DataSeeder.java @@ -0,0 +1,157 @@ +package com.coffeecodesyndicate.api.config; + +import java.time.LocalDate; + +import com.coffeecodesyndicate.api.models.Application; +import com.coffeecodesyndicate.api.models.ApplicationStatus; +import com.coffeecodesyndicate.api.models.Pet; +import com.coffeecodesyndicate.api.models.User; +import com.coffeecodesyndicate.api.repositories.ApplicationRepository; +import com.coffeecodesyndicate.api.repositories.PetRepository; +import com.coffeecodesyndicate.api.repositories.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component + +public class DataSeeder implements CommandLineRunner { + private final PetRepository petRepository; + private final UserRepository userRepository; + private final ApplicationRepository applicationRepository; + + @Autowired + public DataSeeder(PetRepository petRepository, UserRepository userRepository, ApplicationRepository applicationRepository){ + this.petRepository = petRepository; + this.userRepository = userRepository; + this.applicationRepository = applicationRepository; + } + + @Override + public void run(String...args) throws Exception { + User registered = new User(); + registered.setUsername("lukePowell8"); + registered.setPassword("pass123#"); + registered.setEmail("powell8@example.com"); + registered.setIsRegistered(true); + registered.setIsAdmin(false); + + User admin = new User(); + admin.setUsername("adminUser"); + admin.setPassword("$2a$10$Xb2YjK1"); + admin.setEmail("admin@pets.com"); + admin.setIsRegistered(true); + admin.setIsAdmin(true); + + User registered2 = new User(); + registered2.setUsername("janeDoe"); + registered2.setPassword("$2a$10$mkdL"); + registered2.setEmail("petLover@example.com"); + registered2.setIsRegistered(true); + registered2.setIsAdmin(false); + + User registered3 = new User(); + registered3.setUsername("billBrown"); + registered3.setPassword("dogs4Life"); + registered3.setEmail("brownFamily@example.com"); + registered3.setIsRegistered(true); + registered3.setIsAdmin(false); + + User registered4 = new User(); + registered4.setUsername("paisClanton"); + registered4.setPassword("p$2015"); + registered4.setEmail("Paisley@example.com"); + registered4.setIsRegistered(true); + registered4.setIsAdmin(false); + + userRepository.save(admin); + userRepository.save(registered); + userRepository.save(registered2); + userRepository.save(registered3); + userRepository.save(registered4); + + Pet pet1 = new Pet(); + pet1.setName("Buddy"); + pet1.setBreed("Labrador"); + pet1.setAge(3); + pet1.setDescription("Friendly dog"); + pet1.setIsAdopted(true); + pet1.setOwner(registered3); + + Pet pet2 = new Pet(); + pet2.setName("Whiskers"); + pet2.setBreed("Tabby"); + pet2.setAge(2); + pet2.setDescription("Playful cat"); + pet2.setIsAdopted(true); + pet2.setOwner(registered4); + + Pet pet3 = new Pet(); + pet3.setName("Duke"); + pet3.setBreed("Boxer"); + pet3.setAge(6); + pet3.setDescription("Gentle giant, gets along with everyone."); + pet3.setIsAdopted(false); + pet3.setOwner(null); + + Pet pet4 = new Pet(); + pet4.setName("Oliver"); + pet4.setBreed("Maine Coon"); + pet4.setAge(1); + pet4.setDescription("Playful kitten, loves climbing and chasing toys."); + pet4.setIsAdopted(false); + pet4.setOwner(null); + + Pet pet5 = new Pet(); + pet5.setName("Molly"); + pet5.setBreed("Beagle"); + pet5.setAge(4); + pet5.setDescription("Energetic and loves walks."); + pet5.setIsAdopted(true); + pet5.setOwner(registered2); + + petRepository.save(pet1); + petRepository.save(pet2); + petRepository.save(pet3); + petRepository.save(pet4); + petRepository.save(pet5); + + + Application app1 = new Application(); + app1.setStatus(ApplicationStatus.PENDING); // assuming you have PENDING, APPROVED, REJECTED in your enum + app1.setApplicationDate(LocalDate.now().minusDays(3)); + app1.setFormTitle("Adoption for Duke"); + app1.setFormBody("I'm looking for a longtime companion for my family."); + app1.setUser(registered2); // assign to an existing user + app1.setPet(pet3); // assign to an existing pet + + Application app2 = new Application(); + app2.setStatus(ApplicationStatus.APPROVED); + app2.setApplicationDate(LocalDate.now().minusDays(10)); + app2.setFormTitle("Adoption for Whiskers"); + app2.setFormBody("I have shown how responsible I was this school year. Mom said I could get a kitty."); + app2.setUser(registered4); + app2.setPet(pet2); + + Application app3 = new Application(); + app3.setStatus(ApplicationStatus.REJECTED); + app3.setApplicationDate(LocalDate.now().minusDays(1)); + app3.setFormTitle("Adoption for Oliver"); + app3.setFormBody("Have always wanted a Maine Coon and have experience with cats."); + app3.setUser(registered); + app3.setPet(pet4); + + applicationRepository.save(app1); + applicationRepository.save(app2); + applicationRepository.save(app3); + + + + + + + } + + +} + diff --git a/src/main/java/com/coffeecodesyndicate/api/controllers/AdminController.java b/src/main/java/com/coffeecodesyndicate/api/controllers/AdminController.java index 378018f..42760f0 100644 --- a/src/main/java/com/coffeecodesyndicate/api/controllers/AdminController.java +++ b/src/main/java/com/coffeecodesyndicate/api/controllers/AdminController.java @@ -15,7 +15,7 @@ @RestController @RequestMapping("/admin/pets") -@PreAuthorize("hasRole('ADMIN')") +@PreAuthorize("hasRole('isAdmin')") public class AdminController { private final PetRepository PetRepository; From 56f82a1b37d15e3039032e358a6e15410c2f870e Mon Sep 17 00:00:00 2001 From: Kavya sri meka <138339735+mks257@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:19:08 -0500 Subject: [PATCH 17/25] Create README.md --- README.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..58aad51 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +MV-API +A Spring Boot 3.5.5 application that provides user management and authentication using JWT. +Includes registration, login, and secured endpoints, with support for role-based access. +πŸš€ Features +β€’ User registration & login +β€’ Password hashing with BCrypt +β€’ JWT access & refresh tokens +β€’ Global exception handling +β€’ H2 database for dev/test +β€’ CORS configuration for frontend (React/Vue/Angular) +β€’ Actuator endpoints for health checks +πŸ“¦ Tech Stack +β€’ Java 21 +β€’ Spring Boot 3.5.5 (Web, Data JPA, Security, Validation, Actuator) +β€’ JJWT 0.11.5 +β€’ H2 Database (dev/test) +βš™οΈ Getting Started +Prerequisites: +- Java 21 +- Maven 3.9+ +- VS Code or IntelliJ IDEA +Build & Run: +./mvnw clean install +./mvnw spring-boot:run +App will be available at http://localhost:8080 +πŸ”‘ Authentication Flow +1. Register a User +POST /api/auth/register +Content-Type: application/json + +{ + "email": "user@example.com", + "username": "testuser", + "password": "password123" +} +2. Login +POST /api/auth/login +Content-Type: application/json + +{ + "email": "user@example.com", + "password": "password123" +} +βœ”οΈ Returns a JWT token. +3. Call Protected Endpoint +GET /api/users/me +Authorization: Bearer +βš™οΈ Configuration +Main properties are in src/main/resources/application.properties: + +spring.application.name=MV-API + +# JWT +security.jwt.secret=mysupersecretkeymysupersecretkey123! +security.jwt.expMinutes=60 +security.jwt.refreshDays=14 + +# CORS +app.cors.allowed-origins=http://localhost:5173,http://localhost:3000 +🐳 Docker +Build & run in Docker: +docker build -t mv-api . +docker run -p 8080:8080 mv-api +πŸ“‚ Project Structure +src/main/java/com/coffeecodesyndicate/api + β”œβ”€β”€ config/ # Security, JWT, exception handling + β”œβ”€β”€ controllers/ # REST controllers (Auth, User, Admin, etc.) + β”œβ”€β”€ dto/ # DTOs for requests & responses + β”œβ”€β”€ models/ # Entities (User, Application, Pet, etc.) + β”œβ”€β”€ repositories/ # Spring Data JPA repositories + β”œβ”€β”€ services/ # Business logic + └── MvApiApplication.java # Main entry point +πŸ§ͺ Testing +./mvnw test From 7c013f7132afd19d81b002c8784492f419788ccd Mon Sep 17 00:00:00 2001 From: Kavya sri meka <138339735+mks257@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:22:18 -0500 Subject: [PATCH 18/25] Update README.md --- README.md | 81 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 58aad51..fdd5d5d 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,51 @@ -MV-API -A Spring Boot 3.5.5 application that provides user management and authentication using JWT. +# MV-API + +A Spring Boot 3.5.5 application that provides **user management and authentication using JWT**. Includes registration, login, and secured endpoints, with support for role-based access. -πŸš€ Features -β€’ User registration & login -β€’ Password hashing with BCrypt -β€’ JWT access & refresh tokens -β€’ Global exception handling -β€’ H2 database for dev/test -β€’ CORS configuration for frontend (React/Vue/Angular) -β€’ Actuator endpoints for health checks -πŸ“¦ Tech Stack -β€’ Java 21 -β€’ Spring Boot 3.5.5 (Web, Data JPA, Security, Validation, Actuator) -β€’ JJWT 0.11.5 -β€’ H2 Database (dev/test) -βš™οΈ Getting Started -Prerequisites: -- Java 21 -- Maven 3.9+ -- VS Code or IntelliJ IDEA -Build & Run: + +--- + +## πŸš€ Features +- User registration & login +- Password hashing with **BCrypt** +- JWT access & refresh tokens +- Global exception handling +- H2 database for dev/test +- CORS configuration for frontend (React/Vue/Angular) +- Actuator endpoints for health checks + +--- + +## πŸ“¦ Tech Stack +- **Java 21** +- **Spring Boot 3.5.5** + - Web + - Data JPA + - Security + - Validation + - Actuator +- **JJWT 0.11.5** +- **H2 Database** (dev/test) + +--- + +## βš™οΈ Getting Started + +### Prerequisites +- Java 21 +- Maven 3.9+ +- VS Code or IntelliJ IDEA + +### Build & Run +```bash ./mvnw clean install ./mvnw spring-boot:run -App will be available at http://localhost:8080 +App will be available at πŸ‘‰ http://localhost:8080 + πŸ”‘ Authentication Flow 1. Register a User +http +Copy code POST /api/auth/register Content-Type: application/json @@ -34,6 +55,8 @@ Content-Type: application/json "password": "password123" } 2. Login +http +Copy code POST /api/auth/login Content-Type: application/json @@ -42,12 +65,17 @@ Content-Type: application/json "password": "password123" } βœ”οΈ Returns a JWT token. + 3. Call Protected Endpoint +http +Copy code GET /api/users/me Authorization: Bearer βš™οΈ Configuration -Main properties are in src/main/resources/application.properties: +Edit src/main/resources/application.properties: +properties +Copy code spring.application.name=MV-API # JWT @@ -59,9 +87,14 @@ security.jwt.refreshDays=14 app.cors.allowed-origins=http://localhost:5173,http://localhost:3000 🐳 Docker Build & run in Docker: + +bash +Copy code docker build -t mv-api . docker run -p 8080:8080 mv-api πŸ“‚ Project Structure +graphql +Copy code src/main/java/com/coffeecodesyndicate/api β”œβ”€β”€ config/ # Security, JWT, exception handling β”œβ”€β”€ controllers/ # REST controllers (Auth, User, Admin, etc.) @@ -71,4 +104,6 @@ src/main/java/com/coffeecodesyndicate/api β”œβ”€β”€ services/ # Business logic └── MvApiApplication.java # Main entry point πŸ§ͺ Testing +bash +Copy code ./mvnw test From 2fffc44f8d8a23f0bacc0c7a7c6661173abbefaf Mon Sep 17 00:00:00 2001 From: Lauren Balch <152127463+llbalch@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:43:57 -0500 Subject: [PATCH 19/25] Update README.md Added some extra information --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fdd5d5d..9797d9a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # MV-API -A Spring Boot 3.5.5 application that provides **user management and authentication using JWT**. +A Spring Boot 3.5.5 RESTful API application that manages online pet adoption +Through relationships between users, applications, and admin tasks **user management and authentication using JWT**. Includes registration, login, and secured endpoints, with support for role-based access. + --- ## πŸš€ Features -- User registration & login +- User registration & login +- Authentication & Authorization with **Auth0 and OAuth**. +- Onwership enforcement - Password hashing with **BCrypt** - JWT access & refresh tokens - Global exception handling @@ -17,7 +21,8 @@ Includes registration, login, and secured endpoints, with support for role-based --- ## πŸ“¦ Tech Stack -- **Java 21** +- **Java 21** +- **Maven** - **Spring Boot 3.5.5** - Web - Data JPA @@ -35,7 +40,8 @@ Includes registration, login, and secured endpoints, with support for role-based - Java 21 - Maven 3.9+ - VS Code or IntelliJ IDEA - +- Cloned repository + ### Build & Run ```bash ./mvnw clean install From 561c15546761ca9b7c8b505e4d957c526a1e3620 Mon Sep 17 00:00:00 2001 From: Lauren Balch <152127463+llbalch@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:54:43 -0500 Subject: [PATCH 20/25] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9797d9a..9758278 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # MV-API A Spring Boot 3.5.5 RESTful API application that manages online pet adoption -Through relationships between users, applications, and admin tasks **user management and authentication using JWT**. +Through relationships between users, applications, and admin management. Includes registration, login, and secured endpoints, with support for role-based access. @@ -10,7 +10,7 @@ Includes registration, login, and secured endpoints, with support for role-based ## πŸš€ Features - User registration & login - Authentication & Authorization with **Auth0 and OAuth**. -- Onwership enforcement +- Ownership enforcement - Password hashing with **BCrypt** - JWT access & refresh tokens - Global exception handling From 1129c182218e7e66626fe55796dac3bb2a2b397e Mon Sep 17 00:00:00 2001 From: Lauren Balch <152127463+llbalch@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:56:01 -0500 Subject: [PATCH 21/25] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9758278..c6c4117 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # MV-API A Spring Boot 3.5.5 RESTful API application that manages online pet adoption -Through relationships between users, applications, and admin management. +through relationships between users, applications, and admin management. Includes registration, login, and secured endpoints, with support for role-based access. From e312df892c69de873706fd8320ab5235fc8bf63e Mon Sep 17 00:00:00 2001 From: mks257 <138339735+mks257@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:57:05 -0500 Subject: [PATCH 22/25] updated exception handler --- .../api/config/GlobalExceptionHandler.java | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java b/src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java index 118fc92..8006f91 100644 --- a/src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java +++ b/src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java @@ -4,11 +4,17 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.AuthenticationException; import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.NoHandlerFoundException; import java.time.Instant; import java.util.HashMap; @@ -39,15 +45,51 @@ public ResponseEntity handleBadCredentials(BadCredentialsExceptio return build(HttpStatus.UNAUTHORIZED, "Unauthorized", "Invalid credentials", req); } - /* ---------- 400: Validation errors (@Valid) ---------- */ + // Covers missing/invalid token, expired token (if mapped), not authenticated, etc. + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuth(AuthenticationException ex, HttpServletRequest req) { + return build(HttpStatus.UNAUTHORIZED, "Unauthorized", "Authentication is required to access this resource.", req); + } + + /* ---------- 403: Authorization failures ---------- */ + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDenied(AccessDeniedException ex, HttpServletRequest req) { + return build(HttpStatus.FORBIDDEN, "Forbidden", "You do not have permission to access this resource.", req); + } + + /* ---------- 400: Validation / bad request ---------- */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidation(MethodArgumentNotValidException ex, HttpServletRequest req) { Map errors = new HashMap<>(); for (FieldError fe : ex.getBindingResult().getFieldErrors()) { errors.put(fe.getField(), fe.getDefaultMessage()); } - String message = "Validation failed"; - return build(HttpStatus.BAD_REQUEST, "Bad Request", message, req, errors); + return build(HttpStatus.BAD_REQUEST, "Bad Request", "Validation failed", req, errors); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleUnreadable(HttpMessageNotReadableException ex, HttpServletRequest req) { + return build(HttpStatus.BAD_REQUEST, "Bad Request", "Malformed JSON request", req); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity handleMissingParam(MissingServletRequestParameterException ex, HttpServletRequest req) { + String msg = "Missing required parameter: " + ex.getParameterName(); + return build(HttpStatus.BAD_REQUEST, "Bad Request", msg, req); + } + + /* ---------- 404 / 405: Routing issues ---------- */ + // To trigger this, ensure: + // spring.mvc.throw-exception-if-no-handler-found=true + // spring.web.resources.add-mappings=false + @ExceptionHandler(NoHandlerFoundException.class) + public ResponseEntity handleNotFound(NoHandlerFoundException ex, HttpServletRequest req) { + return build(HttpStatus.NOT_FOUND, "Not Found", "No handler found for the requested path.", req); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity handleMethodNotAllowed(HttpRequestMethodNotSupportedException ex, HttpServletRequest req) { + return build(HttpStatus.METHOD_NOT_ALLOWED, "Method Not Allowed", "HTTP method not supported for this endpoint.", req); } /* ---------- 500: Fallback ---------- */ From ce2dcf90d82e6a706ff62c8eb94b5ee308940fe0 Mon Sep 17 00:00:00 2001 From: mks257 <138339735+mks257@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:58:36 -0500 Subject: [PATCH 23/25] updated exception handler --- pom.xml | 2 +- .../java/com/coffeecodesyndicate/api/config/SecurityConfig.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 08a361d..e08404a 100644 --- a/pom.xml +++ b/pom.xml @@ -136,4 +136,4 @@ - + \ No newline at end of file diff --git a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java index 9451838..6fa00da 100644 --- a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java +++ b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java @@ -127,4 +127,4 @@ public CorsConfigurationSource corsConfigurationSource( source.registerCorsConfiguration("/**", config); return source; } -} +} \ No newline at end of file From 9e6bf5ab5ad1faad051ad909d188350e01c8b9e4 Mon Sep 17 00:00:00 2001 From: Lauren Balch Date: Fri, 19 Sep 2025 16:27:03 -0500 Subject: [PATCH 24/25] Deleted Second SecurityChainFilter instance --- .../api/config/SecurityConfig.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java index 9451838..fd441eb 100644 --- a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java +++ b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java @@ -61,17 +61,6 @@ public AuthenticationProvider authenticationProvider( return provider; } - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/unregistered/register", "/login").permitAll() - .requestMatchers("/unregistered/**").permitAll() - .anyRequest().authenticated() - ) - .oauth2Login(oauth2 -> {}); - - @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration cfg) throws Exception { return cfg.getAuthenticationManager(); @@ -98,6 +87,7 @@ public SecurityFilterChain securityFilterChain( // ok to keep httpBasic() for now; you can remove when you switch to JWT filter .httpBasic(basic -> {}) .formLogin(form -> form.disable()) + .oauth2Login(oauth2 -> {}) .sessionManagement(sm -> sm.sessionCreationPolicy( org.springframework.security.config.http.SessionCreationPolicy.STATELESS )); From c7182a004b995d4a85be2dbc35e9d700d1109845 Mon Sep 17 00:00:00 2001 From: Lauren Balch <152127463+llbalch@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:15:48 -0500 Subject: [PATCH 25/25] Revert "Kavya" --- pom.xml | 2 +- .../api/config/GlobalExceptionHandler.java | 48 ++----------------- .../api/config/SecurityConfig.java | 2 +- 3 files changed, 5 insertions(+), 47 deletions(-) diff --git a/pom.xml b/pom.xml index e08404a..08a361d 100644 --- a/pom.xml +++ b/pom.xml @@ -136,4 +136,4 @@ - \ No newline at end of file + diff --git a/src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java b/src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java index 8006f91..118fc92 100644 --- a/src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java +++ b/src/main/java/com/coffeecodesyndicate/api/config/GlobalExceptionHandler.java @@ -4,17 +4,11 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.AuthenticationException; import org.springframework.validation.FieldError; -import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.servlet.NoHandlerFoundException; import java.time.Instant; import java.util.HashMap; @@ -45,51 +39,15 @@ public ResponseEntity handleBadCredentials(BadCredentialsExceptio return build(HttpStatus.UNAUTHORIZED, "Unauthorized", "Invalid credentials", req); } - // Covers missing/invalid token, expired token (if mapped), not authenticated, etc. - @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuth(AuthenticationException ex, HttpServletRequest req) { - return build(HttpStatus.UNAUTHORIZED, "Unauthorized", "Authentication is required to access this resource.", req); - } - - /* ---------- 403: Authorization failures ---------- */ - @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity handleAccessDenied(AccessDeniedException ex, HttpServletRequest req) { - return build(HttpStatus.FORBIDDEN, "Forbidden", "You do not have permission to access this resource.", req); - } - - /* ---------- 400: Validation / bad request ---------- */ + /* ---------- 400: Validation errors (@Valid) ---------- */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidation(MethodArgumentNotValidException ex, HttpServletRequest req) { Map errors = new HashMap<>(); for (FieldError fe : ex.getBindingResult().getFieldErrors()) { errors.put(fe.getField(), fe.getDefaultMessage()); } - return build(HttpStatus.BAD_REQUEST, "Bad Request", "Validation failed", req, errors); - } - - @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity handleUnreadable(HttpMessageNotReadableException ex, HttpServletRequest req) { - return build(HttpStatus.BAD_REQUEST, "Bad Request", "Malformed JSON request", req); - } - - @ExceptionHandler(MissingServletRequestParameterException.class) - public ResponseEntity handleMissingParam(MissingServletRequestParameterException ex, HttpServletRequest req) { - String msg = "Missing required parameter: " + ex.getParameterName(); - return build(HttpStatus.BAD_REQUEST, "Bad Request", msg, req); - } - - /* ---------- 404 / 405: Routing issues ---------- */ - // To trigger this, ensure: - // spring.mvc.throw-exception-if-no-handler-found=true - // spring.web.resources.add-mappings=false - @ExceptionHandler(NoHandlerFoundException.class) - public ResponseEntity handleNotFound(NoHandlerFoundException ex, HttpServletRequest req) { - return build(HttpStatus.NOT_FOUND, "Not Found", "No handler found for the requested path.", req); - } - - @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - public ResponseEntity handleMethodNotAllowed(HttpRequestMethodNotSupportedException ex, HttpServletRequest req) { - return build(HttpStatus.METHOD_NOT_ALLOWED, "Method Not Allowed", "HTTP method not supported for this endpoint.", req); + String message = "Validation failed"; + return build(HttpStatus.BAD_REQUEST, "Bad Request", message, req, errors); } /* ---------- 500: Fallback ---------- */ diff --git a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java index c7e356f..fd441eb 100644 --- a/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java +++ b/src/main/java/com/coffeecodesyndicate/api/config/SecurityConfig.java @@ -117,4 +117,4 @@ public CorsConfigurationSource corsConfigurationSource( source.registerCorsConfiguration("/**", config); return source; } -} \ No newline at end of file +}