diff --git a/pom.xml b/pom.xml
index f35a823..0bfbb1f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,6 +51,18 @@
spring-boot-starter-flyway
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
+
org.flywaydb
@@ -88,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/AppConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java
index c238438..f5ca726 100644
--- a/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java
+++ b/src/main/java/dev/pasinduog/eventsphere/config/AppConfig.java
@@ -1,9 +1,16 @@
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;
+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 +41,31 @@ public RestClient restClient() {
public Logger logger() {
return Logger.getLogger("dev.pasinduog.eventsphere.config");
}
+
+ @Bean
+ 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/v1/auth/login endpoint.")
+ ));
+ }
}
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..79408d9
--- /dev/null
+++ b/src/main/java/dev/pasinduog/eventsphere/config/SecurityConfig.java
@@ -0,0 +1,97 @@
+package dev.pasinduog.eventsphere.config;
+
+import dev.pasinduog.eventsphere.filter.JwtAuthFilter;
+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;
+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;
+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 org.springframework.web.util.UriComponentsBuilder;
+
+import java.util.List;
+
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+ private final CustomOAuth2UserService customOAuth2UserService;
+ private final JwtAuthFilter jwtAuthFilter;
+ private final OAuth2CodeService oAuth2CodeService;
+
+ @Value("${app.frontend.base-url}")
+ private String frontendBaseUrl;
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) {
+ 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/**",
+ "/oauth2/authorization/**",
+ "/login/oauth2/code/**"
+ ).permitAll()
+ .anyRequest().authenticated()
+ )
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
+ .addFilterBefore(jwtAuthFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class)
+ .oauth2Login(oauth2 -> oauth2
+ .userInfoEndpoint(userInfo -> userInfo
+ .userService(customOAuth2UserService)
+ )
+ .successHandler((request, response, authentication) -> {
+ DefaultOAuth2User oauthUser = (DefaultOAuth2User) authentication.getPrincipal();
+ if (oauthUser != null && oauthUser.getAttribute("email") != null) {
+ String email = oauthUser.getAttribute("email");
+ String code = oAuth2CodeService.generateCode(email);
+ response.sendRedirect(buildFrontendLoginRedirect("code", code));
+ } else {
+ response.sendRedirect(buildFrontendLoginRedirect("error", "github_email_missing"));
+ }
+ })
+ )
+ .logout(logout -> logout
+ .logoutUrl("/api/v1/auth/logout")
+ .logoutSuccessHandler((request, response, authentication) -> response.setStatus(200))
+ );
+ return http.build();
+ }
+
+ private String buildFrontendLoginRedirect(String parameterName, String parameterValue) {
+ return UriComponentsBuilder.fromUriString(frontendBaseUrl)
+ .path("/login")
+ .queryParam(parameterName, parameterValue)
+ .build()
+ .toUriString();
+ }
+
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+ 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);
+ 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..3e1fc41
--- /dev/null
+++ b/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java
@@ -0,0 +1,53 @@
+package dev.pasinduog.eventsphere.controller;
+
+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;
+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;
+ private final OAuth2CodeService oAuth2CodeService;
+
+ @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);
+ }
+
+ @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..386ab38 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;
@@ -23,21 +24,25 @@ List getUpcomingEvents() {
}
@GetMapping("/{eventId}/matches")
+ @PreAuthorize("isAuthenticated()")
List getMatchSuggestions(@PathVariable String eventId, @RequestParam String userId) {
return aiMatchmakingService.getMatchSuggestions(eventId, userId);
}
@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);
}
@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 4ba99b3..4516c34 100644
--- a/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java
+++ b/src/main/java/dev/pasinduog/eventsphere/controller/UserController.java
@@ -1,26 +1,54 @@
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;
+import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
+import java.security.Principal;
+import java.util.Map;
+
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
+ @GetMapping("/by-email")
+ @PreAuthorize("hasAuthority('ADMIN')")
+ UserResponse getUserByEmail(@RequestParam String email) {
+ return userService.getUserByEmail(email);
+ }
+
+ @GetMapping
+ @PreAuthorize("hasAuthority('ADMIN')")
+ UserResponse getUserById(@RequestParam String userId) {
+ return userService.getUserById(userId);
+ }
+
+ @GetMapping("/me")
+ @PreAuthorize("isAuthenticated()")
+ UserResponse getCurrentUser(Principal principal) {
+ String email = principal.getName();
+ return userService.getUserByEmail(email);
+ }
+
@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")
- UserResponse getUserByEmail(@RequestParam String email) {
- return userService.getUserByEmail(email);
+ @PutMapping("/me/profile")
+ @PreAuthorize("isAuthenticated()")
+ 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/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/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/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/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/filter/JwtAuthFilter.java b/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java
new file mode 100644
index 0000000..b35912e
--- /dev/null
+++ b/src/main/java/dev/pasinduog/eventsphere/filter/JwtAuthFilter.java
@@ -0,0 +1,60 @@
+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.jspecify.annotations.NullMarked;
+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
+ @NullMarked
+ 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);
+ 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)) {
+ 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..6974c2e 100644
--- a/src/main/java/dev/pasinduog/eventsphere/model/User.java
+++ b/src/main/java/dev/pasinduog/eventsphere/model/User.java
@@ -4,14 +4,22 @@
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 {
+@NullMarked
+public class User implements UserDetails {
private String id;
private String fullName;
private String email;
@@ -19,4 +27,20 @@ public class User {
private String role;
private String skillsAndInterests;
private LocalDateTime createdAt;
+
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of(new SimpleGrantedAuthority(this.role));
+ }
+
+ @Override
+ public @Nullable String getPassword() {
+ return this.passwordHash;
+ }
+
+ @Override
+ public String getUsername() {
+ return this.email;
+ }
}
\ No newline at end of file
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..69357f5 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"));
@@ -46,9 +60,15 @@ 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 = ?";
+ 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();
}
@@ -58,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 + ")";
- return jdbcTemplate.query(sql, rowMapper());
+ 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(), ids.toArray());
}
@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/CustomOAuth2UserService.java b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java
new file mode 100644
index 0000000..cec9745
--- /dev/null
+++ b/src/main/java/dev/pasinduog/eventsphere/service/CustomOAuth2UserService.java
@@ -0,0 +1,120 @@
+package dev.pasinduog.eventsphere.service;
+
+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.RestClientException;
+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.*;
+import java.util.logging.Logger;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+
+@Service
+@RequiredArgsConstructor
+public class CustomOAuth2UserService extends DefaultOAuth2UserService {
+ private final UserRepository userRepository;
+ private final Logger logger;
+ private final PasswordEncoder passwordEncoder;
+
+ @Value("#{'${security.admin-emails:}'.empty ? {} : '${security.admin-emails:}'.split(',')}")
+ private List adminEmails;
+
+ @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) {
+ 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;
+ try {
+ response = restTemplate.exchange(
+ "https://api.github.com/user/emails",
+ HttpMethod.GET,
+ entity,
+ 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",
+ "Unable to retrieve email address from GitHub. Please ensure your GitHub account has a verified primary email.", null));
+ }
+
+ List