-
Notifications
You must be signed in to change notification settings - Fork 0
[Feature] Add a simple frontend (bundled) #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,17 @@ | ||
| # General | ||
| .gradle/ | ||
| .idea/ | ||
| build/ | ||
| target/ | ||
| .DS_Store | ||
| logs/ | ||
| .vscode/ | ||
|
|
||
| # Environment files (contain secrets) | ||
| .env | ||
| .env.local | ||
| .env.*.local | ||
| .env.*.local | ||
|
|
||
| # Eclipse | ||
| bin/ | ||
| *.class |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| eclipse.preferences.version=1 | ||
| org.eclipse.jdt.apt.aptEnabled=true | ||
| org.eclipse.jdt.apt.genSrcDir=bin/generated-sources/annotations | ||
| org.eclipse.jdt.apt.genTestSrcDir=bin/generated-test-sources/annotations |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| eclipse.preferences.version=1 | ||
| org.eclipse.jdt.core.classpath.outputOverlappingAnotherSource=ignore | ||
| org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore | ||
| org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull | ||
| org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault | ||
| org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable | ||
| org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled | ||
| org.eclipse.jdt.core.compiler.codegen.targetPlatform=25 | ||
| org.eclipse.jdt.core.compiler.compliance=25 | ||
| org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning | ||
| org.eclipse.jdt.core.compiler.problem.nullReference=warning | ||
| org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning | ||
| org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning | ||
| org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled | ||
| org.eclipse.jdt.core.compiler.processAnnotations=enabled | ||
| org.eclipse.jdt.core.compiler.source=25 | ||
|
Comment on lines
+1
to
+16
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||
| # User Service - Java/Spring Boot | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| A comprehensive user authentication and management service built with **Java 25** and **Spring Boot 4**, architected as a **modular monolith** using **Spring Modulith**. | ||||||||||||||||||||||||||||
| A comprehensive user authentication and management service built with **Java 25** and **Spring Boot 4**, architected as | ||||||||||||||||||||||||||||
| a **modular monolith** using **Spring Modulith**. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ## Features | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -335,6 +336,14 @@ spring: | |||||||||||||||||||||||||||
| password: ${DATABASE_PASSWORD} | ||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ## Users | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| - username/password: daniel1@yopmail.com/daniel@Pass01 | ||||||||||||||||||||||||||||
| - OTP: nkcoder.24@yopmail.com | ||||||||||||||||||||||||||||
| - OAuth2: | ||||||||||||||||||||||||||||
| - Github: daniel5hbs@gmail.com | ||||||||||||||||||||||||||||
| - Google: daniel5hbs@gmail.com | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
Comment on lines
+341
to
+346
|
||||||||||||||||||||||||||||
| - username/password: daniel1@yopmail.com/daniel@Pass01 | |
| - OTP: nkcoder.24@yopmail.com | |
| - OAuth2: | |
| - Github: daniel5hbs@gmail.com | |
| - Google: daniel5hbs@gmail.com | |
| The following examples are **for local development with a fresh database only**. They are placeholders and | |
| are **not real accounts**. In your own environment, register your own test users through the normal flows. | |
| - Username/password example: `user@example.com` / `<your-strong-password>` | |
| - OTP example: `otp-user@example.com` | |
| - OAuth2 examples (create these in your own providers and configure locally): | |
| - GitHub: `github-test-user@example.com` | |
| - Google: `google-test-user@example.com` |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,12 @@ | ||||||||
| #!/usr/bin/env sh | ||||||||
|
|
||||||||
| # Need to export the mail credentials for local run | ||||||||
| #export MAIL_USERNAME= | ||||||||
| #export MAIL_PASSWORD= | ||||||||
| ./gradlew bootRun --args='--spring.profiles.active=local' --no-daemon | ||||||||
| # Load environment variables from .env file if it exists | ||||||||
| if [ -f .env ]; then | ||||||||
| set -a | ||||||||
| . ./.env | ||||||||
| set +a | ||||||||
| fi | ||||||||
|
|
||||||||
| # --no-configuration-cache ensures Gradle reads fresh environment variables | ||||||||
| # --rerun-tasks ensures bootRun actually runs | ||||||||
| ./gradlew bootRun --args='--spring.profiles.active=local' --no-daemon --no-configuration-cache --rerun-tasks | ||||||||
|
Comment on lines
+11
to
+12
|
||||||||
| # --rerun-tasks ensures bootRun actually runs | |
| ./gradlew bootRun --args='--spring.profiles.active=local' --no-daemon --no-configuration-cache --rerun-tasks | |
| ./gradlew bootRun --args='--spring.profiles.active=local' --no-daemon --no-configuration-cache |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package org.nkcoder.user.infrastructure.security; | ||
|
|
||
| import java.net.URI; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.core.ParameterizedTypeReference; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.HttpMethod; | ||
| import org.springframework.http.RequestEntity; | ||
| import org.springframework.http.ResponseEntity; | ||
| 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.DefaultOAuth2User; | ||
| import org.springframework.security.oauth2.core.user.OAuth2User; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.client.RestTemplate; | ||
|
|
||
| /** | ||
| * Custom OAuth2UserService that fetches additional user info (like email) from providers that don't include it in the | ||
| * standard user info response. | ||
| */ | ||
| @Component | ||
| public class CustomOAuth2UserService extends DefaultOAuth2UserService { | ||
|
|
||
| private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class); | ||
| private static final String GITHUB_EMAILS_URL = "https://api.github.com/user/emails"; | ||
|
|
||
| private final RestTemplate restTemplate = new RestTemplate(); | ||
|
||
|
|
||
| @Override | ||
| public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { | ||
| OAuth2User oAuth2User = super.loadUser(userRequest); | ||
|
|
||
| String registrationId = userRequest.getClientRegistration().getRegistrationId(); | ||
|
|
||
| if ("github".equalsIgnoreCase(registrationId)) { | ||
| return enrichGitHubUser(userRequest, oAuth2User); | ||
| } | ||
|
|
||
| return oAuth2User; | ||
| } | ||
|
|
||
| private OAuth2User enrichGitHubUser(OAuth2UserRequest userRequest, OAuth2User oAuth2User) { | ||
| String email = oAuth2User.getAttribute("email"); | ||
|
|
||
| if (email == null || email.isBlank()) { | ||
| logger.debug("GitHub user email not in attributes, fetching from /user/emails"); | ||
| email = fetchGitHubPrimaryEmail(userRequest.getAccessToken().getTokenValue()); | ||
| } | ||
|
|
||
| if (email != null) { | ||
| // Create new attributes map with email included | ||
| Map<String, Object> attributes = new HashMap<>(oAuth2User.getAttributes()); | ||
| attributes.put("email", email); | ||
|
|
||
| return new DefaultOAuth2User( | ||
| oAuth2User.getAuthorities(), | ||
| attributes, | ||
| userRequest | ||
| .getClientRegistration() | ||
| .getProviderDetails() | ||
| .getUserInfoEndpoint() | ||
| .getUserNameAttributeName()); | ||
| } | ||
|
|
||
| return oAuth2User; | ||
| } | ||
|
|
||
| private String fetchGitHubPrimaryEmail(String accessToken) { | ||
| try { | ||
| HttpHeaders headers = new HttpHeaders(); | ||
| headers.setBearerAuth(accessToken); | ||
| headers.set("Accept", "application/vnd.github+json"); | ||
|
|
||
| RequestEntity<Void> request = new RequestEntity<>(headers, HttpMethod.GET, URI.create(GITHUB_EMAILS_URL)); | ||
|
|
||
| ResponseEntity<List<Map<String, Object>>> response = | ||
| restTemplate.exchange(request, new ParameterizedTypeReference<>() {}); | ||
|
|
||
| List<Map<String, Object>> emails = response.getBody(); | ||
| if (emails == null || emails.isEmpty()) { | ||
| logger.warn("No emails returned from GitHub API"); | ||
| return null; | ||
| } | ||
|
|
||
| // Find primary email, or first verified email, or first email | ||
| return emails.stream() | ||
| .filter(e -> Boolean.TRUE.equals(e.get("primary"))) | ||
| .map(e -> (String) e.get("email")) | ||
| .findFirst() | ||
| .orElseGet(() -> emails.stream() | ||
| .filter(e -> Boolean.TRUE.equals(e.get("verified"))) | ||
| .map(e -> (String) e.get("email")) | ||
| .findFirst() | ||
| .orElseGet(() -> (String) emails.get(0).get("email"))); | ||
|
||
|
|
||
| } catch (Exception e) { | ||
| logger.error("Failed to fetch GitHub emails: {}", e.getMessage()); | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,19 +31,22 @@ public class SecurityConfig { | |||||||
| private final CorsProperties corsProperties; | ||||||||
| private final OAuth2AuthenticationSuccessHandler oAuth2SuccessHandler; | ||||||||
| private final OAuth2AuthenticationFailureHandler oAuth2FailureHandler; | ||||||||
| private final CustomOAuth2UserService customOAuth2UserService; | ||||||||
|
|
||||||||
| @Autowired | ||||||||
| public SecurityConfig( | ||||||||
| JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, | ||||||||
| JwtAuthenticationFilter jwtAuthenticationFilter, | ||||||||
| CorsProperties corsProperties, | ||||||||
| @Autowired(required = false) OAuth2AuthenticationSuccessHandler oAuth2SuccessHandler, | ||||||||
| @Autowired(required = false) OAuth2AuthenticationFailureHandler oAuth2FailureHandler) { | ||||||||
| OAuth2AuthenticationSuccessHandler oAuth2SuccessHandler, | ||||||||
| OAuth2AuthenticationFailureHandler oAuth2FailureHandler, | ||||||||
| CustomOAuth2UserService customOAuth2UserService) { | ||||||||
| this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint; | ||||||||
| this.jwtAuthenticationFilter = jwtAuthenticationFilter; | ||||||||
| this.corsProperties = corsProperties; | ||||||||
| this.oAuth2SuccessHandler = oAuth2SuccessHandler; | ||||||||
| this.oAuth2FailureHandler = oAuth2FailureHandler; | ||||||||
| this.customOAuth2UserService = customOAuth2UserService; | ||||||||
| } | ||||||||
|
|
||||||||
| @Bean | ||||||||
|
|
@@ -57,18 +60,36 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | |||||||
| .csrf(AbstractHttpConfigurer::disable) | ||||||||
| .requestCache(RequestCacheConfigurer::disable) | ||||||||
| .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||||||||
| .headers(headers -> headers.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'")) | ||||||||
| .headers(headers -> headers.contentSecurityPolicy( | ||||||||
| csp -> csp.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'" | ||||||||
| + " https://cdn.tailwindcss.com; style-src 'self' 'unsafe-inline'")) | ||||||||
|
Comment on lines
+64
to
+65
|
||||||||
| csp -> csp.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'" | |
| + " https://cdn.tailwindcss.com; style-src 'self' 'unsafe-inline'")) | |
| csp -> csp.policyDirectives("default-src 'self'; script-src 'self' https://cdn.tailwindcss.com; style-src 'self'")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eclipse project files (.project, .settings/) are typically IDE-specific and should not be committed to version control as they can cause conflicts between team members using different IDEs or IDE configurations. Consider adding these files to .gitignore and documenting any required IDE setup in a separate CONTRIBUTING.md or IDE_SETUP.md file instead.