Skip to content

Commit c2bf154

Browse files
authored
Merge pull request #20 from nkcoder/feature/ui_page
[Feature] Add a simple frontend (bundled)
2 parents d74bd51 + 2fe80cc commit c2bf154

30 files changed

+2118
-43
lines changed

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
# General
12
.gradle/
23
.idea/
34
build/
45
target/
56
.DS_Store
67
logs/
8+
.vscode/
79

810
# Environment files (contain secrets)
911
.env
1012
.env.local
11-
.env.*.local
13+
.env.*.local
14+
15+
# Eclipse
16+
bin/
17+
*.class

.project

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,30 @@
55
<projects>
66
</projects>
77
<buildSpec>
8+
<buildCommand>
9+
<name>org.eclipse.jdt.core.javabuilder</name>
10+
<arguments>
11+
</arguments>
12+
</buildCommand>
813
<buildCommand>
914
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
1015
<arguments>
1116
</arguments>
1217
</buildCommand>
1318
</buildSpec>
1419
<natures>
20+
<nature>org.eclipse.jdt.core.javanature</nature>
1521
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
1622
</natures>
23+
<filteredResources>
24+
<filter>
25+
<id>1766918873060</id>
26+
<name></name>
27+
<type>30</type>
28+
<matcher>
29+
<id>org.eclipse.core.resources.regexFilterMatcher</id>
30+
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
31+
</matcher>
32+
</filter>
33+
</filteredResources>
1734
</projectDescription>

.settings/org.eclipse.buildship.core.prefs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
arguments=--init-script /var/folders/24/bhk_9mjj6ksffpdxxlhxjg700000gp/T/db3b08fc4a9ef609cb16b96b200fa13e563f396e9bb1ed0905fdab7bc3bc513b.gradle
1+
arguments=--init-script /var/folders/24/bhk_9mjj6ksffpdxxlhxjg700000gp/T/db3b08fc4a9ef609cb16b96b200fa13e563f396e9bb1ed0905fdab7bc3bc513b.gradle --init-script /var/folders/24/bhk_9mjj6ksffpdxxlhxjg700000gp/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle
22
auto.sync=false
33
build.scans.enabled=false
44
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
55
connection.project.dir=
66
eclipse.preferences.version=1
77
gradle.user.home=
8-
java.home=/Library/Java/JavaVirtualMachines/jdk-21.jdk/Contents/Home
8+
java.home=/Users/ling/Library/Java/JavaVirtualMachines/graalvm-jdk-25/Contents/Home
99
jvm.arguments=
1010
offline.mode=false
1111
override.workspace.settings=true
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
eclipse.preferences.version=1
2+
org.eclipse.jdt.apt.aptEnabled=true
3+
org.eclipse.jdt.apt.genSrcDir=bin/generated-sources/annotations
4+
org.eclipse.jdt.apt.genTestSrcDir=bin/generated-test-sources/annotations
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
eclipse.preferences.version=1
2+
org.eclipse.jdt.core.classpath.outputOverlappingAnotherSource=ignore
3+
org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
4+
org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull
5+
org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault
6+
org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable
7+
org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
8+
org.eclipse.jdt.core.compiler.codegen.targetPlatform=25
9+
org.eclipse.jdt.core.compiler.compliance=25
10+
org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning
11+
org.eclipse.jdt.core.compiler.problem.nullReference=warning
12+
org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning
13+
org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
14+
org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled
15+
org.eclipse.jdt.core.compiler.processAnnotations=enabled
16+
org.eclipse.jdt.core.compiler.source=25

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# User Service - Java/Spring Boot
22

3-
A comprehensive user authentication and management service built with **Java 25** and **Spring Boot 4**, architected as a **modular monolith** using **Spring Modulith**.
3+
A comprehensive user authentication and management service built with **Java 25** and **Spring Boot 4**, architected as
4+
a **modular monolith** using **Spring Modulith**.
45

56
## Features
67

@@ -335,6 +336,14 @@ spring:
335336
password: ${DATABASE_PASSWORD}
336337
```
337338
339+
## Users
340+
341+
- username/password: daniel1@yopmail.com/daniel@Pass01
342+
- OTP: nkcoder.24@yopmail.com
343+
- OAuth2:
344+
- Github: daniel5hbs@gmail.com
345+
- Google: daniel5hbs@gmail.com
346+
338347
## References
339348
340349
- [Spring Modulith Documentation](https://docs.spring.io/spring-modulith/reference/)

auto/run

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
#!/usr/bin/env sh
22

3-
# Need to export the mail credentials for local run
4-
#export MAIL_USERNAME=
5-
#export MAIL_PASSWORD=
6-
./gradlew bootRun --args='--spring.profiles.active=local' --no-daemon
3+
# Load environment variables from .env file if it exists
4+
if [ -f .env ]; then
5+
set -a
6+
. ./.env
7+
set +a
8+
fi
9+
10+
# --no-configuration-cache ensures Gradle reads fresh environment variables
11+
# --rerun-tasks ensures bootRun actually runs
12+
./gradlew bootRun --args='--spring.profiles.active=local' --no-daemon --no-configuration-cache --rerun-tasks

build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ tasks.named<org.springframework.boot.gradle.tasks.run.BootRun>("bootRun") {
126126
jvmArgs = listOf(
127127
"-Xms256m", "-Xmx512m", "-XX:+UseG1GC", "-XX:+UseStringDeduplication"
128128
)
129+
// Pass environment variables at execution time (not configuration time)
130+
// This ensures .env variables sourced by auto/run are available
131+
doFirst {
132+
environment(System.getenv())
133+
}
129134
}
130135

131136

src/main/java/org/nkcoder/user/application/service/OtpApplicationService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ public AuthResult verifyOtp(VerifyOtpCommand command) {
162162

163163
User user = userRepository.findByEmail(email).orElseThrow(() -> new AuthenticationException(USER_NOT_FOUND));
164164

165+
// Mark email as verified since OTP verification proves email ownership
166+
if (!user.isEmailVerified()) {
167+
user.verifyEmail();
168+
userRepository.save(user);
169+
}
170+
165171
userRepository.updateLastLoginAt(user.getId(), LocalDateTime.now());
166172

167173
TokenFamily tokenFamily = TokenFamily.generate();
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package org.nkcoder.user.infrastructure.security;
2+
3+
import java.net.URI;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import org.springframework.core.ParameterizedTypeReference;
10+
import org.springframework.http.HttpHeaders;
11+
import org.springframework.http.HttpMethod;
12+
import org.springframework.http.RequestEntity;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
15+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
16+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
17+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
18+
import org.springframework.security.oauth2.core.user.OAuth2User;
19+
import org.springframework.stereotype.Component;
20+
import org.springframework.web.client.RestTemplate;
21+
22+
/**
23+
* Custom OAuth2UserService that fetches additional user info (like email) from providers that don't include it in the
24+
* standard user info response.
25+
*/
26+
@Component
27+
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
28+
29+
private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class);
30+
private static final String GITHUB_EMAILS_URL = "https://api.github.com/user/emails";
31+
32+
private final RestTemplate restTemplate = new RestTemplate();
33+
34+
@Override
35+
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
36+
OAuth2User oAuth2User = super.loadUser(userRequest);
37+
38+
String registrationId = userRequest.getClientRegistration().getRegistrationId();
39+
40+
if ("github".equalsIgnoreCase(registrationId)) {
41+
return enrichGitHubUser(userRequest, oAuth2User);
42+
}
43+
44+
return oAuth2User;
45+
}
46+
47+
private OAuth2User enrichGitHubUser(OAuth2UserRequest userRequest, OAuth2User oAuth2User) {
48+
String email = oAuth2User.getAttribute("email");
49+
50+
if (email == null || email.isBlank()) {
51+
logger.debug("GitHub user email not in attributes, fetching from /user/emails");
52+
email = fetchGitHubPrimaryEmail(userRequest.getAccessToken().getTokenValue());
53+
}
54+
55+
if (email != null) {
56+
// Create new attributes map with email included
57+
Map<String, Object> attributes = new HashMap<>(oAuth2User.getAttributes());
58+
attributes.put("email", email);
59+
60+
return new DefaultOAuth2User(
61+
oAuth2User.getAuthorities(),
62+
attributes,
63+
userRequest
64+
.getClientRegistration()
65+
.getProviderDetails()
66+
.getUserInfoEndpoint()
67+
.getUserNameAttributeName());
68+
}
69+
70+
return oAuth2User;
71+
}
72+
73+
private String fetchGitHubPrimaryEmail(String accessToken) {
74+
try {
75+
HttpHeaders headers = new HttpHeaders();
76+
headers.setBearerAuth(accessToken);
77+
headers.set("Accept", "application/vnd.github+json");
78+
79+
RequestEntity<Void> request = new RequestEntity<>(headers, HttpMethod.GET, URI.create(GITHUB_EMAILS_URL));
80+
81+
ResponseEntity<List<Map<String, Object>>> response =
82+
restTemplate.exchange(request, new ParameterizedTypeReference<>() {});
83+
84+
List<Map<String, Object>> emails = response.getBody();
85+
if (emails == null || emails.isEmpty()) {
86+
logger.warn("No emails returned from GitHub API");
87+
return null;
88+
}
89+
90+
// Find primary email, or first verified email, or first email
91+
return emails.stream()
92+
.filter(e -> Boolean.TRUE.equals(e.get("primary")))
93+
.map(e -> (String) e.get("email"))
94+
.findFirst()
95+
.orElseGet(() -> emails.stream()
96+
.filter(e -> Boolean.TRUE.equals(e.get("verified")))
97+
.map(e -> (String) e.get("email"))
98+
.findFirst()
99+
.orElseGet(() -> (String) emails.get(0).get("email")));
100+
101+
} catch (Exception e) {
102+
logger.error("Failed to fetch GitHub emails: {}", e.getMessage());
103+
return null;
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)