Skip to content
This repository was archived by the owner on Jul 23, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Instead of "your_github_client_id_value" write the "client_id" value
# that you received when creating your OAuth2 app
GITHUB_CLIENT_ID=your_github_client_id_value

# Instead of "your_github_client_secret_value" write the "client_secret" value
# that you received when creating your OAuth2 app
GITHUB_CLIENT_SECRET=your_github_client_secret_value

# Instead of "your_yandex_client_id_value" write the "client_id" value
# that you received when creating your OAuth2 app
YANDEX_CLIENT_ID=your_yandex_client_id_value

# Instead of "your_yandex_client_secret_value" write the "client_secret" value
# that you received when creating your OAuth2 app
YANDEX_CLIENT_SECRET=your_yandex_client_secret_value
9 changes: 5 additions & 4 deletions .github/workflows/github-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v2
- name: Set up JDK 19
uses: actions/setup-java@v1
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v2
with:
java-version: 19
distribution: zulu
java-version: 21
- name: Run unit tests
run: make test-unit-only
- name: Run integration tests
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ jobs:
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: src/widget
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
uses: actions/deploy-pages@v4
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,6 @@ fabric.properties

# JPA Buddy settings
/.jpb/

# Amplicode
amplicode.xml
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,35 @@ Highlight text and press <kbd>Ctrl+Enter</kbd>

Before you can build this project, you must install and configure the following dependencies on your machine:

1. Java 19
1. Java 21
2. Docker, Docker Compose

### Yandex authorization

To enable Yandex authorization, you need to register on [Yandex ID OAuth](https://oauth.yandex.ru/) and create your web application,
add `ClientID` and `Client secret` in your secret

```bash
YANDEX_CLIENT_ID=your_yadex_client_id_values
YANDEX_CLIENT_SECRET=your_yandex_client_secret_values
```

### Registration/Authorization with GitHub

For registration or/and authorization account with GitHub:
- Create OAuth app https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app.
- Authorization callback URL = {baseUrl}/login/oauth2/code/github, for example: http://localhost:8080/login/oauth2/code/github
- Get values the `Client ID` and `Client Secret` and add to environment variables in any known way.

For example, you can create an `.env` file in the root of the project, where you can enter the names of variables
and their values as shown below:
```bash
GITHUB_CLIENT_ID=your_github_client_id_values
GITHUB_CLIENT_SECRET=your_github_client_secret_values
```
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.
You can copy this file and rename to `.env`, change and use.

### Packaging as uber-jar

To build the final jar:
Expand Down
11 changes: 9 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
group = "io.hexlet"
version = "0.0.1-SNAPSHOT"
description = "Hexlet Typo Reporter"
java.sourceCompatibility = JavaVersion.VERSION_19
java.sourceCompatibility = JavaVersion.VERSION_21

plugins {
id("java")
Expand Down Expand Up @@ -32,6 +32,9 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.session:spring-session-core")
runtimeOnly("org.springframework.boot:spring-boot-devtools")
// Thymeleaf
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE")
Expand All @@ -40,13 +43,16 @@ dependencies {
implementation("org.webjars:bootstrap:5.2.3")
implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0")
// Database
runtimeOnly("org.postgresql:postgresql:42.5.4")
runtimeOnly("org.postgresql:postgresql:42.5.5")
implementation("io.hypersistence:hypersistence-utils-hibernate-60:3.2.0")
implementation("org.liquibase:liquibase-core:4.26.0")
// Utils
compileOnly("org.projectlombok:lombok-mapstruct-binding:0.2.0")
implementation("org.ocpsoft.prettytime:prettytime:5.0.6.Final")
implementation("org.mapstruct:mapstruct:1.5.3.Final")
implementation("io.github.cdimascio:dotenv-java:3.2.0")
implementation("org.antlr:antlr4-runtime:4.10.1")
implementation("org.mockito:mockito-inline:5.2.0")
// Annotation processors
annotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final")
// Testing
Expand All @@ -55,6 +61,7 @@ dependencies {
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
testImplementation("com.github.database-rider:rider-spring:1.36.0")
testImplementation("org.wiremock:wiremock-standalone:3.13.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testAnnotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final")
}
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/io/hexlet/typoreporter/HexletTypoReporter.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package io.hexlet.typoreporter;

import io.github.cdimascio.dotenv.Dotenv;
import io.github.cdimascio.dotenv.DotenvEntry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Set;

import static java.util.Optional.ofNullable;

Expand All @@ -15,6 +18,15 @@
public class HexletTypoReporter {

public static void main(String[] args) {
Dotenv dotenv = Dotenv.configure()
.ignoreIfMalformed()
.ignoreIfMissing()
.load();

Set<DotenvEntry> dotenvInFile = dotenv.entries(Dotenv.Filter.DECLARED_IN_ENV_FILE);
dotenvInFile.forEach(entry ->
System.setProperty(entry.getKey(), entry.getValue()));

final var env = SpringApplication.run(HexletTypoReporter.class, args).getEnvironment();
logApplicationStartup(env);
}
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/io/hexlet/typoreporter/config/OAuth2Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.hexlet.typoreporter.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;

@Configuration
public class OAuth2Config {

@Bean
public DefaultOAuth2UserService defaultOAuth2UserService() {
return new DefaultOAuth2UserService();
}
}
11 changes: 10 additions & 1 deletion src/main/java/io/hexlet/typoreporter/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.hexlet.typoreporter.config;

import io.hexlet.typoreporter.handler.OAuth2AuthenticationFailureHandler;
import io.hexlet.typoreporter.handler.exception.ForbiddenDomainException;
import io.hexlet.typoreporter.handler.exception.WorkspaceNotFoundException;
import io.hexlet.typoreporter.security.service.AccountDetailService;
import io.hexlet.typoreporter.security.service.SecuredWorkspaceService;
import io.hexlet.typoreporter.service.oauth2.SocialOAuth2UserService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -78,7 +80,8 @@ public SecurityContextRepository securityContextRepository() {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
SecurityContextRepository securityContextRepository,
DynamicCorsConfigurationSource dynamicCorsConfigurationSource) throws Exception {
DynamicCorsConfigurationSource dynamicCorsConfigurationSource,
SocialOAuth2UserService socialOAuth2UserService) throws Exception {
http.httpBasic();
http.cors();
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
Expand All @@ -95,6 +98,12 @@ public SecurityFilterChain filterChain(HttpSecurity http,
.defaultSuccessUrl("/workspaces")
.permitAll()
)
.oauth2Login(oauth -> oauth
.loginPage("/login")
.userInfoEndpoint(userInfo ->
userInfo.userService(socialOAuth2UserService))
.defaultSuccessUrl("/workspaces", true)
.failureHandler(new OAuth2AuthenticationFailureHandler()))
.csrf(csrf -> csrf
.ignoringRequestMatchers(
new AntPathRequestMatcher("/api/**", POST.name()),
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/io/hexlet/typoreporter/config/WebClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.hexlet.typoreporter.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

@Bean
public WebClient githubWebClient(WebClient.Builder builder,
@Value("${spring.security.oauth2.client.provider.github.user-info-uri}") String githubUserURL) {

return builder.baseUrl(githubUserURL)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

public enum AuthProvider {

EMAIL, GITHUB, GOOGLE
EMAIL, GITHUB, GOOGLE, YANDEX;

public static AuthProvider fromString(String value) {
for (AuthProvider provider : AuthProvider.values()) {
if (provider.name().equalsIgnoreCase(value)) {
return provider;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.hexlet.typoreporter.handler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import java.io.IOException;

public class OAuth2AuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {

String errorMessage = exception.getMessage();
if (errorMessage == null) {
OAuth2AuthenticationException oAuth2Exception = (OAuth2AuthenticationException) exception;
OAuth2Error error = oAuth2Exception.getError();
errorMessage = error.getErrorCode();
}

request.getSession().setAttribute("errorMessage", errorMessage.trim());
response.sendRedirect("/login?error");
}
}
19 changes: 18 additions & 1 deletion src/main/java/io/hexlet/typoreporter/service/AccountService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.hexlet.typoreporter.service.account.UsernameAlreadyExistException;
import io.hexlet.typoreporter.service.account.signup.SignupAccount;
import io.hexlet.typoreporter.service.account.signup.SignupAccountUseCase;
import io.hexlet.typoreporter.service.dto.account.CustomUserDetails;
import io.hexlet.typoreporter.service.dto.account.InfoAccount;
import io.hexlet.typoreporter.service.dto.account.UpdatePassword;
import io.hexlet.typoreporter.service.dto.account.UpdateProfile;
Expand All @@ -20,6 +21,9 @@
import io.hexlet.typoreporter.handler.exception.NewPasswordTheSameException;
import io.hexlet.typoreporter.handler.exception.OldPasswordWrongException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -66,7 +70,20 @@ public InfoAccount signup(SignupAccount signupAccount) throws UsernameAlreadyExi
accToSave.setEmail(normalizedEmail);
accToSave.setUsername(normalizedUsername);
accToSave.setPassword(passwordEncoder.encode(signupAccount.password()));
accToSave.setAuthProvider(AuthProvider.EMAIL);
if (accToSave.getAuthProvider() == null) {
accToSave.setAuthProvider(AuthProvider.EMAIL);
}

CustomUserDetails accountDetail = new CustomUserDetails(
normalizedEmail,
accToSave.getPassword(),
normalizedUsername,
List.of(new SimpleGrantedAuthority("ROLE_USER"))
);

var tempAuth = new UsernamePasswordAuthenticationToken(accountDetail, null, accountDetail.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(tempAuth);

accountRepository.save(accToSave);
return accountMapper.toInfoAccount(accToSave);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ public record SignupAccount(
String email,
String password,
String firstName,
String lastName
String lastName,
String authProvider
) {

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package io.hexlet.typoreporter.service.dto.account;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;

@AllArgsConstructor
public class CustomUserDetails implements UserDetails {

private String email;
private String password;
@Getter
private final String nickname;
private String username;
private Collection<? extends GrantedAuthority> authorities;

@Override
Expand Down Expand Up @@ -48,5 +47,9 @@ public boolean isCredentialsNonExpired() {
public boolean isEnabled() {
return true;
}

public String getNickname() {
return username;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.hexlet.typoreporter.service.dto.oauth2;

import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;

import java.util.Collection;
import java.util.Map;

@Getter
public class CustomOAuth2User extends DefaultOAuth2User {

private final String nickname;

public CustomOAuth2User(Collection<? extends GrantedAuthority> authorities,
Map<String, Object> attributes,
String nameAttributeKey,
String nickname) {
super(authorities, attributes, nameAttributeKey);
this.nickname = nickname;
}
}
Loading