Skip to content
This repository was archived by the owner on Jul 23, 2025. It is now read-only.

Commit 7634a94

Browse files
authored
Merge pull request #323 from Sanapol/add-yandex-authorisation-319
[#319] Add yandex authorisation
2 parents 6a69851 + 8ff04ed commit 7634a94

27 files changed

+588
-9
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
GITHUB_CLIENT_ID=
2+
GITHUB_CLIENT_SECRET=
3+
YANDEX_CLIENT_ID=
4+
YANDEX_CLIENT_SECRET=

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,31 @@ Before you can build this project, you must install and configure the following
1717
1. Java 19
1818
2. Docker, Docker Compose
1919

20+
### Yandex authorization
21+
22+
To enable Yandex authorization, you need to register on [Yandex ID OAuth](https://oauth.yandex.ru/) and create your web application,
23+
add `ClientID` and `Client secret` in your secret
24+
25+
```bash
26+
YANDEX_CLIENT_ID=your_yadex_client_id_values
27+
YANDEX_CLIENT_SECRET=your_yandex_client_secret_values
28+
```
29+
### Registration/Authorization with GitHub
30+
31+
For registration or/and authorization account with GitHub:
32+
- Create OAuth app https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app.
33+
- Get values the `Client ID` and `Client Secret` and add to environment variables in any known way.
34+
35+
For example, you can create an `.env` file in the root of the project, where you can enter the names of variables
36+
and their values as shown below:
37+
```bash
38+
GITHUB_CLIENT_ID=your_github_client_id_values
39+
GITHUB_CLIENT_SECRET=your_github_client_secret_values
40+
```
41+
A `.env.example` file has been created in the root of the project, which specifies the variables as they should be specified. For these variables, you need to specify the values you received.
42+
You can copy this file and rename to `.env`, change and use.
43+
```
44+
2045
### Packaging as uber-jar
2146
2247
To build the final jar:

build.gradle.kts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ dependencies {
3232
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
3333
implementation("org.springframework.boot:spring-boot-starter-actuator")
3434
implementation("org.springframework.boot:spring-boot-starter-validation")
35+
implementation ("org.springframework.boot:spring-boot-starter-oauth2-client")
36+
implementation("org.springframework.boot:spring-boot-starter-webflux")
37+
implementation("org.springframework.session:spring-session-core")
3538
runtimeOnly("org.springframework.boot:spring-boot-devtools")
3639
// Thymeleaf
3740
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE")
@@ -40,13 +43,15 @@ dependencies {
4043
implementation("org.webjars:bootstrap:5.2.3")
4144
implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0")
4245
// Database
43-
runtimeOnly("org.postgresql:postgresql:42.5.4")
46+
runtimeOnly("org.postgresql:postgresql:42.5.5")
4447
implementation("io.hypersistence:hypersistence-utils-hibernate-60:3.2.0")
4548
implementation("org.liquibase:liquibase-core:4.26.0")
4649
// Utils
4750
compileOnly("org.projectlombok:lombok-mapstruct-binding:0.2.0")
4851
implementation("org.ocpsoft.prettytime:prettytime:5.0.6.Final")
4952
implementation("org.mapstruct:mapstruct:1.5.3.Final")
53+
implementation("io.github.cdimascio:dotenv-java:3.2.0")
54+
implementation("org.antlr:antlr4-runtime:4.10.1")
5055
// Annotation processors
5156
annotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final")
5257
// Testing
@@ -55,6 +60,7 @@ dependencies {
5560
testImplementation("org.testcontainers:junit-jupiter")
5661
testImplementation("org.testcontainers:postgresql")
5762
testImplementation("com.github.database-rider:rider-spring:1.36.0")
63+
testImplementation("org.springframework.cloud:spring-cloud-contract-wiremock:4.0.2")
5864
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
5965
testAnnotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final")
6066
}

src/main/java/io/hexlet/typoreporter/HexletTypoReporter.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package io.hexlet.typoreporter;
22

3+
import io.github.cdimascio.dotenv.Dotenv;
4+
import io.github.cdimascio.dotenv.DotenvEntry;
35
import lombok.extern.slf4j.Slf4j;
46
import org.springframework.boot.SpringApplication;
57
import org.springframework.boot.autoconfigure.SpringBootApplication;
68
import org.springframework.core.env.Environment;
79

810
import java.net.InetAddress;
911
import java.net.UnknownHostException;
12+
import java.util.Set;
1013

1114
import static java.util.Optional.ofNullable;
1215

@@ -15,6 +18,15 @@
1518
public class HexletTypoReporter {
1619

1720
public static void main(String[] args) {
21+
Dotenv dotenv = Dotenv.configure()
22+
.ignoreIfMalformed()
23+
.ignoreIfMissing()
24+
.load();
25+
26+
Set<DotenvEntry> dotenvInFile = dotenv.entries(Dotenv.Filter.DECLARED_IN_ENV_FILE);
27+
dotenvInFile.forEach(entry ->
28+
System.setProperty(entry.getKey(), entry.getValue()));
29+
1830
final var env = SpringApplication.run(HexletTypoReporter.class, args).getEnvironment();
1931
logApplicationStartup(env);
2032
}

src/main/java/io/hexlet/typoreporter/config/SecurityConfig.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.hexlet.typoreporter.handler.exception.WorkspaceNotFoundException;
55
import io.hexlet.typoreporter.security.service.AccountDetailService;
66
import io.hexlet.typoreporter.security.service.SecuredWorkspaceService;
7+
import io.hexlet.typoreporter.service.oauth2.OAuth2Service;
78
import jakarta.servlet.FilterChain;
89
import jakarta.servlet.ServletException;
910
import jakarta.servlet.http.HttpServletRequest;
@@ -78,15 +79,16 @@ public SecurityContextRepository securityContextRepository() {
7879
@Bean
7980
public SecurityFilterChain filterChain(HttpSecurity http,
8081
SecurityContextRepository securityContextRepository,
81-
DynamicCorsConfigurationSource dynamicCorsConfigurationSource) throws Exception {
82+
DynamicCorsConfigurationSource dynamicCorsConfigurationSource,
83+
OAuth2Service oAuth2Service) throws Exception {
8284
http.httpBasic();
8385
http.cors();
8486
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
8587

8688
http.authorizeHttpRequests(authz -> authz
8789
.requestMatchers(GET, "/webjars/**", "/widget/**", "/fragments/**", "/img/**",
8890
"/favicon.ico").permitAll()
89-
.requestMatchers("/", "/login", "/signup", "/error", "/about").permitAll()
91+
.requestMatchers("/", "/login", "/signup", "/error", "/about", "/oauth2/**").permitAll()
9092
.anyRequest().authenticated()
9193
)
9294
.formLogin(login -> login
@@ -101,6 +103,11 @@ public SecurityFilterChain filterChain(HttpSecurity http,
101103
new AntPathRequestMatcher("/typo/form/*", POST.name())
102104
)
103105
)
106+
.oauth2Login(oauth2 -> oauth2
107+
.loginPage("/login")
108+
.userInfoEndpoint(userInfo -> userInfo.userService(oAuth2Service))
109+
.defaultSuccessUrl("/workspaces", true)
110+
)
104111
.addFilterBefore(corsFilter(dynamicCorsConfigurationSource), CorsFilter.class);
105112

106113
http.securityContext().securityContextRepository(securityContextRepository);

src/main/java/io/hexlet/typoreporter/domain/account/AuthProvider.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
public enum AuthProvider {
44

5-
EMAIL, GITHUB, GOOGLE
5+
EMAIL("EMAIL"), GITHUB("GITHUB"), GOOGLE("GOOGLE"), YANDEX("YANDEX");
66

7+
private String name;
8+
9+
AuthProvider(String name) {
10+
this.name = name;
11+
}
12+
13+
public String getName() {
14+
return name;
15+
}
16+
17+
public static AuthProvider fromName(String name) {
18+
for (AuthProvider provider : values()) {
19+
if (provider.name.equals(name)) {
20+
return provider;
21+
}
22+
}
23+
throw new IllegalArgumentException("Provider not found: " + name);
24+
}
725
}

src/main/java/io/hexlet/typoreporter/service/AccountService.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.hexlet.typoreporter.service.account.UsernameAlreadyExistException;
99
import io.hexlet.typoreporter.service.account.signup.SignupAccount;
1010
import io.hexlet.typoreporter.service.account.signup.SignupAccountUseCase;
11+
import io.hexlet.typoreporter.service.dto.account.CustomUserDetails;
1112
import io.hexlet.typoreporter.service.dto.account.InfoAccount;
1213
import io.hexlet.typoreporter.service.dto.account.UpdatePassword;
1314
import io.hexlet.typoreporter.service.dto.account.UpdateProfile;
@@ -20,6 +21,9 @@
2021
import io.hexlet.typoreporter.handler.exception.NewPasswordTheSameException;
2122
import io.hexlet.typoreporter.handler.exception.OldPasswordWrongException;
2223
import lombok.RequiredArgsConstructor;
24+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
25+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
26+
import org.springframework.security.core.context.SecurityContextHolder;
2327
import org.springframework.security.crypto.password.PasswordEncoder;
2428
import org.springframework.stereotype.Service;
2529
import org.springframework.transaction.annotation.Transactional;
@@ -66,7 +70,22 @@ public InfoAccount signup(SignupAccount signupAccount) throws UsernameAlreadyExi
6670
accToSave.setEmail(normalizedEmail);
6771
accToSave.setUsername(normalizedUsername);
6872
accToSave.setPassword(passwordEncoder.encode(signupAccount.password()));
69-
accToSave.setAuthProvider(AuthProvider.EMAIL);
73+
74+
if (accToSave.getAuthProvider() == null) {
75+
accToSave.setAuthProvider(AuthProvider.EMAIL);
76+
}
77+
78+
CustomUserDetails accountDetail = new CustomUserDetails(
79+
normalizedEmail,
80+
accToSave.getPassword(),
81+
normalizedUsername,
82+
List.of(new SimpleGrantedAuthority("ROLE_USER"))
83+
);
84+
85+
var tempAuth = new UsernamePasswordAuthenticationToken(
86+
accountDetail, null, accountDetail.getAuthorities());
87+
SecurityContextHolder.getContext().setAuthentication(tempAuth);
88+
7089
accountRepository.save(accToSave);
7190
return accountMapper.toInfoAccount(accToSave);
7291
}

src/main/java/io/hexlet/typoreporter/service/account/signup/SignupAccount.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ public record SignupAccount(
55
String email,
66
String password,
77
String firstName,
8-
String lastName
8+
String lastName,
9+
String authProvider
910
) {
1011

1112
@Override
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.hexlet.typoreporter.service.oauth2;
2+
3+
import lombok.Getter;
4+
import org.springframework.security.core.GrantedAuthority;
5+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
6+
7+
import java.util.Collection;
8+
import java.util.Map;
9+
10+
@Getter
11+
public class CustomOAuth2User extends DefaultOAuth2User {
12+
private final String nickname;
13+
14+
public CustomOAuth2User(Collection<? extends GrantedAuthority> authorities,
15+
Map<String, Object> attributes,
16+
String keyAttribute,
17+
String nickname) {
18+
super(authorities, attributes, keyAttribute);
19+
this.nickname = nickname;
20+
}
21+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.hexlet.typoreporter.service.oauth2;
2+
3+
import org.springframework.core.ParameterizedTypeReference;
4+
import org.springframework.http.HttpHeaders;
5+
import org.springframework.web.reactive.function.client.WebClient;
6+
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
public class GithubOAuth2UserInfo implements OAuth2UserInfo {
11+
12+
private final String accessToken;
13+
private final Map<String, Object> attributes;
14+
15+
public GithubOAuth2UserInfo(String accessToken, Map<String, Object> attributes) {
16+
this.accessToken = accessToken;
17+
this.attributes = attributes;
18+
}
19+
20+
@Override
21+
public String getEmail() {
22+
var email = attributes.get("email");
23+
if (email == null) {
24+
WebClient webClient = WebClient.builder()
25+
.baseUrl("https://api.github.com")
26+
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
27+
.build();
28+
29+
List<Map<String, Object>> emails = webClient.get()
30+
.uri("/user/emails")
31+
.retrieve()
32+
.bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() { })
33+
.block();
34+
35+
email = emails.stream()
36+
.filter(e -> Boolean.TRUE.equals(e.get("primary")))
37+
.map(e -> (String) e.get("email"))
38+
.findFirst()
39+
.orElse(null);
40+
}
41+
return (String) email;
42+
}
43+
44+
@Override
45+
public String getUsername() {
46+
return attributes.get("login").toString();
47+
}
48+
49+
@Override
50+
public String getFirstName() {
51+
String[] names = attributes.get("name").toString().split(" ");
52+
return names.length > 0 ? names[0] : "";
53+
}
54+
55+
@Override
56+
public String getLastName() {
57+
String[] names = attributes.get("name").toString().split(" ");
58+
return names.length > 1 ? names[1] : "";
59+
}
60+
61+
@Override
62+
public Map<String, Object> getAttributes() {
63+
return attributes;
64+
}
65+
}

0 commit comments

Comments
 (0)