Skip to content

Commit d4d862d

Browse files
authored
Merge pull request #67 from TP-RENTPLACE/development
Development
2 parents bb02f99 + 75cb9ec commit d4d862d

97 files changed

Lines changed: 1842 additions & 328 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/backend-ci-cd.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,38 @@ jobs:
9292
runs-on: ubuntu-latest
9393
needs: build-and-push-docker-image
9494
steps:
95+
- name: Set up SSH key
96+
run: |
97+
mkdir -p ~/.ssh
98+
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
99+
chmod 600 ~/.ssh/id_rsa
100+
eval $(ssh-agent -s)
101+
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
102+
- name: List files
103+
run: ls -la
104+
105+
- name: Create app.env file
106+
run: |
107+
echo "DB_NAME=${{ secrets.DB_NAME }}" >> .env
108+
echo "DB_USER=${{ secrets.DB_USER }}" >> .env
109+
echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> .env
110+
echo "JWT_SECRET_ACCESS=${{ secrets.JWT_SECRET_ACCESS }}" >> .env
111+
echo "JWT_SECRET_REFRESH=${{ secrets.JWT_SECRET_REFRESH }}" >> .env
112+
echo "JWT_EXPIRATION_TIME_IN_DAYS_REFRESH=${{ secrets.JWT_EXPIRATION_TIME_IN_DAYS_REFRESH }}" >> .env
113+
echo "JWT_EXPIRATION_TIME_IN_MINUTES_ACCESS=${{ secrets.JWT_EXPIRATION_TIME_IN_MINUTES_ACCESS }}" >> .env
114+
echo "MAIL_USERNAME=${{ secrets.MAIL_USERNAME }}" >> .env
115+
echo "MAIL_PASSWORD =${{ secrets.MAIL_PASSWORD }}" >> .env
116+
echo "COMMISSION_FOR_RENTER_IN_PERCENT=${{ secrets.COMMISSION_FOR_RENTER_IN_PERCENT }}" >> .env
117+
echo "COMMISSION_FOR_OWNER_IN_PERCENT=${{ secrets.COMMISSION_FOR_OWNER_IN_PERCENT }}" >> .env
118+
echo "OPENROUTER_API_URL=${{ secrets.OPENROUTER_API_URL }}" >> .env
119+
echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" >> .env
120+
echo "OPENROUTER_API_MODEL=${{ secrets.OPENROUTER_API_MODEL }}" >> .env
121+
echo "OPENROUTER_API_DEFAULT_SYSTEM_PROMPT=${{ secrets.OPENROUTER_API_DEFAULT_SYSTEM_PROMPT }}" >> .env
122+
123+
- name: Copy files to the server
124+
run: |
125+
eval $(ssh-agent -s)
126+
scp ./.env ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:${{ secrets.APP_PATH }}/
95127
- name: SSH Execute Commands
96128
uses: appleboy/ssh-action@v1
97129
with:

rentplace/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,15 @@ repositories {
2525

2626
dependencies {
2727
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
28+
implementation 'org.springframework.boot:spring-boot-starter-web'
29+
implementation 'org.springframework.boot:spring-boot-starter-webflux'
2830
runtimeOnly 'org.flywaydb:flyway-database-postgresql:11.2.0'
2931
implementation 'org.flywaydb:flyway-core:11.2.0'
3032
implementation 'org.springframework.boot:spring-boot-starter-web'
3133
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
3234
implementation 'org.flywaydb:flyway-core'
35+
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:8.0.1'
36+
implementation("commons-codec:commons-codec:1.17.2")
3337

3438
implementation("org.springframework.boot:spring-boot-starter-mail:3.4.3")
3539

@@ -38,6 +42,7 @@ dependencies {
3842
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
3943
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
4044
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
45+
implementation("com.postmarkapp:postmark:1.11.1")
4146

4247
compileOnly 'org.projectlombok:lombok'
4348
runtimeOnly 'org.postgresql:postgresql'
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package kattsyn.dev.rentplace.aspects;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.aspectj.lang.ProceedingJoinPoint;
5+
import org.aspectj.lang.annotation.Around;
6+
import org.aspectj.lang.annotation.Aspect;
7+
import org.springframework.stereotype.Component;
8+
9+
@Slf4j
10+
@Aspect
11+
@Component
12+
public class LoggingAspect {
13+
14+
@Around("execution(public * kattsyn.dev.rentplace.services..*(..))")
15+
public Object logServiceMethods(ProceedingJoinPoint joinPoint) throws Throwable {
16+
String methodName = joinPoint.getSignature().toShortString();
17+
log.info("Вызов метода: {}", methodName);
18+
long startTime = System.currentTimeMillis();
19+
try {
20+
Object result = joinPoint.proceed();
21+
double elapsedTime = (double) (System.currentTimeMillis() - startTime) / 1000;
22+
log.info("Метод {} выполнен успешно за {} с", methodName, elapsedTime);
23+
return result;
24+
} catch (Throwable ex) {
25+
double elapsedTime = (double) (System.currentTimeMillis() - startTime) / 1000;
26+
log.error("Метод {} завершился с ошибкой за {} с. Ошибка: {}", methodName, elapsedTime, ex.getMessage());
27+
throw ex;
28+
}
29+
}
30+
}

rentplace/src/main/java/kattsyn/dev/rentplace/auth/JwtProvider.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ public class JwtProvider {
3030
public JwtProvider(
3131
@Value("${jwt.secret.access}") String jwtAccessSecret,
3232
@Value("${jwt.secret.refresh}") String jwtRefreshSecret,
33-
@Value("${jwt.expiration_time_in_minutes.access}") int accessTokenExpTimeInMinutes,
34-
@Value("${jwt.expiration_time_in_days.refresh}") int refreshTokenExpTimeInDays
33+
@Value("${jwt.expiration-time-in-minutes.access}") int accessTokenExpTimeInMinutes,
34+
@Value("${jwt.expiration-time-in-days.refresh}") int refreshTokenExpTimeInDays
3535
) {
3636
this.jwtAccessSecret = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtAccessSecret));
3737
this.jwtRefreshSecret = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtRefreshSecret));
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package kattsyn.dev.rentplace.configs;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import org.springframework.boot.context.properties.ConfigurationProperties;
5+
6+
@ConfigurationProperties(prefix = "email.retry")
7+
public record EmailRetryProperties(
8+
@NotBlank Integer maxAttempts,
9+
@NotBlank Integer delayMs
10+
) {
11+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package kattsyn.dev.rentplace.configs;
2+
3+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.http.HttpHeaders;
7+
import org.springframework.web.reactive.function.client.WebClient;
8+
9+
@Configuration
10+
@EnableConfigurationProperties(OpenRouterProperties.class)
11+
public class OpenRouterConfig {
12+
13+
@Bean
14+
public WebClient webClient(OpenRouterProperties props) {
15+
return WebClient.builder()
16+
.baseUrl(props.url())
17+
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + props.key())
18+
.build();
19+
}
20+
21+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package kattsyn.dev.rentplace.configs;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import org.springframework.boot.context.properties.ConfigurationProperties;
5+
6+
@ConfigurationProperties(prefix = "openrouter.api")
7+
public record OpenRouterProperties(
8+
@NotBlank String url,
9+
@NotBlank String key,
10+
@NotBlank String defaultSystemPrompt,
11+
@NotBlank String model) {
12+
}

rentplace/src/main/java/kattsyn/dev/rentplace/configs/SecurityConfig.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import org.springframework.http.HttpMethod;
1010
import org.springframework.http.HttpStatus;
1111
import org.springframework.security.authentication.AuthenticationManager;
12-
import org.springframework.security.config.Customizer;
1312
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
1413
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
1514
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -19,6 +18,11 @@
1918
import org.springframework.security.web.SecurityFilterChain;
2019
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
2120
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
21+
import org.springframework.web.cors.CorsConfiguration;
22+
import org.springframework.web.cors.CorsConfigurationSource;
23+
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
24+
25+
import java.util.List;
2226

2327
@Configuration
2428
@EnableWebSecurity
@@ -63,13 +67,14 @@ public void init() {
6367
@Bean
6468
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
6569
http
66-
.cors(Customizer.withDefaults())
70+
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
6771
.csrf(CsrfConfigurer::disable)
6872
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
6973
.authorizeHttpRequests(
7074
authorize -> authorize
7175
.requestMatchers(PUBLIC_URLS).permitAll()
7276
.requestMatchers(HttpMethod.GET, PUBLIC_URLS_GET).permitAll()
77+
.requestMatchers(HttpMethod.POST, "/api/v1/properties/filtered/").permitAll()
7378
.requestMatchers(HttpMethod.DELETE, ADMIN_URLS).hasAuthority("ROLE_ADMIN")
7479
.requestMatchers(HttpMethod.POST, ADMIN_URLS).hasAuthority("ROLE_ADMIN")
7580
.requestMatchers(HttpMethod.PATCH, ADMIN_URLS).hasAuthority("ROLE_ADMIN")
@@ -82,6 +87,21 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
8287
return http.build();
8388
}
8489

90+
@Bean
91+
public CorsConfigurationSource corsConfigurationSource() {
92+
CorsConfiguration config = new CorsConfiguration();
93+
config.setAllowedOriginPatterns(List.of("*"));
94+
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
95+
config.setAllowedHeaders(List.of("*"));
96+
config.setExposedHeaders(List.of("Authorization", "Cache-Control", "Content-Type", "Set-Cookie", "User-Agent", "X-Forwarded-For"));
97+
config.setAllowCredentials(true);
98+
config.setMaxAge(3600L);
99+
100+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
101+
source.registerCorsConfiguration("/**", config);
102+
return source;
103+
}
104+
85105
@Bean
86106
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
87107
return configuration.getAuthenticationManager();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package kattsyn.dev.rentplace.controllers;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
5+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
6+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
7+
import io.swagger.v3.oas.annotations.tags.Tag;
8+
import jakarta.security.auth.message.AuthException;
9+
import jakarta.validation.Valid;
10+
import kattsyn.dev.rentplace.dtos.ai.GenerateDescriptionRequest;
11+
import kattsyn.dev.rentplace.dtos.ai.GenerateDescriptionResponse;
12+
import kattsyn.dev.rentplace.services.AiService;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.http.MediaType;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.web.bind.annotation.*;
17+
18+
@RestController
19+
@RequestMapping("${api.path}/ai")
20+
@RequiredArgsConstructor
21+
@Tag(name = "AI Controller", description = "Интеграция с OpenRouter для работы с LLM моделями")
22+
public class AiController {
23+
24+
private final AiService aiService;
25+
26+
@Operation(
27+
summary = "Сгенерировать описание проекта",
28+
description = "Принимает системный и пользовательский промпты, отсылает их в Open Router AI и возвращает сгенерированный текст."
29+
)
30+
@ApiResponses({
31+
@ApiResponse(responseCode = "200", description = "Сгенерированное описание получено"),
32+
@ApiResponse(responseCode = "400", description = "Неверные данные запроса"),
33+
@ApiResponse(responseCode = "401", description = "Неавторизованный доступ"),
34+
@ApiResponse(responseCode = "403", description = "Доступ запрещён"),
35+
@ApiResponse(responseCode = "404", description = "Пользователь не найден"),
36+
@ApiResponse(responseCode = "429", description = "Превышен лимит AI-запросов (10 в час)"),
37+
@ApiResponse(responseCode = "500", description = "Внутренняя ошибка сервера")
38+
})
39+
@PostMapping(path = "/description", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
40+
@SecurityRequirement(name = "JWT")
41+
public ResponseEntity<GenerateDescriptionResponse> generateDescription(
42+
@Valid @ModelAttribute GenerateDescriptionRequest request
43+
) throws AuthException {
44+
GenerateDescriptionResponse response = new GenerateDescriptionResponse(aiService.generateDescription(request));
45+
return ResponseEntity.ok(response);
46+
}
47+
48+
}

rentplace/src/main/java/kattsyn/dev/rentplace/controllers/AuthController.java

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
55
import io.swagger.v3.oas.annotations.tags.Tag;
66
import jakarta.security.auth.message.AuthException;
7-
import kattsyn.dev.rentplace.dtos.*;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import kattsyn.dev.rentplace.dtos.requests.CodeRequest;
9+
import kattsyn.dev.rentplace.dtos.requests.JwtRequest;
10+
import kattsyn.dev.rentplace.dtos.requests.RefreshJwtRequest;
11+
import kattsyn.dev.rentplace.dtos.requests.RegisterRequest;
12+
import kattsyn.dev.rentplace.dtos.responses.CodeResponse;
13+
import kattsyn.dev.rentplace.dtos.responses.JwtResponse;
14+
import kattsyn.dev.rentplace.dtos.users.UserDTO;
815
import kattsyn.dev.rentplace.services.AuthService;
9-
import kattsyn.dev.rentplace.services.VerificationCodeService;
1016
import lombok.RequiredArgsConstructor;
1117
import org.springframework.http.ResponseEntity;
1218
import org.springframework.web.bind.annotation.*;
@@ -18,25 +24,26 @@
1824
public class AuthController {
1925

2026
private final AuthService authService;
21-
private final VerificationCodeService verificationCodeService;
2227

2328
@PostMapping("/code-request")
2429
@Operation(
2530
summary = "Запросить код по почте",
2631
description = "Запрос на получение кода авторизации по почте"
2732
)
2833
public ResponseEntity<CodeResponse> requestCode(@RequestBody CodeRequest codeRequest) {
29-
return ResponseEntity.ok(verificationCodeService.generateAndSendCode(codeRequest.getEmail()));
34+
return ResponseEntity.ok(authService.getCodeResponse(codeRequest.getEmail()));
3035
}
3136

3237
@Operation(
3338
summary = "Запрос на авторизацию",
3439
description = "Получает email и код с почты. Возвращает JWT токены"
3540
)
3641
@PostMapping("/login")
37-
public ResponseEntity<JwtResponse> login(@RequestBody JwtRequest authRequest/*,
42+
public ResponseEntity<JwtResponse> login(HttpServletRequest request, @RequestBody JwtRequest authRequest/*,
3843
HttpServletResponse response*/) throws AuthException {
39-
JwtResponse tokens = authService.login(authRequest);
44+
45+
46+
JwtResponse tokens = authService.login(authRequest, request);
4047

4148
/*
4249
ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", tokens.getRefreshToken())
@@ -67,14 +74,26 @@ public ResponseEntity<JwtResponse> login(@RequestBody JwtRequest authRequest/*,
6774
.body(tokens);
6875
}
6976

77+
@Operation(
78+
summary = "Запрос на авторизацию в админ-панель",
79+
description = "Получает email и код с почты. Возвращает JWT токены. Пускает только администраторов."
80+
)
81+
@PostMapping("/admin/login")
82+
public ResponseEntity<JwtResponse> adminLogin(@RequestBody JwtRequest authRequest, HttpServletRequest httpServletRequest/*,
83+
HttpServletResponse response*/) throws AuthException {
84+
JwtResponse tokens = authService.adminLogin(authRequest, httpServletRequest);
85+
return ResponseEntity.ok()
86+
.body(tokens);
87+
}
88+
7089
@Operation(
7190
summary = "Запрос на регистрацию",
7291
description = "Получает email и код с почты, а также имя и фамилию пользователя. Возвращает JWT токены"
7392
)
7493
@PostMapping("/register")
75-
public ResponseEntity<JwtResponse> register(@RequestBody RegisterRequest registerRequest/*,
94+
public ResponseEntity<JwtResponse> register(@RequestBody RegisterRequest registerRequest, HttpServletRequest httpServletRequest/*,
7695
HttpServletResponse response*/) throws AuthException {
77-
JwtResponse tokens = authService.register(registerRequest);
96+
JwtResponse tokens = authService.register(registerRequest, httpServletRequest);
7897

7998
return ResponseEntity.ok()
8099
.body(tokens);
@@ -92,14 +111,13 @@ public ResponseEntity<Void> checkCode(@RequestBody JwtRequest authRequest/*,
92111
}
93112

94113

95-
96114
@Operation(
97115
summary = "Запрос на обновление AccessToken'а",
98116
description = "Получает RefreshToken, возвращает новый AccessToken"
99117
)
100118
@PostMapping("/token")
101-
public ResponseEntity<JwtResponse> getNewAccessToken(@RequestBody RefreshJwtRequest request) {
102-
final JwtResponse token = authService.getAccessToken(request.getRefreshToken());
119+
public ResponseEntity<JwtResponse> getNewAccessToken(@RequestBody RefreshJwtRequest request, HttpServletRequest httpServletRequest) throws AuthException {
120+
final JwtResponse token = authService.getAccessToken(request.getRefreshToken(), httpServletRequest);
103121
return ResponseEntity.ok(token);
104122
}
105123

@@ -108,8 +126,8 @@ public ResponseEntity<JwtResponse> getNewAccessToken(@RequestBody RefreshJwtRequ
108126
description = "Принимает еще не истекший RefreshToken и возвращает новый, продленный."
109127
)
110128
@PostMapping("/refresh")
111-
public ResponseEntity<JwtResponse> refresh(/*@CookieValue(name = "refreshToken") String refreshToken, HttpServletResponse response*/ @RequestBody RefreshJwtRequest request) throws AuthException {
112-
JwtResponse jwtResponse = authService.refresh(request.getRefreshToken());
129+
public ResponseEntity<JwtResponse> refresh(/*@CookieValue(name = "refreshToken") String refreshToken, HttpServletResponse response*/ @RequestBody RefreshJwtRequest refreshJwtRequest, HttpServletRequest request) throws AuthException {
130+
JwtResponse jwtResponse = authService.refresh(refreshJwtRequest.getRefreshToken(), request);
113131

114132
/*
115133
Cookie refreshCookie = new Cookie("refreshToken", jwtResponse.getRefreshToken());

0 commit comments

Comments
 (0)