From 0f8cb6420204d02255ee420bbbfc2539cb97d5f3 Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Sat, 11 Apr 2026 22:37:17 +0530 Subject: [PATCH 01/20] feat: Add user registration functionality with password encoding and validation --- pom.xml | 6 ++++++ .../eventsphere/config/AppConfig.java | 7 +++++++ .../controller/UserController.java | 6 +++--- .../eventsphere/dto/RegisterRequest.java | 9 +++++++++ .../eventsphere/service/UserService.java | 4 ++-- .../service/impl/UserServiceImpl.java | 15 +++++++++++++- .../db/migration/V3__insert_sample_data.sql | 20 +++++++++---------- 7 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 src/main/java/dev/pasinduog/eventsphere/dto/RegisterRequest.java diff --git a/pom.xml b/pom.xml index f35a823..9adcfa0 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,12 @@ spring-boot-starter-flyway + + + org.springframework.boot + spring-boot-starter-security + + org.flywaydb diff --git a/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java index c238438..26c1678 100644 --- a/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java +++ b/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java @@ -4,6 +4,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.client.RestClient; import java.net.http.HttpClient; @@ -34,4 +36,9 @@ public RestClient restClient() { public Logger logger() { return Logger.getLogger("dev.pasinduog.eventsphere.config"); } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } } diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java index 4ba99b3..6a21d31 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java @@ -1,7 +1,7 @@ package dev.pasinduog.eventsphere.controller; +import dev.pasinduog.eventsphere.dto.RegisterRequest; import dev.pasinduog.eventsphere.dto.UserResponse; -import dev.pasinduog.eventsphere.model.User; import dev.pasinduog.eventsphere.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -15,8 +15,8 @@ public class UserController { @PostMapping("/register") @ResponseStatus(HttpStatus.CREATED) - boolean registerUser(@RequestBody User user) { - return userService.registerUser(user); + boolean registerUser(@RequestBody RegisterRequest request) { + return userService.registerUser(request); } @GetMapping("/by-email") diff --git a/src/main/java/dev/pasinduog/eventsphere/dto/RegisterRequest.java b/src/main/java/dev/pasinduog/eventsphere/dto/RegisterRequest.java new file mode 100644 index 0000000..8abc0db --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/dto/RegisterRequest.java @@ -0,0 +1,9 @@ +package dev.pasinduog.eventsphere.dto; + +public record RegisterRequest( + String fullName, + String email, + String password, + String skillsAndInterests +) { +} diff --git a/src/main/java/dev/pasinduog/eventsphere/service/UserService.java b/src/main/java/dev/pasinduog/eventsphere/service/UserService.java index 7541887..1d4a3b6 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/UserService.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/UserService.java @@ -1,9 +1,9 @@ package dev.pasinduog.eventsphere.service; +import dev.pasinduog.eventsphere.dto.RegisterRequest; import dev.pasinduog.eventsphere.dto.UserResponse; -import dev.pasinduog.eventsphere.model.User; public interface UserService { - boolean registerUser(User user); + boolean registerUser(RegisterRequest request); UserResponse getUserByEmail(String email); } diff --git a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java index 88045a3..18e6a3e 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java @@ -1,11 +1,14 @@ package dev.pasinduog.eventsphere.service.impl; +import dev.pasinduog.eventsphere.dto.RegisterRequest; import dev.pasinduog.eventsphere.dto.UserResponse; +import dev.pasinduog.eventsphere.exception.UserAlreadyExistsException; import dev.pasinduog.eventsphere.exception.UserNotFoundException; import dev.pasinduog.eventsphere.model.User; import dev.pasinduog.eventsphere.repository.UserRepository; import dev.pasinduog.eventsphere.service.UserService; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.UUID; @@ -14,10 +17,20 @@ @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; @Override - public boolean registerUser(User user) { + public boolean registerUser(RegisterRequest request) { + if (userRepository.findByEmail(request.email()).isPresent()) { + throw new UserAlreadyExistsException("Email already exists"); + } + User user = new User(); user.setId(UUID.randomUUID().toString()); + user.setFullName(request.fullName()); + user.setEmail(request.email()); + user.setPasswordHash(passwordEncoder.encode(request.password())); + user.setRole("ATTENDEE"); + user.setSkillsAndInterests(request.skillsAndInterests()); return userRepository.save(user); } diff --git a/src/main/resources/db/migration/V3__insert_sample_data.sql b/src/main/resources/db/migration/V3__insert_sample_data.sql index 12b2aad..ae631cf 100644 --- a/src/main/resources/db/migration/V3__insert_sample_data.sql +++ b/src/main/resources/db/migration/V3__insert_sample_data.sql @@ -3,16 +3,16 @@ -- 1. USERS (10 Records - Admins, Speakers, Attendees) INSERT INTO users (id, full_name, email, password_hash, role, skills_and_interests) VALUES - ('886a8539-5426-4c95-b50c-c54d72e55d28', 'Pasindu Owa Gamage', 'admin1@eventsphere.com', 'dummy_hash_123', 'ADMIN', 'System Architecture, Spring Boot, Angular'), - ('eb334f13-7461-4d28-9e0e-7b933a5e5e4e', 'Kasun Perera', 'admin2@eventsphere.com', 'dummy_hash_123', 'ADMIN', 'Event Management, Marketing'), - ('1ea3c9fd-ad68-4105-8220-3a15694bc083', 'Dr. Ruwan Kumara', 'speaker1@eventsphere.com', 'dummy_hash_123', 'SPEAKER', 'AI, Machine Learning, Python'), - ('94d5ca84-a17c-45ed-8506-cf4220f5fa80', 'Sarah Jenkins', 'speaker2@eventsphere.com', 'dummy_hash_123', 'SPEAKER', 'Cloud Computing, AWS, DevOps'), - ('7743f6b1-d520-4a8f-b7a5-18e1d384e0f4', 'Nimali Silva', 'nimali@gmail.com', 'dummy_hash_123', 'ATTENDEE', 'Java, Spring Boot, Looking for Internships'), - ('280ac445-578a-46c6-b879-7816c7c691bc', 'Chamara Fernando', 'chamara@gmail.com', 'dummy_hash_123', 'ATTENDEE', 'Angular, Frontend Development, UI/UX'), - ('67f35b78-02fe-4879-8fe5-04d3633d5a56', 'Lahiru Senanayake', 'lahiru@gmail.com', 'dummy_hash_123', 'ATTENDEE', 'Microservices, Docker, Kubernetes'), - ('f8bcd1ca-8ee9-4b29-a689-ee691614dd8d', 'Gayantha De Silva', 'gayantha@gmail.com', 'dummy_hash_123', 'ATTENDEE', 'Looking for open-source contributors, Java'), - ('a3affe9d-1b0e-444e-a883-a24221f9aeb2', 'Sanduni Perera', 'sanduni@gmail.com', 'dummy_hash_123', 'ATTENDEE', 'Data Science, Gemini AI APIs'), - ('5ff6e1d8-fa5c-4ff3-8e69-8d41ae1f728d', 'Tharindu Bandara', 'tharindu@gmail.com', 'dummy_hash_123', 'ATTENDEE', 'Spring Boot, Backend Development'); + ('886a8539-5426-4c95-b50c-c54d72e55d28', 'Pasindu Owa Gamage', 'admin1@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ADMIN', 'System Architecture, Spring Boot, Angular'), + ('eb334f13-7461-4d28-9e0e-7b933a5e5e4e', 'Kasun Perera', 'admin2@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ADMIN', 'Event Management, Marketing'), + ('1ea3c9fd-ad68-4105-8220-3a15694bc083', 'Dr. Ruwan Kumara', 'speaker1@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'SPEAKER', 'AI, Machine Learning, Python'), + ('94d5ca84-a17c-45ed-8506-cf4220f5fa80', 'Sarah Jenkins', 'speaker2@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'SPEAKER', 'Cloud Computing, AWS, DevOps'), + ('7743f6b1-d520-4a8f-b7a5-18e1d384e0f4', 'Nimali Silva', 'nimali@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Java, Spring Boot, Looking for Internships'), + ('280ac445-578a-46c6-b879-7816c7c691bc', 'Chamara Fernando', 'chamara@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Angular, Frontend Development, UI/UX'), + ('67f35b78-02fe-4879-8fe5-04d3633d5a56', 'Lahiru Senanayake', 'lahiru@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Microservices, Docker, Kubernetes'), + ('f8bcd1ca-8ee9-4b29-a689-ee691614dd8d', 'Gayantha De Silva', 'gayantha@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Looking for open-source contributors, Java'), + ('a3affe9d-1b0e-444e-a883-a24221f9aeb2', 'Sanduni Perera', 'sanduni@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Data Science, Gemini AI APIs'), + ('5ff6e1d8-fa5c-4ff3-8e69-8d41ae1f728d', 'Tharindu Bandara', 'tharindu@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Spring Boot, Backend Development'); -- 2. EVENTS (10 Records) INSERT INTO events (id, organizer_id, title, description, start_time, end_time, max_attendees, status) VALUES From b0bd3097f963cf01531578aaded410d81748e2ca Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Sat, 11 Apr 2026 23:33:37 +0530 Subject: [PATCH 02/20] feat: Implement GitHub OAuth2 authentication with user registration and profile update functionality --- pom.xml | 6 ++ .../eventsphere/config/SecurityConfig.java | 46 ++++++++++++++ .../controller/UserController.java | 17 +++++- .../repository/UserRepository.java | 1 + .../repository/impl/UserRepositoryImpl.java | 6 ++ .../service/CustomOAuth2UserService.java | 60 +++++++++++++++++++ .../eventsphere/service/UserService.java | 3 + .../service/impl/UserServiceImpl.java | 11 ++++ src/main/resources/application.yml | 8 +++ 9 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java create mode 100644 src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java diff --git a/pom.xml b/pom.xml index 9adcfa0..7d2a2e7 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,12 @@ spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-client + + org.flywaydb diff --git a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java new file mode 100644 index 0000000..fec9a33 --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java @@ -0,0 +1,46 @@ +package dev.pasinduog.eventsphere.config; + +import dev.pasinduog.eventsphere.service.CustomOAuth2UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final CustomOAuth2UserService customOAuth2UserService; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/**", "/swagger-ui/**", "/v3/api-docs/**", "/ws-event-chat/**").permitAll() + .anyRequest().authenticated() + ) + .formLogin(AbstractAuthenticationFilterConfigurer::permitAll) + .httpBasic(basic -> { + }) + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService) + ) + .defaultSuccessUrl("http://localhost:8080/swagger-ui.html", true) + ) + .logout(logout -> logout + .logoutUrl("/api/v1/auth/logout") + .invalidateHttpSession(true) + .clearAuthentication(true) + .deleteCookies("JSESSIONID") + .logoutSuccessHandler((request, response, authentication) -> + response.setStatus(200)) + ); + return http.build(); + } +} diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java index 6a21d31..8171f86 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java @@ -2,25 +2,36 @@ import dev.pasinduog.eventsphere.dto.RegisterRequest; import dev.pasinduog.eventsphere.dto.UserResponse; +import dev.pasinduog.eventsphere.model.User; import dev.pasinduog.eventsphere.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import java.util.Map; + @RestController @RequestMapping("/api/v1/users") @RequiredArgsConstructor public class UserController { private final UserService userService; + @GetMapping("/by-email") + UserResponse getUserByEmail(@RequestParam String email) { + return userService.getUserByEmail(email); + } + @PostMapping("/register") @ResponseStatus(HttpStatus.CREATED) boolean registerUser(@RequestBody RegisterRequest request) { return userService.registerUser(request); } - @GetMapping("/by-email") - UserResponse getUserByEmail(@RequestParam String email) { - return userService.getUserByEmail(email); + @PutMapping("/{userId}/profile") + boolean updateProfile(@PathVariable String userId, @RequestBody Map updates){ + User user = userService.getUserById(userId); + if (updates.containsKey("skillsAndInterests")) user.setSkillsAndInterests(updates.get("skillsAndInterests")); + if (updates.containsKey("fullName")) user.setSkillsAndInterests(updates.get("fullName")); + return userService.updateUser(user); } } diff --git a/src/main/java/dev/pasinduog/eventsphere/repository/UserRepository.java b/src/main/java/dev/pasinduog/eventsphere/repository/UserRepository.java index b694e3c..fd7e0a7 100644 --- a/src/main/java/dev/pasinduog/eventsphere/repository/UserRepository.java +++ b/src/main/java/dev/pasinduog/eventsphere/repository/UserRepository.java @@ -7,6 +7,7 @@ public interface UserRepository { boolean save(User user); + boolean update(User user); Optional findById(String id); List findByIds(List ids); Optional findByEmail(String email); diff --git a/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java b/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java index 0af9c2f..3210f16 100644 --- a/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java @@ -46,6 +46,12 @@ public boolean save(User user) { } } + @Override + public boolean update(User user) { + String sql = "UPDATE users SET full_name = ?, skills_and_interests = ? WHERE id = ?"; + return jdbcTemplate.update(sql, user.getFullName(), user.getSkillsAndInterests(), user.getId()) > 0; + } + @Override public Optional findById(String id) { String sql = "SELECT id, full_name, email, `role`, skills_and_interests, created_at FROM users WHERE id = ?"; diff --git a/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java new file mode 100644 index 0000000..30c1eea --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java @@ -0,0 +1,60 @@ +package dev.pasinduog.eventsphere.service; + +import dev.pasinduog.eventsphere.model.User; +import dev.pasinduog.eventsphere.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.logging.Logger; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + private final UserRepository userRepository; + private final Logger logger; + private final PasswordEncoder passwordEncoder; + + private final List adminEmails = List.of( + "pasinduogdev@gmail.com", + "pasinduogdev2@gmail.com" + ); + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(userRequest); + String email = oAuth2User.getAttribute("email"); + String name = oAuth2User.getAttribute("name"); + if (name == null) name = oAuth2User.getAttribute("login"); + if (email == null) throw new OAuth2AuthenticationException("Email not found from GitHub provider"); + Optional userOptional = userRepository.findByEmail(email); + if (userOptional.isEmpty()) { + User user = new User(); + user.setId(UUID.randomUUID().toString()); + user.setFullName(name); + user.setEmail(email); + user.setPasswordHash(passwordEncoder.encode(UUID.randomUUID().toString())); + if (adminEmails.contains(email)) { + user.setRole("ADMIN"); + } else { + user.setRole("ATTENDEE"); + } + user.setSkillsAndInterests("GitHub User"); + user.setCreatedAt(LocalDateTime.now()); + + userRepository.save(user); + logger.info("New user registered via GitHub with role: " + user.getRole()); + } else { + logger.warning("User already exists"); + } + return oAuth2User; + } +} diff --git a/src/main/java/dev/pasinduog/eventsphere/service/UserService.java b/src/main/java/dev/pasinduog/eventsphere/service/UserService.java index 1d4a3b6..e1757c2 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/UserService.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/UserService.java @@ -2,8 +2,11 @@ import dev.pasinduog.eventsphere.dto.RegisterRequest; import dev.pasinduog.eventsphere.dto.UserResponse; +import dev.pasinduog.eventsphere.model.User; public interface UserService { boolean registerUser(RegisterRequest request); UserResponse getUserByEmail(String email); + User getUserById(String userId); + boolean updateUser(User user); } diff --git a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java index 18e6a3e..6c12e6a 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java @@ -47,4 +47,15 @@ public UserResponse getUserByEmail(String email) { user.getSkillsAndInterests() ); } + + @Override + public User getUserById(String userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId)); + } + + @Override + public boolean updateUser(User user) { + return userRepository.update(user); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b525513..b9c8fa5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,6 +9,14 @@ spring: enabled: true baseline-on-migrate: true + security: + oauth2: + client: + registration: + github: + client-id: ${GITHUB_CLIENT_ID} + client-secret: ${GITHUB_CLIENT_SECRET} + gemini: api: url: https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent From d3fd966b5cccf556aa15357e87df63becff372ec Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Mon, 13 Apr 2026 00:41:29 +0530 Subject: [PATCH 03/20] feat: Implement JWT authentication with login functionality and security configuration --- pom.xml | 24 +++++ .../eventsphere/config/SecurityConfig.java | 56 +++++++++--- .../controller/AuthController.java | 34 +++++++ .../controller/UserController.java | 2 +- .../eventsphere/dto/LoginRequest.java | 6 ++ .../eventsphere/dto/LoginResponse.java | 4 + .../eventsphere/filter/JwtAuthFilter.java | 54 ++++++++++++ .../dev/pasinduog/eventsphere/model/User.java | 28 +++++- .../eventsphere/service/JwtService.java | 21 +++++ .../service/impl/JwtServiceImpl.java | 88 +++++++++++++++++++ src/main/resources/application.yml | 9 +- 11 files changed, 311 insertions(+), 15 deletions(-) create mode 100644 src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java create mode 100644 src/main/java/dev/pasinduog/eventsphere/dto/LoginRequest.java create mode 100644 src/main/java/dev/pasinduog/eventsphere/dto/LoginResponse.java create mode 100644 src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java create mode 100644 src/main/java/dev/pasinduog/eventsphere/service/JwtService.java create mode 100644 src/main/java/dev/pasinduog/eventsphere/service/impl/JwtServiceImpl.java diff --git a/pom.xml b/pom.xml index 7d2a2e7..0bfbb1f 100644 --- a/pom.xml +++ b/pom.xml @@ -100,6 +100,30 @@ og4dev-spring-response 1.4.0 + + + + io.jsonwebtoken + jjwt-api + 0.13.0 + compile + + + + + io.jsonwebtoken + jjwt-impl + 0.13.0 + runtime + + + + + io.jsonwebtoken + jjwt-jackson + 0.13.0 + runtime + \ No newline at end of file diff --git a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java index fec9a33..ad2f589 100644 --- a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java +++ b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java @@ -1,46 +1,80 @@ package dev.pasinduog.eventsphere.config; +import dev.pasinduog.eventsphere.filter.JwtAuthFilter; +import dev.pasinduog.eventsphere.model.User; +import dev.pasinduog.eventsphere.repository.UserRepository; import dev.pasinduog.eventsphere.service.CustomOAuth2UserService; +import dev.pasinduog.eventsphere.service.JwtService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; 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.List; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; + private final JwtAuthFilter jwtAuthFilter; + private final JwtService jwtService; + private final UserRepository userRepository; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) { http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) // අනිවාර්යයි! .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/**", "/swagger-ui/**", "/v3/api-docs/**", "/ws-event-chat/**").permitAll() + .requestMatchers("/api/v1/auth/**", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html", "/ws-event-chat/**").permitAll() .anyRequest().authenticated() ) - .formLogin(AbstractAuthenticationFilterConfigurer::permitAll) - .httpBasic(basic -> { - }) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .userService(customOAuth2UserService) ) - .defaultSuccessUrl("http://localhost:8080/swagger-ui.html", true) + .successHandler((request, response, authentication) -> { + DefaultOAuth2User oauthUser = (DefaultOAuth2User) authentication.getPrincipal(); + if (oauthUser != null && oauthUser.getAttribute("email") != null) { + String email = oauthUser.getAttribute("email"); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new RuntimeException("User not found")); + + String token = jwtService.generateToken(user); + response.sendRedirect("http://localhost:4200/login?token=" + token); + } else { + response.sendRedirect("http://localhost:4200/login?error=github_email_missing"); + } + }) ) .logout(logout -> logout .logoutUrl("/api/v1/auth/logout") - .invalidateHttpSession(true) - .clearAuthentication(true) - .deleteCookies("JSESSIONID") - .logoutSuccessHandler((request, response, authentication) -> - response.setStatus(200)) + .logoutSuccessHandler((request, response, authentication) -> response.setStatus(200)) ); return http.build(); } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("http://localhost:4200")); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type")); + configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } } diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java b/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java new file mode 100644 index 0000000..131d959 --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java @@ -0,0 +1,34 @@ +package dev.pasinduog.eventsphere.controller; + +import dev.pasinduog.eventsphere.dto.LoginRequest; +import dev.pasinduog.eventsphere.dto.LoginResponse; +import dev.pasinduog.eventsphere.exception.UserNotFoundException; +import dev.pasinduog.eventsphere.model.User; +import dev.pasinduog.eventsphere.repository.UserRepository; +import dev.pasinduog.eventsphere.service.JwtService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class AuthController { + + private final UserRepository userRepository; + private final JwtService jwtService; + private final PasswordEncoder passwordEncoder; + + @PostMapping("/login") + public LoginResponse login(@RequestBody LoginRequest request) { + User user = userRepository.findByEmail(request.email()) + .orElseThrow(() -> new UserNotFoundException("User not found")); + + if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) { + throw new BadCredentialsException("Invalid credentials"); + } + String token = jwtService.generateToken(user); + return new LoginResponse(token); + } +} \ No newline at end of file diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java index 8171f86..c3cb53f 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java @@ -31,7 +31,7 @@ boolean registerUser(@RequestBody RegisterRequest request) { boolean updateProfile(@PathVariable String userId, @RequestBody Map updates){ User user = userService.getUserById(userId); if (updates.containsKey("skillsAndInterests")) user.setSkillsAndInterests(updates.get("skillsAndInterests")); - if (updates.containsKey("fullName")) user.setSkillsAndInterests(updates.get("fullName")); + if (updates.containsKey("fullName")) user.setFullName(updates.get("fullName")); return userService.updateUser(user); } } diff --git a/src/main/java/dev/pasinduog/eventsphere/dto/LoginRequest.java b/src/main/java/dev/pasinduog/eventsphere/dto/LoginRequest.java new file mode 100644 index 0000000..cb7308f --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/dto/LoginRequest.java @@ -0,0 +1,6 @@ +package dev.pasinduog.eventsphere.dto; + +public record LoginRequest( + String email, + String password +) {} diff --git a/src/main/java/dev/pasinduog/eventsphere/dto/LoginResponse.java b/src/main/java/dev/pasinduog/eventsphere/dto/LoginResponse.java new file mode 100644 index 0000000..2e1622e --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/dto/LoginResponse.java @@ -0,0 +1,4 @@ +package dev.pasinduog.eventsphere.dto; + +public record LoginResponse(String token) { +} diff --git a/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java b/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java new file mode 100644 index 0000000..0dbef97 --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java @@ -0,0 +1,54 @@ +package dev.pasinduog.eventsphere.filter; + +import dev.pasinduog.eventsphere.model.User; +import dev.pasinduog.eventsphere.repository.UserRepository; +import dev.pasinduog.eventsphere.service.JwtService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + private final JwtService jwtService; + private final UserRepository userRepository; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String userEmail; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(7); + userEmail = jwtService.extractUsername(jwt); + + if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { + User user = userRepository.findByEmail(userEmail).orElse(null); + if (user != null && jwtService.isTokenValid(jwt, user)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/dev/pasinduog/eventsphere/model/User.java b/src/main/java/dev/pasinduog/eventsphere/model/User.java index 8863a85..9304668 100644 --- a/src/main/java/dev/pasinduog/eventsphere/model/User.java +++ b/src/main/java/dev/pasinduog/eventsphere/model/User.java @@ -4,14 +4,21 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; @AllArgsConstructor @NoArgsConstructor @Getter @Setter -public class User { +public class User implements UserDetails { private String id; private String fullName; private String email; @@ -19,4 +26,23 @@ public class User { private String role; private String skillsAndInterests; private LocalDateTime createdAt; + + + @Override + @NullMarked + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority(this.role)); + } + + @Override + public @Nullable String getPassword() { + return this.passwordHash; + } + + @Override + @NullMarked + public String getUsername() { + return this.email; + } + } \ No newline at end of file diff --git a/src/main/java/dev/pasinduog/eventsphere/service/JwtService.java b/src/main/java/dev/pasinduog/eventsphere/service/JwtService.java new file mode 100644 index 0000000..bdcc1a2 --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/service/JwtService.java @@ -0,0 +1,21 @@ +package dev.pasinduog.eventsphere.service; + +import io.jsonwebtoken.Claims; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import javax.crypto.SecretKey; +import java.util.Collection; +import java.util.Date; + +public interface JwtService { + String generateToken(UserDetails userDetails); + String getFirstAuthority(Collection authorities); + boolean isTokenValid(String token, UserDetails userDetails); + boolean isTokenExpired(String token); + String extractUsername(String token); + String extractRole(String token); + Date extractExpiration(String token); + Claims extractAllClaims(String token); + SecretKey getSigningKey(); +} \ No newline at end of file diff --git a/src/main/java/dev/pasinduog/eventsphere/service/impl/JwtServiceImpl.java b/src/main/java/dev/pasinduog/eventsphere/service/impl/JwtServiceImpl.java new file mode 100644 index 0000000..e795c90 --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/service/impl/JwtServiceImpl.java @@ -0,0 +1,88 @@ +package dev.pasinduog.eventsphere.service.impl; + +import dev.pasinduog.eventsphere.service.JwtService; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@SuppressWarnings("unused") +public class JwtServiceImpl implements JwtService { + + @Value("${security.jwt.key}") + private String secretKey; + + @Value("${security.jwt.expiration}") + private long expiration; + + @Override + public String generateToken(UserDetails userDetails) { + Map extraClaims = new HashMap<>(); + extraClaims.put("role", getFirstAuthority(userDetails.getAuthorities())); + return Jwts.builder() + .claims(extraClaims) + .subject(userDetails.getUsername()) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey()) + .compact(); + } + + @Override + public String getFirstAuthority(Collection authorities) { + return authorities.stream().findFirst().map(GrantedAuthority::getAuthority) + .orElse("ATTENDEE"); + } + + @Override + public boolean isTokenValid(String token, UserDetails userDetails) { + return extractUsername(token).equals(userDetails.getUsername()) && !isTokenExpired(token); + } + + @Override + public boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + @Override + public String extractUsername(String token) { + return extractAllClaims(token).getSubject(); + } + + @Override + public String extractRole(String token) { + return (String) extractAllClaims(token).get("role"); + } + + @Override + public Date extractExpiration(String token) { + return extractAllClaims(token).getExpiration(); + } + + @Override + public Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + @Override + public SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b9c8fa5..7754179 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,7 +17,12 @@ spring: client-id: ${GITHUB_CLIENT_ID} client-secret: ${GITHUB_CLIENT_SECRET} -gemini: +Gemini: api: url: https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent - key: ${GEMINI_API_KEY} \ No newline at end of file + key: ${GEMINI_API_KEY} + +security: + jwt: + key: ${JWT_KEY} + expiration: 1640995200 \ No newline at end of file From 76999ccca1183d4869599bd5b2a065763017f288 Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Mon, 13 Apr 2026 13:42:51 +0530 Subject: [PATCH 04/20] feat: Add OAuth2 callback handling and validation for user authentication --- .../eventsphere/config/SecurityConfig.java | 19 ++++++-------- .../controller/AuthController.java | 19 ++++++++++++++ .../controller/EventController.java | 3 +++ .../controller/UserController.java | 2 ++ .../dto/OAuth2CallbackRequest.java | 4 +++ .../exception/InvalidAuthCodeException.java | 10 ++++++++ .../repository/impl/UserRepositoryImpl.java | 22 +++++++++++++--- .../service/OAuth2CodeService.java | 6 +++++ .../service/impl/OAuth2CodeServiceImpl.java | 25 +++++++++++++++++++ .../db/migration/V3__insert_sample_data.sql | 20 +++++++-------- 10 files changed, 104 insertions(+), 26 deletions(-) create mode 100644 src/main/java/dev/pasinduog/eventsphere/dto/OAuth2CallbackRequest.java create mode 100644 src/main/java/dev/pasinduog/eventsphere/exception/InvalidAuthCodeException.java create mode 100644 src/main/java/dev/pasinduog/eventsphere/service/OAuth2CodeService.java create mode 100644 src/main/java/dev/pasinduog/eventsphere/service/impl/OAuth2CodeServiceImpl.java diff --git a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java index ad2f589..916ddc4 100644 --- a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java +++ b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java @@ -1,13 +1,12 @@ package dev.pasinduog.eventsphere.config; import dev.pasinduog.eventsphere.filter.JwtAuthFilter; -import dev.pasinduog.eventsphere.model.User; -import dev.pasinduog.eventsphere.repository.UserRepository; import dev.pasinduog.eventsphere.service.CustomOAuth2UserService; -import dev.pasinduog.eventsphere.service.JwtService; +import dev.pasinduog.eventsphere.service.OAuth2CodeService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -22,12 +21,12 @@ @Configuration @EnableWebSecurity +@EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; private final JwtAuthFilter jwtAuthFilter; - private final JwtService jwtService; - private final UserRepository userRepository; + private final OAuth2CodeService oAuth2CodeService; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) { @@ -35,7 +34,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .cors(cors -> cors.configurationSource(corsConfigurationSource())) // අනිවාර්යයි! .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/**", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html", "/ws-event-chat/**").permitAll() + .requestMatchers("/api/v1/auth/**", "/api/v1/users/register", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html", "/ws-event-chat/**").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) @@ -48,12 +47,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { DefaultOAuth2User oauthUser = (DefaultOAuth2User) authentication.getPrincipal(); if (oauthUser != null && oauthUser.getAttribute("email") != null) { String email = oauthUser.getAttribute("email"); - - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new RuntimeException("User not found")); - - String token = jwtService.generateToken(user); - response.sendRedirect("http://localhost:4200/login?token=" + token); + String code = oAuth2CodeService.generateCode(email); + response.sendRedirect("http://localhost:4200/login?code=" + code); } else { response.sendRedirect("http://localhost:4200/login?error=github_email_missing"); } diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java b/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java index 131d959..3e1fc41 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java @@ -2,10 +2,13 @@ import dev.pasinduog.eventsphere.dto.LoginRequest; import dev.pasinduog.eventsphere.dto.LoginResponse; +import dev.pasinduog.eventsphere.dto.OAuth2CallbackRequest; +import dev.pasinduog.eventsphere.exception.InvalidAuthCodeException; import dev.pasinduog.eventsphere.exception.UserNotFoundException; import dev.pasinduog.eventsphere.model.User; import dev.pasinduog.eventsphere.repository.UserRepository; import dev.pasinduog.eventsphere.service.JwtService; +import dev.pasinduog.eventsphere.service.OAuth2CodeService; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.crypto.password.PasswordEncoder; @@ -19,6 +22,7 @@ public class AuthController { private final UserRepository userRepository; private final JwtService jwtService; private final PasswordEncoder passwordEncoder; + private final OAuth2CodeService oAuth2CodeService; @PostMapping("/login") public LoginResponse login(@RequestBody LoginRequest request) { @@ -31,4 +35,19 @@ public LoginResponse login(@RequestBody LoginRequest request) { String token = jwtService.generateToken(user); return new LoginResponse(token); } + + @PostMapping("/oauth2/callback") + public LoginResponse oauth2Callback(@RequestBody OAuth2CallbackRequest request) { + String email = oAuth2CodeService.validateCodeAndGetEmail(request.code()); + + if (email == null) { + throw new InvalidAuthCodeException("Invalid or expired authorization code"); + } + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UserNotFoundException("User not found")); + + String token = jwtService.generateToken(user); + return new LoginResponse(token); + } } \ No newline at end of file diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java b/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java index 25346dd..963583f 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java @@ -6,6 +6,7 @@ import dev.pasinduog.eventsphere.service.AiMatchmakingService; import dev.pasinduog.eventsphere.service.EventService; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -28,11 +29,13 @@ List getMatchSuggestions(@PathVariable String eventId, } @PostMapping + @PreAuthorize("hasAuthority('ORGANIZER') or hasAuthority('ADMIN')") boolean createEvent(@RequestBody Event event){ return eventService.createEvent(event); } @PostMapping("/{eventId}/register") + @PreAuthorize("isAuthenticated()") boolean registerEvent(@PathVariable String eventId, @RequestParam String userId){ return eventService.registerUserForEvent(eventId, userId); } diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java index c3cb53f..544cdd6 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java @@ -6,6 +6,7 @@ import dev.pasinduog.eventsphere.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.Map; @@ -28,6 +29,7 @@ boolean registerUser(@RequestBody RegisterRequest request) { } @PutMapping("/{userId}/profile") + @PreAuthorize("isAuthenticated()") boolean updateProfile(@PathVariable String userId, @RequestBody Map updates){ User user = userService.getUserById(userId); if (updates.containsKey("skillsAndInterests")) user.setSkillsAndInterests(updates.get("skillsAndInterests")); diff --git a/src/main/java/dev/pasinduog/eventsphere/dto/OAuth2CallbackRequest.java b/src/main/java/dev/pasinduog/eventsphere/dto/OAuth2CallbackRequest.java new file mode 100644 index 0000000..6f16217 --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/dto/OAuth2CallbackRequest.java @@ -0,0 +1,4 @@ +package dev.pasinduog.eventsphere.dto; + +public record OAuth2CallbackRequest(String code) { +} diff --git a/src/main/java/dev/pasinduog/eventsphere/exception/InvalidAuthCodeException.java b/src/main/java/dev/pasinduog/eventsphere/exception/InvalidAuthCodeException.java new file mode 100644 index 0000000..9988d2e --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/exception/InvalidAuthCodeException.java @@ -0,0 +1,10 @@ +package dev.pasinduog.eventsphere.exception; + +import io.github.og4dev.exception.ApiException; +import org.springframework.http.HttpStatus; + +public class InvalidAuthCodeException extends ApiException { + public InvalidAuthCodeException(String message) { + super(message, HttpStatus.UNAUTHORIZED); + } +} diff --git a/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java b/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java index 3210f16..82174de 100644 --- a/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java @@ -18,6 +18,20 @@ public class UserRepositoryImpl implements UserRepository { private final JdbcTemplate jdbcTemplate; private RowMapper rowMapper() { + return ((rs, rowNum) -> { + User user = new User(); + user.setId(rs.getString("id")); + user.setFullName(rs.getString("full_name")); + user.setEmail(rs.getString("email")); + user.setRole(rs.getString("role")); + user.setPasswordHash(rs.getString("password_hash")); + user.setSkillsAndInterests(rs.getString("skills_and_interests")); + user.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime()); + return user; + }); + } + + private RowMapper customRowMapper() { return ((rs, rowNum) -> { User user = new User(); user.setId(rs.getString("id")); @@ -54,7 +68,7 @@ public boolean update(User user) { @Override public Optional findById(String id) { - String sql = "SELECT id, full_name, email, `role`, skills_and_interests, created_at FROM users WHERE id = ?"; + String sql = "SELECT id, full_name, email, `role`, password_hash, skills_and_interests, created_at FROM users WHERE id = ?"; return jdbcTemplate.query(sql, rowMapper(), id).stream().findFirst(); } @@ -64,19 +78,19 @@ public List findByIds(List ids) { return List.of(); } String inSql = String.join(",", java.util.Collections.nCopies(ids.size(), "?")); - String sql = "SELECT id, full_name, email, `role`, skills_and_interests, created_at FROM users WHERE id IN (" + inSql + ")"; + String sql = "SELECT id, full_name, email, `role`, password_hash, skills_and_interests, created_at FROM users WHERE id IN (" + inSql + ")"; return jdbcTemplate.query(sql, rowMapper()); } @Override public Optional findByEmail(String email) { - String sql = "SELECT id, full_name, email, `role`, skills_and_interests, created_at FROM users WHERE email = ?"; + String sql = "SELECT id, full_name, email, `role`, password_hash, skills_and_interests, created_at FROM users WHERE email = ?"; return jdbcTemplate.query(sql, rowMapper(), email).stream().findFirst(); } @Override public List findAll() { String sql = "SELECT id, full_name, email, `role`, skills_and_interests, created_at FROM users"; - return jdbcTemplate.query(sql, rowMapper()); + return jdbcTemplate.query(sql, customRowMapper()); } } diff --git a/src/main/java/dev/pasinduog/eventsphere/service/OAuth2CodeService.java b/src/main/java/dev/pasinduog/eventsphere/service/OAuth2CodeService.java new file mode 100644 index 0000000..22eea90 --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/service/OAuth2CodeService.java @@ -0,0 +1,6 @@ +package dev.pasinduog.eventsphere.service; + +public interface OAuth2CodeService { + String generateCode(String email); + String validateCodeAndGetEmail(String code); +} diff --git a/src/main/java/dev/pasinduog/eventsphere/service/impl/OAuth2CodeServiceImpl.java b/src/main/java/dev/pasinduog/eventsphere/service/impl/OAuth2CodeServiceImpl.java new file mode 100644 index 0000000..98d855b --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/service/impl/OAuth2CodeServiceImpl.java @@ -0,0 +1,25 @@ +package dev.pasinduog.eventsphere.service.impl; + +import dev.pasinduog.eventsphere.service.OAuth2CodeService; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class OAuth2CodeServiceImpl implements OAuth2CodeService { + private final Map authCodes = new ConcurrentHashMap<>(); + + @Override + public String generateCode(String email) { + String code = UUID.randomUUID().toString(); + authCodes.put(code, email); + return code; + } + + @Override + public String validateCodeAndGetEmail(String code) { + return authCodes.remove(code); + } +} diff --git a/src/main/resources/db/migration/V3__insert_sample_data.sql b/src/main/resources/db/migration/V3__insert_sample_data.sql index ae631cf..f820e0b 100644 --- a/src/main/resources/db/migration/V3__insert_sample_data.sql +++ b/src/main/resources/db/migration/V3__insert_sample_data.sql @@ -3,16 +3,16 @@ -- 1. USERS (10 Records - Admins, Speakers, Attendees) INSERT INTO users (id, full_name, email, password_hash, role, skills_and_interests) VALUES - ('886a8539-5426-4c95-b50c-c54d72e55d28', 'Pasindu Owa Gamage', 'admin1@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ADMIN', 'System Architecture, Spring Boot, Angular'), - ('eb334f13-7461-4d28-9e0e-7b933a5e5e4e', 'Kasun Perera', 'admin2@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ADMIN', 'Event Management, Marketing'), - ('1ea3c9fd-ad68-4105-8220-3a15694bc083', 'Dr. Ruwan Kumara', 'speaker1@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'SPEAKER', 'AI, Machine Learning, Python'), - ('94d5ca84-a17c-45ed-8506-cf4220f5fa80', 'Sarah Jenkins', 'speaker2@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'SPEAKER', 'Cloud Computing, AWS, DevOps'), - ('7743f6b1-d520-4a8f-b7a5-18e1d384e0f4', 'Nimali Silva', 'nimali@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Java, Spring Boot, Looking for Internships'), - ('280ac445-578a-46c6-b879-7816c7c691bc', 'Chamara Fernando', 'chamara@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Angular, Frontend Development, UI/UX'), - ('67f35b78-02fe-4879-8fe5-04d3633d5a56', 'Lahiru Senanayake', 'lahiru@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Microservices, Docker, Kubernetes'), - ('f8bcd1ca-8ee9-4b29-a689-ee691614dd8d', 'Gayantha De Silva', 'gayantha@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Looking for open-source contributors, Java'), - ('a3affe9d-1b0e-444e-a883-a24221f9aeb2', 'Sanduni Perera', 'sanduni@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Data Science, Gemini AI APIs'), - ('5ff6e1d8-fa5c-4ff3-8e69-8d41ae1f728d', 'Tharindu Bandara', 'tharindu@gmail.com', '$2a$10$dXJ3SWoG7P50lGmMkkmwe.20cQQubK3.HCGJvuPEs4r3wX1fGjTMW', 'ATTENDEE', 'Spring Boot, Backend Development'); + ('886a8539-5426-4c95-b50c-c54d72e55d28', 'Pasindu Owa Gamage', 'admin1@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ADMIN', 'System Architecture, Spring Boot, Angular'), + ('eb334f13-7461-4d28-9e0e-7b933a5e5e4e', 'Kasun Perera', 'admin2@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ADMIN', 'Event Management, Marketing'), + ('1ea3c9fd-ad68-4105-8220-3a15694bc083', 'Dr. Ruwan Kumara', 'speaker1@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'SPEAKER', 'AI, Machine Learning, Python'), + ('94d5ca84-a17c-45ed-8506-cf4220f5fa80', 'Sarah Jenkins', 'speaker2@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'SPEAKER', 'Cloud Computing, AWS, DevOps'), + ('7743f6b1-d520-4a8f-b7a5-18e1d384e0f4', 'Nimali Silva', 'nimali@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ATTENDEE', 'Java, Spring Boot, Looking for Internships'), + ('280ac445-578a-46c6-b879-7816c7c691bc', 'Chamara Fernando', 'chamara@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ATTENDEE', 'Angular, Frontend Development, UI/UX'), + ('67f35b78-02fe-4879-8fe5-04d3633d5a56', 'Lahiru Senanayake', 'lahiru@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ATTENDEE', 'Microservices, Docker, Kubernetes'), + ('f8bcd1ca-8ee9-4b29-a689-ee691614dd8d', 'Gayantha De Silva', 'gayantha@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ATTENDEE', 'Looking for open-source contributors, Java'), + ('a3affe9d-1b0e-444e-a883-a24221f9aeb2', 'Sanduni Perera', 'sanduni@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ATTENDEE', 'Data Science, Gemini AI APIs'), + ('5ff6e1d8-fa5c-4ff3-8e69-8d41ae1f728d', 'Tharindu Bandara', 'tharindu@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ATTENDEE', 'Spring Boot, Backend Development'); -- 2. EVENTS (10 Records) INSERT INTO events (id, organizer_id, title, description, start_time, end_time, max_attendees, status) VALUES From a054335d03a4fa45e2a42ba1cc6ae823c4c088a8 Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Mon, 13 Apr 2026 14:54:36 +0530 Subject: [PATCH 05/20] feat: Enhance API security with OpenAPI configuration and role-based access control --- .../eventsphere/config/AppConfig.java | 27 +++++++++++++++++++ .../controller/EventController.java | 2 ++ .../controller/UserController.java | 15 ++++++++--- .../eventsphere/service/UserService.java | 1 + .../service/impl/UserServiceImpl.java | 5 ++++ .../db/migration/V1__init_schema.sql | 20 +++++++++++--- .../db/migration/V3__insert_sample_data.sql | 6 +++++ 7 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java index 26c1678..d70cef2 100644 --- a/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java +++ b/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java @@ -1,6 +1,11 @@ package dev.pasinduog.eventsphere.config; import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.JdkClientHttpRequestFactory; @@ -41,4 +46,26 @@ public Logger logger() { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean + public OpenAPI openApiConfig() { + final String securitySchemeName = "bearerAuth"; + + return new OpenAPI().info( + new Info() + .title("EventSphere API") + .description("This is the EventSphere API Documentation") + .version("1.0.0") + .termsOfService("https://github.com/pasinduog/eventsphere")) + .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) + .components(new Components() + .addSecuritySchemes(securitySchemeName, + new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description("Enter your JWT token. You can obtain it from the /api/auth/login endpoint.") + )); + } } diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java b/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java index 963583f..386ab38 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java @@ -24,6 +24,7 @@ List getUpcomingEvents() { } @GetMapping("/{eventId}/matches") + @PreAuthorize("isAuthenticated()") List getMatchSuggestions(@PathVariable String eventId, @RequestParam String userId) { return aiMatchmakingService.getMatchSuggestions(eventId, userId); } @@ -41,6 +42,7 @@ boolean registerEvent(@PathVariable String eventId, @RequestParam String userId) } @PostMapping("/{eventId}/matchmaking") + @PreAuthorize("isAuthenticated()") AiMatchResult generateNetworkingMatches(@PathVariable String eventId, @RequestParam String userId){ return aiMatchmakingService.generateMatchesForUser(eventId, userId); } diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java index 544cdd6..04c6e65 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java @@ -9,6 +9,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.Map; @RestController @@ -18,20 +19,28 @@ public class UserController { private final UserService userService; @GetMapping("/by-email") + @PreAuthorize("hasAuthority('ADMIN') or hasAuthority('ORGANIZER')") UserResponse getUserByEmail(@RequestParam String email) { return userService.getUserByEmail(email); } + @GetMapping("/me") + @PreAuthorize("isAuthenticated()") + UserResponse getCurrentUser(Principal principal) { + String email = principal.getName(); + return userService.getUserByEmail(email); + } + @PostMapping("/register") @ResponseStatus(HttpStatus.CREATED) boolean registerUser(@RequestBody RegisterRequest request) { return userService.registerUser(request); } - @PutMapping("/{userId}/profile") + @PutMapping("/me/profile") @PreAuthorize("isAuthenticated()") - boolean updateProfile(@PathVariable String userId, @RequestBody Map updates){ - User user = userService.getUserById(userId); + boolean updateProfile(Principal principal, @RequestBody Map updates){ + User user = userService.getUserEntityByEmail(principal.getName()); if (updates.containsKey("skillsAndInterests")) user.setSkillsAndInterests(updates.get("skillsAndInterests")); if (updates.containsKey("fullName")) user.setFullName(updates.get("fullName")); return userService.updateUser(user); diff --git a/src/main/java/dev/pasinduog/eventsphere/service/UserService.java b/src/main/java/dev/pasinduog/eventsphere/service/UserService.java index e1757c2..731ef0b 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/UserService.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/UserService.java @@ -9,4 +9,5 @@ public interface UserService { UserResponse getUserByEmail(String email); User getUserById(String userId); boolean updateUser(User user); + User getUserEntityByEmail(String email); } diff --git a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java index 6c12e6a..2386440 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java @@ -58,4 +58,9 @@ public User getUserById(String userId) { public boolean updateUser(User user) { return userRepository.update(user); } + + @Override + public User getUserEntityByEmail(String email) { + return userRepository.findByEmail(email).orElseThrow(() -> new UserNotFoundException("User not found")); + } } diff --git a/src/main/resources/db/migration/V1__init_schema.sql b/src/main/resources/db/migration/V1__init_schema.sql index 0c40f40..5a2994b 100644 --- a/src/main/resources/db/migration/V1__init_schema.sql +++ b/src/main/resources/db/migration/V1__init_schema.sql @@ -1,15 +1,26 @@ -- V1__init_schema.sql -- Database: virtual_events_db (MySQL) +-- 0.1 ROLES +CREATE TABLE roles ( + name VARCHAR(50) PRIMARY KEY +); + +-- 0.2 EVENT STATUSES +CREATE TABLE event_statuses ( + name VARCHAR(50) PRIMARY KEY +); + -- 1. USERS TABLE CREATE TABLE users ( id VARCHAR(36) PRIMARY KEY, full_name VARCHAR(150) NOT NULL, email VARCHAR(150) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, - role ENUM('ADMIN', 'ATTENDEE', 'SPEAKER') DEFAULT 'ATTENDEE', + role VARCHAR(50) DEFAULT 'ATTENDEE', skills_and_interests TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (role) REFERENCES roles(name) ); -- 2. EVENTS TABLE @@ -21,9 +32,10 @@ CREATE TABLE events ( start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, max_attendees INT NOT NULL, - status ENUM('UPCOMING', 'LIVE', 'COMPLETED') DEFAULT 'UPCOMING', + status VARCHAR(50) DEFAULT 'UPCOMING', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (organizer_id) REFERENCES users(id) ON DELETE CASCADE + FOREIGN KEY (organizer_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (status) REFERENCES event_statuses(name) ); -- 3. EVENT REGISTRATIONS (TICKETING) diff --git a/src/main/resources/db/migration/V3__insert_sample_data.sql b/src/main/resources/db/migration/V3__insert_sample_data.sql index f820e0b..f1e7e15 100644 --- a/src/main/resources/db/migration/V3__insert_sample_data.sql +++ b/src/main/resources/db/migration/V3__insert_sample_data.sql @@ -1,6 +1,9 @@ -- V3__insert_sample_data.sql -- Insert Sample Data for Testing +-- 0.1. ROLE (3 Records) +INSERT INTO roles (name) VALUES ('ADMIN'), ('ATTENDEE'), ('SPEAKER'); + -- 1. USERS (10 Records - Admins, Speakers, Attendees) INSERT INTO users (id, full_name, email, password_hash, role, skills_and_interests) VALUES ('886a8539-5426-4c95-b50c-c54d72e55d28', 'Pasindu Owa Gamage', 'admin1@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ADMIN', 'System Architecture, Spring Boot, Angular'), @@ -14,6 +17,9 @@ INSERT INTO users (id, full_name, email, password_hash, role, skills_and_interes ('a3affe9d-1b0e-444e-a883-a24221f9aeb2', 'Sanduni Perera', 'sanduni@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ATTENDEE', 'Data Science, Gemini AI APIs'), ('5ff6e1d8-fa5c-4ff3-8e69-8d41ae1f728d', 'Tharindu Bandara', 'tharindu@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ATTENDEE', 'Spring Boot, Backend Development'); +-- 0.2. EVENT STATUS (4 Records) +INSERT INTO event_statuses (name) VALUES ('UPCOMING'), ('LIVE'), ('COMPLETED'), ('UNAVAILABLE'); + -- 2. EVENTS (10 Records) INSERT INTO events (id, organizer_id, title, description, start_time, end_time, max_attendees, status) VALUES ('0a55db3f-f2bc-4c06-8a1f-10ba24747196', '886a8539-5426-4c95-b50c-c54d72e55d28', 'Sri Lanka Tech Summit 2026', 'The biggest tech summit in LK.', '2026-06-15 09:00:00', '2026-06-15 17:00:00', 500, 'UPCOMING'), From d8fc344747e619cf41ffdc88f2857452e8635c88 Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Mon, 13 Apr 2026 15:27:36 +0530 Subject: [PATCH 06/20] feat: Enhance GitHub OAuth2 user authentication by fetching email and supporting dynamic admin email configuration --- .../service/CustomOAuth2UserService.java | 67 +++++++++++++++++-- src/main/resources/application.yml | 4 +- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java index 30c1eea..71d9fc1 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java @@ -3,18 +3,29 @@ import dev.pasinduog.eventsphere.model.User; import dev.pasinduog.eventsphere.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.core.ParameterizedTypeReference; import java.time.LocalDateTime; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.logging.Logger; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; @Service @RequiredArgsConstructor @@ -23,10 +34,8 @@ public class CustomOAuth2UserService extends DefaultOAuth2UserService { private final Logger logger; private final PasswordEncoder passwordEncoder; - private final List adminEmails = List.of( - "pasinduogdev@gmail.com", - "pasinduogdev2@gmail.com" - ); + @Value("#{'${security.admin-emails:}'.empty ? {} : '${security.admin-emails:}'.split(',')}") + private final List adminEmails; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { @@ -34,7 +43,54 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic String email = oAuth2User.getAttribute("email"); String name = oAuth2User.getAttribute("name"); if (name == null) name = oAuth2User.getAttribute("login"); - if (email == null) throw new OAuth2AuthenticationException("Email not found from GitHub provider"); + + if (email == null) { + email = fetchEmailFromGitHub(userRequest.getAccessToken().getTokenValue()); + } + + if (email == null) { + throw new OAuth2AuthenticationException(new OAuth2Error("email_not_found", "Email not found from GitHub provider", null)); + } + + oAuth2User = enhanceOAuth2UserWithEmail(oAuth2User, email); + registerNewUserIfNeeded(email, name); + + return oAuth2User; + } + + private String fetchEmailFromGitHub(String token) { + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(token); + HttpEntity entity = new HttpEntity<>("", headers); + + ResponseEntity>> response = restTemplate.exchange( + "https://api.github.com/user/emails", + HttpMethod.GET, + entity, + new ParameterizedTypeReference<>() {} + ); + + List> emails = response.getBody(); + if (emails != null) { + for (Map emailObj : emails) { + if (Boolean.TRUE.equals(emailObj.get("primary")) && Boolean.TRUE.equals(emailObj.get("verified"))) { + return (String) emailObj.get("email"); + } + } + } + return null; + } + + private OAuth2User enhanceOAuth2UserWithEmail(OAuth2User oAuth2User, String email) { + Map attributes = new HashMap<>(oAuth2User.getAttributes()); + if (!attributes.containsKey("email") || attributes.get("email") == null) { + attributes.put("email", email); + } + return new DefaultOAuth2User(oAuth2User.getAuthorities(), attributes, "email"); + } + + private void registerNewUserIfNeeded(String email, String name) { Optional userOptional = userRepository.findByEmail(email); if (userOptional.isEmpty()) { User user = new User(); @@ -55,6 +111,5 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic } else { logger.warning("User already exists"); } - return oAuth2User; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7754179..8976fdf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,6 +16,7 @@ spring: github: client-id: ${GITHUB_CLIENT_ID} client-secret: ${GITHUB_CLIENT_SECRET} + scope: user:email, read:user Gemini: api: @@ -25,4 +26,5 @@ Gemini: security: jwt: key: ${JWT_KEY} - expiration: 1640995200 \ No newline at end of file + expiration: 1640995200 + admin-emails: pasinduogdev@gmail.com \ No newline at end of file From 962d9165cfdd69e174d1a6b89b81cb9917b2d076 Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Mon, 13 Apr 2026 15:31:09 +0530 Subject: [PATCH 07/20] fix: Correct casing for Gemini API key and URL configuration properties --- .../eventsphere/service/impl/AiMatchmakingServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/service/impl/AiMatchmakingServiceImpl.java b/src/main/java/dev/pasinduog/eventsphere/service/impl/AiMatchmakingServiceImpl.java index 543061d..6ab9e5a 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/impl/AiMatchmakingServiceImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/impl/AiMatchmakingServiceImpl.java @@ -31,10 +31,10 @@ public class AiMatchmakingServiceImpl implements AiMatchmakingService { private final AiMatchSuggestionRepository aiMatchSuggestionRepository; private final EventRegistrationRepository eventRegistrationRepository; - @Value("${gemini.api.key}") + @Value("${Gemini.api.key}") private String apiKey; - @Value("${gemini.api.url}") + @Value("${Gemini.api.url}") private String apiUrl; @Override From 40a0e79f64fbdb68bca96c0e720e5410b3bc50f7 Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Mon, 13 Apr 2026 15:35:51 +0530 Subject: [PATCH 08/20] feat: Add user retrieval by ID with role-based access control and response formatting --- .../eventsphere/controller/UserController.java | 6 ++++++ .../pasinduog/eventsphere/service/UserService.java | 2 +- .../eventsphere/service/impl/UserServiceImpl.java | 12 ++++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java index 04c6e65..636e21b 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java @@ -24,6 +24,12 @@ UserResponse getUserByEmail(@RequestParam String email) { return userService.getUserByEmail(email); } + @GetMapping + @PreAuthorize("hasAuthority('ADMIN') or hasAuthority('ORGANIZER')") + UserResponse getUserById(@RequestParam String userId) { + return userService.getUserById(userId); + } + @GetMapping("/me") @PreAuthorize("isAuthenticated()") UserResponse getCurrentUser(Principal principal) { diff --git a/src/main/java/dev/pasinduog/eventsphere/service/UserService.java b/src/main/java/dev/pasinduog/eventsphere/service/UserService.java index 731ef0b..4d4aac1 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/UserService.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/UserService.java @@ -7,7 +7,7 @@ public interface UserService { boolean registerUser(RegisterRequest request); UserResponse getUserByEmail(String email); - User getUserById(String userId); + UserResponse getUserById(String userId); boolean updateUser(User user); User getUserEntityByEmail(String email); } diff --git a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java index 2386440..d292350 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java @@ -49,9 +49,17 @@ public UserResponse getUserByEmail(String email) { } @Override - public User getUserById(String userId) { - return userRepository.findById(userId) + public UserResponse getUserById(String userId) { + User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId)); + + return new UserResponse( + user.getId(), + user.getFullName(), + user.getEmail(), + user.getRole(), + user.getSkillsAndInterests() + ); } @Override From 1f8eaaee87b93fca10157bb23842b92cf0bbd75e Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Mon, 13 Apr 2026 15:46:07 +0530 Subject: [PATCH 09/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java index d70cef2..f5ca726 100644 --- a/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java +++ b/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java @@ -65,7 +65,7 @@ public OpenAPI openApiConfig() { .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT") - .description("Enter your JWT token. You can obtain it from the /api/auth/login endpoint.") + .description("Enter your JWT token. You can obtain it from the /api/v1/auth/login endpoint.") )); } } From 1d05a888f5aa3abdc6a2b22d60de0ba9993e3ffc Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Mon, 13 Apr 2026 15:46:23 +0530 Subject: [PATCH 10/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../pasinduog/eventsphere/config/SecurityConfig.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java index 916ddc4..0541138 100644 --- a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java +++ b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java @@ -34,7 +34,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .cors(cors -> cors.configurationSource(corsConfigurationSource())) // අනිවාර්යයි! .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/**", "/api/v1/users/register", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html", "/ws-event-chat/**").permitAll() + .requestMatchers( + "/api/v1/auth/**", + "/api/v1/users/register", + "/swagger-ui/**", + "/v3/api-docs/**", + "/swagger-ui.html", + "/ws-event-chat/**", + "/oauth2/authorization/**", + "/login/oauth2/code/**" + ).permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) From 4c0e08b85257b7a4ff3589b965ba174460417502 Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Mon, 13 Apr 2026 15:46:50 +0530 Subject: [PATCH 11/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../java/dev/pasinduog/eventsphere/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java index 0541138..b221108 100644 --- a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java +++ b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java @@ -46,7 +46,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { ).permitAll() .anyRequest().authenticated() ) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) .addFilterBefore(jwtAuthFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo From d7f7ed523562952ad0d1f309a18848153c1adba5 Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Mon, 13 Apr 2026 15:47:26 +0530 Subject: [PATCH 12/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../eventsphere/config/SecurityConfig.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java index b221108..198d77f 100644 --- a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java +++ b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java @@ -4,6 +4,7 @@ import dev.pasinduog.eventsphere.service.CustomOAuth2UserService; import dev.pasinduog.eventsphere.service.OAuth2CodeService; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -16,6 +17,7 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.util.UriComponentsBuilder; import java.util.List; @@ -28,6 +30,9 @@ public class SecurityConfig { private final JwtAuthFilter jwtAuthFilter; private final OAuth2CodeService oAuth2CodeService; + @Value("${app.frontend.base-url}") + private String frontendBaseUrl; + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) { http @@ -57,9 +62,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { if (oauthUser != null && oauthUser.getAttribute("email") != null) { String email = oauthUser.getAttribute("email"); String code = oAuth2CodeService.generateCode(email); - response.sendRedirect("http://localhost:4200/login?code=" + code); + response.sendRedirect(buildFrontendLoginRedirect("code", code)); } else { - response.sendRedirect("http://localhost:4200/login?error=github_email_missing"); + response.sendRedirect(buildFrontendLoginRedirect("error", "github_email_missing")); } }) ) @@ -70,10 +75,18 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { return http.build(); } + private String buildFrontendLoginRedirect(String parameterName, String parameterValue) { + return UriComponentsBuilder.fromHttpUrl(frontendBaseUrl) + .path("/login") + .queryParam(parameterName, parameterValue) + .build() + .toUriString(); + } + @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(List.of("http://localhost:4200")); + configuration.setAllowedOrigins(List.of(frontendBaseUrl)); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type")); configuration.setAllowCredentials(true); From 1c6149afd375be1a14d9e3db9765ebde1017de7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:12:13 +0000 Subject: [PATCH 13/20] fix: default role to ATTENDEE when null in getAuthorities(), fix fromHttpUrl removed in Spring 7 Agent-Logs-Url: https://github.com/PasinduOG/eventsphere/sessions/e1363c15-ab4d-4386-9e06-36516a988e9b Co-authored-by: PasinduOG <126347762+PasinduOG@users.noreply.github.com> --- .../dev/pasinduog/eventsphere/config/SecurityConfig.java | 2 +- src/main/java/dev/pasinduog/eventsphere/model/User.java | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java index 198d77f..79408d9 100644 --- a/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java +++ b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java @@ -76,7 +76,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { } private String buildFrontendLoginRedirect(String parameterName, String parameterValue) { - return UriComponentsBuilder.fromHttpUrl(frontendBaseUrl) + return UriComponentsBuilder.fromUriString(frontendBaseUrl) .path("/login") .queryParam(parameterName, parameterValue) .build() diff --git a/src/main/java/dev/pasinduog/eventsphere/model/User.java b/src/main/java/dev/pasinduog/eventsphere/model/User.java index 9304668..f2004ba 100644 --- a/src/main/java/dev/pasinduog/eventsphere/model/User.java +++ b/src/main/java/dev/pasinduog/eventsphere/model/User.java @@ -4,7 +4,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -29,9 +28,9 @@ public class User implements UserDetails { @Override - @NullMarked public Collection getAuthorities() { - return List.of(new SimpleGrantedAuthority(this.role)); + String effectiveRole = (this.role != null) ? this.role : "ATTENDEE"; + return List.of(new SimpleGrantedAuthority(effectiveRole)); } @Override @@ -40,7 +39,6 @@ public Collection getAuthorities() { } @Override - @NullMarked public String getUsername() { return this.email; } From 7a7a3b51945fc568e9d620b90a0aa49c3b8dca60 Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Mon, 13 Apr 2026 23:55:46 +0530 Subject: [PATCH 14/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../eventsphere/repository/impl/UserRepositoryImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java b/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java index 82174de..69357f5 100644 --- a/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/repository/impl/UserRepositoryImpl.java @@ -79,7 +79,7 @@ public List findByIds(List ids) { } String inSql = String.join(",", java.util.Collections.nCopies(ids.size(), "?")); String sql = "SELECT id, full_name, email, `role`, password_hash, skills_and_interests, created_at FROM users WHERE id IN (" + inSql + ")"; - return jdbcTemplate.query(sql, rowMapper()); + return jdbcTemplate.query(sql, rowMapper(), ids.toArray()); } @Override From 0fa650eb8d345158e00b22bbd606ce4d08cc039e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:35:07 +0000 Subject: [PATCH 15/20] fix: remove final from adminEmails for @Value field injection; add error handling in fetchEmailFromGitHub Agent-Logs-Url: https://github.com/PasinduOG/eventsphere/sessions/2c67ab62-fd77-4811-9b65-06d6ca0e8b0f Co-authored-by: PasinduOG <126347762+PasinduOG@users.noreply.github.com> --- .../service/CustomOAuth2UserService.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java index 71d9fc1..97943dc 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java @@ -11,6 +11,7 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpEntity; @@ -35,7 +36,7 @@ public class CustomOAuth2UserService extends DefaultOAuth2UserService { private final PasswordEncoder passwordEncoder; @Value("#{'${security.admin-emails:}'.empty ? {} : '${security.admin-emails:}'.split(',')}") - private final List adminEmails; + private List adminEmails; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { @@ -64,12 +65,19 @@ private String fetchEmailFromGitHub(String token) { headers.setBearerAuth(token); HttpEntity entity = new HttpEntity<>("", headers); - ResponseEntity>> response = restTemplate.exchange( - "https://api.github.com/user/emails", - HttpMethod.GET, - entity, - new ParameterizedTypeReference<>() {} - ); + ResponseEntity>> response; + try { + response = restTemplate.exchange( + "https://api.github.com/user/emails", + HttpMethod.GET, + entity, + new ParameterizedTypeReference<>() {} + ); + } catch (RestClientException ex) { + throw new OAuth2AuthenticationException( + new OAuth2Error("github_email_fetch_failed", + "Failed to fetch email from GitHub: " + ex.getMessage(), null)); + } List> emails = response.getBody(); if (emails != null) { From c372f04d33207ecf26c073a5eab1863d16bc1424 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:36:57 +0000 Subject: [PATCH 16/20] fix: use generic error message and log exception in fetchEmailFromGitHub Agent-Logs-Url: https://github.com/PasinduOG/eventsphere/sessions/2c67ab62-fd77-4811-9b65-06d6ca0e8b0f Co-authored-by: PasinduOG <126347762+PasinduOG@users.noreply.github.com> --- .../pasinduog/eventsphere/service/CustomOAuth2UserService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java index 97943dc..797ae70 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java @@ -74,9 +74,10 @@ private String fetchEmailFromGitHub(String token) { new ParameterizedTypeReference<>() {} ); } catch (RestClientException ex) { + logger.warning("Failed to fetch email from GitHub API: " + ex.getMessage()); throw new OAuth2AuthenticationException( new OAuth2Error("github_email_fetch_failed", - "Failed to fetch email from GitHub: " + ex.getMessage(), null)); + "Unable to retrieve email address from GitHub. Please ensure your GitHub account has a verified primary email.", null)); } List> emails = response.getBody(); From 82d5d2962990a505422a030425cf4bc77f4a4c75 Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Tue, 14 Apr 2026 22:27:26 +0530 Subject: [PATCH 17/20] feat: update application configuration and enhance user authentication flow --- .../controller/UserController.java | 4 ++-- .../eventsphere/filter/JwtAuthFilter.java | 8 ++++++-- .../dev/pasinduog/eventsphere/model/User.java | 3 ++- .../service/CustomOAuth2UserService.java | 8 ++------ .../service/impl/OAuth2CodeServiceImpl.java | 19 ++++++++++++++----- .../service/impl/UserServiceImpl.java | 3 ++- src/main/resources/application.yml | 13 +++++++++++-- 7 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java index 636e21b..4516c34 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java @@ -19,13 +19,13 @@ public class UserController { private final UserService userService; @GetMapping("/by-email") - @PreAuthorize("hasAuthority('ADMIN') or hasAuthority('ORGANIZER')") + @PreAuthorize("hasAuthority('ADMIN')") UserResponse getUserByEmail(@RequestParam String email) { return userService.getUserByEmail(email); } @GetMapping - @PreAuthorize("hasAuthority('ADMIN') or hasAuthority('ORGANIZER')") + @PreAuthorize("hasAuthority('ADMIN')") UserResponse getUserById(@RequestParam String userId) { return userService.getUserById(userId); } diff --git a/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java b/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java index 0dbef97..db3f0c3 100644 --- a/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java +++ b/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java @@ -34,8 +34,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } jwt = authHeader.substring(7); - userEmail = jwtService.extractUsername(jwt); - + try { + userEmail = jwtService.extractUsername(jwt); + } catch (RuntimeException e) { + filterChain.doFilter(request, response); + return; + } if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { User user = userRepository.findByEmail(userEmail).orElse(null); if (user != null && jwtService.isTokenValid(jwt, user)) { diff --git a/src/main/java/dev/pasinduog/eventsphere/model/User.java b/src/main/java/dev/pasinduog/eventsphere/model/User.java index f2004ba..ff9b625 100644 --- a/src/main/java/dev/pasinduog/eventsphere/model/User.java +++ b/src/main/java/dev/pasinduog/eventsphere/model/User.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -17,6 +18,7 @@ @NoArgsConstructor @Getter @Setter +@NullMarked public class User implements UserDetails { private String id; private String fullName; @@ -42,5 +44,4 @@ public Collection getAuthorities() { public String getUsername() { return this.email; } - } \ No newline at end of file diff --git a/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java index 797ae70..cec9745 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java @@ -20,11 +20,7 @@ import org.springframework.core.ParameterizedTypeReference; import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.logging.Logger; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; @@ -106,7 +102,7 @@ private void registerNewUserIfNeeded(String email, String name) { user.setId(UUID.randomUUID().toString()); user.setFullName(name); user.setEmail(email); - user.setPasswordHash(passwordEncoder.encode(UUID.randomUUID().toString())); + user.setPasswordHash(Objects.requireNonNull(passwordEncoder.encode(UUID.randomUUID().toString()))); if (adminEmails.contains(email)) { user.setRole("ADMIN"); } else { diff --git a/src/main/java/dev/pasinduog/eventsphere/service/impl/OAuth2CodeServiceImpl.java b/src/main/java/dev/pasinduog/eventsphere/service/impl/OAuth2CodeServiceImpl.java index 98d855b..1662a0d 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/impl/OAuth2CodeServiceImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/impl/OAuth2CodeServiceImpl.java @@ -1,25 +1,34 @@ package dev.pasinduog.eventsphere.service.impl; import dev.pasinduog.eventsphere.service.OAuth2CodeService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; -import java.util.Map; +import java.time.Duration; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; @Service +@RequiredArgsConstructor public class OAuth2CodeServiceImpl implements OAuth2CodeService { - private final Map authCodes = new ConcurrentHashMap<>(); + + private final StringRedisTemplate redisTemplate; + private static final Duration EXPIRATION_TIME = Duration.ofMinutes(5); @Override public String generateCode(String email) { String code = UUID.randomUUID().toString(); - authCodes.put(code, email); + redisTemplate.opsForValue().set("oauth2_code:" + code, email, EXPIRATION_TIME); return code; } @Override public String validateCodeAndGetEmail(String code) { - return authCodes.remove(code); + String key = "oauth2_code:" + code; + String email = redisTemplate.opsForValue().get(key); + if (email == null) { + redisTemplate.delete(key); + } + return email; } } diff --git a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java index d292350..4b60aa9 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java @@ -11,6 +11,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.Objects; import java.util.UUID; @Service @@ -28,7 +29,7 @@ public boolean registerUser(RegisterRequest request) { user.setId(UUID.randomUUID().toString()); user.setFullName(request.fullName()); user.setEmail(request.email()); - user.setPasswordHash(passwordEncoder.encode(request.password())); + user.setPasswordHash(Objects.requireNonNull(passwordEncoder.encode(request.password()))); user.setRole("ATTENDEE"); user.setSkillsAndInterests(request.skillsAndInterests()); return userRepository.save(user); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8976fdf..b7134b4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,7 +1,7 @@ spring: datasource: url: jdbc:mysql://localhost:3306/virtual_events_db?createDatabaseIfNotExist=true - username: root + username: ${DB_USERNAME} password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver @@ -18,6 +18,11 @@ spring: client-secret: ${GITHUB_CLIENT_SECRET} scope: user:email, read:user + data: + redis: + host: localhost + port: 6379 + Gemini: api: url: https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent @@ -27,4 +32,8 @@ security: jwt: key: ${JWT_KEY} expiration: 1640995200 - admin-emails: pasinduogdev@gmail.com \ No newline at end of file + admin-emails: pasinduogdev@gmail.com + +app: + frontend: + base-url: ${BASE_URL} \ No newline at end of file From 24c4cb9a7d3bdfcb08d2aaa5d5eef63d3e7abefa Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Tue, 14 Apr 2026 22:28:10 +0530 Subject: [PATCH 18/20] feat: update application configuration and enhance user authentication flow --- .../java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java b/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java index db3f0c3..b35912e 100644 --- a/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java +++ b/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java @@ -8,6 +8,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.NullMarked; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -23,6 +24,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final UserRepository userRepository; @Override + @NullMarked protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); final String jwt; From 2590284caf0649f421dcece82946b7617f7d5237 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:03:16 +0000 Subject: [PATCH 19/20] =?UTF-8?q?Remove=20redundant=20null=20check=20in=20?= =?UTF-8?q?getAuthorities()=20=E2=80=94=20role=20is=20non-null=20under=20@?= =?UTF-8?q?NullMarked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/PasinduOG/eventsphere/sessions/6c9317dc-6684-4dc3-ba7c-c99d6875a8b1 Co-authored-by: PasinduOG <126347762+PasinduOG@users.noreply.github.com> --- src/main/java/dev/pasinduog/eventsphere/model/User.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/model/User.java b/src/main/java/dev/pasinduog/eventsphere/model/User.java index ff9b625..6974c2e 100644 --- a/src/main/java/dev/pasinduog/eventsphere/model/User.java +++ b/src/main/java/dev/pasinduog/eventsphere/model/User.java @@ -31,8 +31,7 @@ public class User implements UserDetails { @Override public Collection getAuthorities() { - String effectiveRole = (this.role != null) ? this.role : "ATTENDEE"; - return List.of(new SimpleGrantedAuthority(effectiveRole)); + return List.of(new SimpleGrantedAuthority(this.role)); } @Override From 0fc10b040ba46ded6b848569ba43b6264c5e5851 Mon Sep 17 00:00:00 2001 From: Pasindu Owa Gamage Date: Tue, 14 Apr 2026 22:37:12 +0530 Subject: [PATCH 20/20] fix: simplify role assignment in getAuthorities method --- src/main/java/dev/pasinduog/eventsphere/model/User.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/dev/pasinduog/eventsphere/model/User.java b/src/main/java/dev/pasinduog/eventsphere/model/User.java index ff9b625..6974c2e 100644 --- a/src/main/java/dev/pasinduog/eventsphere/model/User.java +++ b/src/main/java/dev/pasinduog/eventsphere/model/User.java @@ -31,8 +31,7 @@ public class User implements UserDetails { @Override public Collection getAuthorities() { - String effectiveRole = (this.role != null) ? this.role : "ATTENDEE"; - return List.of(new SimpleGrantedAuthority(effectiveRole)); + return List.of(new SimpleGrantedAuthority(this.role)); } @Override