added ai-moderation image and text

This commit is contained in:
SlimusMinus
2026-07-29 00:56:32 +03:00
parent 7899ebcfb0
commit a6fb187212
34 changed files with 708 additions and 8 deletions

View File

@@ -36,6 +36,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.liquibase:liquibase-core' implementation 'org.liquibase:liquibase-core'
implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.mapstruct:mapstruct:1.5.5.Final' implementation 'org.mapstruct:mapstruct:1.5.5.Final'
@@ -49,6 +50,12 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
//WebClient
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.retry:spring-retry'
implementation 'org.springframework:spring-aspects'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'

View File

@@ -1,18 +1,21 @@
package com.krylov.refound; package com.krylov.refound;
import com.krylov.refound.ai.config.AiProperties;
import com.krylov.refound.ai.config.AiRetryProperties;
import com.krylov.refound.config.MinioProperties; import com.krylov.refound.config.MinioProperties;
import io.awspring.cloud.autoconfigure.s3.S3AutoConfiguration; import io.awspring.cloud.autoconfigure.s3.S3AutoConfiguration;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication( @SpringBootApplication(
scanBasePackages = "com.krylov.refound", scanBasePackages = "com.krylov.refound",
exclude = { S3AutoConfiguration.class } exclude = { S3AutoConfiguration.class }
)@EnableScheduling )@EnableScheduling
@EnableConfigurationProperties(MinioProperties.class) @EnableConfigurationProperties({MinioProperties.class, AiProperties.class, AiRetryProperties.class})
@EnableRetry
public class ReFoundApplication { public class ReFoundApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -0,0 +1,52 @@
package com.krylov.refound.ai.client;
import com.krylov.refound.ai.exception.AiBadRequestException;
import com.krylov.refound.ai.exception.AiServerException;
import com.krylov.refound.ai.exception.AiUnavailableException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
@Slf4j
@Component
@RequiredArgsConstructor
public class AiHttpClient {
private final WebClient aiWebClient;
public <T, R> R post(String uri, T request, Class<R> responseType) {
log.info("AI POST {}", uri);
try {
R response = aiWebClient.post()
.uri(uri)
.bodyValue(request)
.retrieve()
.onStatus(
HttpStatusCode::is4xxClientError,
resp -> resp.bodyToMono(String.class)
.map(AiBadRequestException::new)
)
.onStatus(
HttpStatusCode::is5xxServerError,
resp -> resp.bodyToMono(String.class)
.map(AiServerException::new)
)
.bodyToMono(responseType)
.block();
log.info("AI response received");
return response;
} catch (WebClientRequestException ex) {
log.error("AI connection failed", ex);
throw new AiUnavailableException(ex);
} catch (Exception ex) {
log.error("AI request failed", ex);
throw new AiUnavailableException(ex);
}
}
}

View File

@@ -0,0 +1,64 @@
package com.krylov.refound.ai.client;
import com.krylov.refound.ai.exception.AiBadRequestException;
import com.krylov.refound.ai.exception.AiServerException;
import com.krylov.refound.ai.exception.AiUnavailableException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
@Slf4j
@Component
@RequiredArgsConstructor
public class AiMultipartHttpClient {
private final WebClient aiWebClient;
public <R> R post(String uri, MultipartFile file, Class<R> responseType) {
log.info("AI POST {}", uri);
try {
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part(
"file",
new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return file.getOriginalFilename();
}
}
);
R response = aiWebClient.post()
.uri(uri)
.contentType(MediaType.MULTIPART_FORM_DATA)
.bodyValue(builder.build())
.retrieve()
.onStatus(
HttpStatusCode::is4xxClientError,
resp -> resp.bodyToMono(String.class)
.map(AiBadRequestException::new)
)
.onStatus(
HttpStatusCode::is5xxServerError,
resp -> resp.bodyToMono(String.class)
.map(AiServerException::new)
)
.bodyToMono(responseType)
.block();
log.info("AI image response received");
return response;
} catch (WebClientRequestException ex) {
throw new AiUnavailableException(ex);
} catch (Exception ex) {
throw new RuntimeException("Cannot send image to AI", ex);
}
}
}

View File

@@ -0,0 +1,40 @@
package com.krylov.refound.ai.client;
import com.krylov.refound.ai.dto.ImageModerationResponse;
import com.krylov.refound.ai.exception.AiServerException;
import com.krylov.refound.ai.exception.AiUnavailableException;
import lombok.RequiredArgsConstructor;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Service
@RequiredArgsConstructor
public class ImageModerationClient {
private final AiMultipartHttpClient client;
@Retryable(
retryFor = {
AiUnavailableException.class,
AiServerException.class
},
maxAttempts = 3,
backoff = @Backoff(
delay = 500,
multiplier = 2
)
)
public ImageModerationResponse moderate(
MultipartFile file
) {
return client.post(
"/api/v1/moderation/image",
file,
ImageModerationResponse.class
);
}
}

View File

@@ -0,0 +1,51 @@
package com.krylov.refound.ai.client;
import com.krylov.refound.ai.dto.ModerationResponse;
import com.krylov.refound.ai.dto.TextModerationRequest;
import com.krylov.refound.ai.exception.AiServerException;
import com.krylov.refound.ai.exception.AiUnavailableException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class TextModerationClient {
private final AiHttpClient client;
@Retryable(
retryFor = {
AiUnavailableException.class,
AiServerException.class
},
maxAttempts = 3,
backoff = @Backoff(
delay = 500,
multiplier = 2
)
)
public ModerationResponse moderate(String text) {
log.info("Calling AI service");
return client.post(
"/api/v1/moderation/text",
new TextModerationRequest(text),
ModerationResponse.class
);
}
@Recover
public ModerationResponse recover(AiUnavailableException e, String text) {
log.error("AI unavailable after retries", e);
return ModerationResponse.builder()
.approved(false)
.reason("AI_UNAVAILABLE")
.build();
}
}

View File

@@ -0,0 +1,20 @@
package com.krylov.refound.ai.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Getter
@Setter
@ConfigurationProperties(prefix = "ai")
public class AiProperties {
/**
* http://localhost:8000
*/
private String url;
/**
* 5 seconds
*/
private int timeout;
}

View File

@@ -0,0 +1,16 @@
package com.krylov.refound.ai.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Getter
@Setter
@ConfigurationProperties(prefix = "ai.retry")
public class AiRetryProperties {
private int maxAttempts = 3;
private long delay = 500;
private double multiplier = 2;
}

View File

@@ -0,0 +1,34 @@
package com.krylov.refound.ai.config;
import java.time.Duration;
import reactor.netty.http.client.HttpClient;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
@EnableConfigurationProperties(AiProperties.class)
public class WebClientConfig {
@Bean
public WebClient aiWebClient(AiProperties properties) {
HttpClient httpClient =
HttpClient.create()
.responseTimeout(
Duration.ofSeconds(
properties.getTimeout()
)
);
return WebClient.builder()
.baseUrl(properties.getUrl())
.clientConnector(
new ReactorClientHttpConnector(httpClient)
)
.build();
}
}

View File

@@ -0,0 +1,15 @@
package com.krylov.refound.ai.dto;
import java.util.List;
import lombok.Data;
@Data
public class ImageModerationResponse {
private Boolean approved;
private Double score;
private String reason;
private List<String> detectedLabels;
}

View File

@@ -0,0 +1,19 @@
package com.krylov.refound.ai.dto;
import java.util.List;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class ModerationResponse {
private Boolean approved;
private Double score;
private String reason;
private List<String> detectedLabels;
}

View File

@@ -0,0 +1,9 @@
package com.krylov.refound.ai.dto;
import lombok.Data;
@Data
public class TextModerationRequest {
private final String text;
}

View File

@@ -0,0 +1,9 @@
package com.krylov.refound.ai.exception;
public class AiBadRequestException extends AiException {
public AiBadRequestException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,13 @@
package com.krylov.refound.ai.exception;
public abstract class AiException extends RuntimeException {
protected AiException(String message) {
super(message);
}
protected AiException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,9 @@
package com.krylov.refound.ai.exception;
public class AiServerException extends AiException {
public AiServerException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,9 @@
package com.krylov.refound.ai.exception;
public class AiTimeoutException extends AiException {
public AiTimeoutException(Throwable cause) {
super("AI service timeout", cause);
}
}

View File

@@ -0,0 +1,9 @@
package com.krylov.refound.ai.exception;
public class AiUnavailableException extends AiException {
public AiUnavailableException(Throwable cause) {
super("AI service is unavailable", cause);
}
}

View File

@@ -0,0 +1,19 @@
package com.krylov.refound.ai.exception;
import java.util.List;
import lombok.Getter;
@Getter
public class ContentBlockedException extends RuntimeException {
private final String reason;
private final List<String> detectedLabels;
public ContentBlockedException(String reason, List<String> detectedLabels) {
super(reason);
this.reason = reason;
this.detectedLabels = detectedLabels;
}
}

View File

@@ -0,0 +1,30 @@
package com.krylov.refound.ai.facade;
import com.krylov.refound.ai.pipeline.ContentModerationPipeline;
import com.krylov.refound.ai.pipeline.ModerationContext;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Service
@RequiredArgsConstructor
public class ContentModerationFacade {
private final ContentModerationPipeline pipeline;
public void moderate(String title, String description, List<MultipartFile> images) {
log.info("moderate ContentModerationFacade get");
ModerationContext context =
ModerationContext.builder()
.title(title)
.description(description)
.images(images)
.build();
pipeline.execute(context);
}
}

View File

@@ -0,0 +1,19 @@
package com.krylov.refound.ai.pipeline;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ContentModerationPipeline {
private final List<ModerationProcessor> processors;
public void execute(ModerationContext context) {
for (ModerationProcessor processor : processors) {
processor.process(context);
}
}
}

View File

@@ -0,0 +1,51 @@
package com.krylov.refound.ai.pipeline;
import com.krylov.refound.ai.client.ImageModerationClient;
import com.krylov.refound.ai.dto.ImageModerationResponse;
import com.krylov.refound.ai.exception.ContentBlockedException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
@Order(20)
public class ImageModerationProcessor implements ModerationProcessor {
private final ImageModerationClient client;
@Override
public void process(ModerationContext context) {
List<MultipartFile> images = context.getImages();
if (images == null || images.isEmpty()) {
return;
}
for (MultipartFile image : images) {
if (image == null || image.isEmpty()) {
continue;
}
ImageModerationResponse response = client.moderate(image);
log.info("Image moderation result approved={}, score={}, reason={}, labels={}",
response.getApproved(),
response.getScore(),
response.getReason(),
response.getDetectedLabels()
);
if (!response.getApproved()) {
throw new ContentBlockedException(response.getReason(), response.getDetectedLabels());
}
}
}
}

View File

@@ -0,0 +1,17 @@
package com.krylov.refound.ai.pipeline;
import java.util.List;
import lombok.Builder;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
@Builder
public class ModerationContext {
private String title;
private String description;
private List<MultipartFile> images;
}

View File

@@ -0,0 +1,7 @@
package com.krylov.refound.ai.pipeline;
public interface ModerationProcessor {
void process(ModerationContext context);
}

View File

@@ -0,0 +1,61 @@
package com.krylov.refound.ai.pipeline;
import com.krylov.refound.ai.client.TextModerationClient;
import com.krylov.refound.ai.dto.ModerationResponse;
import com.krylov.refound.ai.exception.ContentBlockedException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
@Order(10)
public class TextModerationProcessor implements ModerationProcessor {
private final TextModerationClient client;
@Override
public void process(ModerationContext context) {
String text = buildText(
context.getTitle(),
context.getDescription()
);
ModerationResponse response = client.moderate(text);
log.info(
"AI moderation result approved={}, score={}, reason={}",
response.getApproved(),
response.getScore(),
response.getReason()
);
if (!response.getApproved()) {
throw new ContentBlockedException(
response.getReason(),
response.getDetectedLabels()
);
}
}
private String buildText(String title, String description) {
StringBuilder builder = new StringBuilder();
if (title != null) {
builder.append(title);
}
if (description != null) {
builder.append("\n")
.append(description);
}
return builder.toString();
}
}

View File

@@ -0,0 +1,10 @@
package com.krylov.refound.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
@Configuration
@EnableRetry
public class RetryConfig {
}

View File

@@ -41,8 +41,7 @@ public class PostController {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public PostRequest create(@ModelAttribute PostRequest request, public PostRequest create(@ModelAttribute PostRequest request, @RequestParam(value = "images", required = false) List<MultipartFile> images) {
@RequestParam(value = "images", required = false) List<MultipartFile> images) {
log.info(request.toString()); log.info(request.toString());
return service.create(request, images); return service.create(request, images);
} }

View File

@@ -0,0 +1,18 @@
package com.krylov.refound.dto;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class ModerationErrorDetails {
private List<String> labels;
private List<String> blockedWords;
private Double score;
}

View File

@@ -6,5 +6,6 @@ public enum ErrorCode {
INTERNAL_ERROR, INTERNAL_ERROR,
BAD_REQUEST, BAD_REQUEST,
UNAUTHORIZED, UNAUTHORIZED,
FORBIDDEN FORBIDDEN,
CONTENT_BLOCKED
} }

View File

@@ -1,6 +1,7 @@
package com.krylov.refound.exception; package com.krylov.refound.exception;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
@@ -12,4 +13,5 @@ public class ErrorResponse {
private String error; private String error;
private String message; private String message;
private String path; private String path;
private Object details;
} }

View File

@@ -1,8 +1,11 @@
package com.krylov.refound.exception; package com.krylov.refound.exception;
import com.krylov.refound.ai.exception.ContentBlockedException;
import com.krylov.refound.dto.ModerationErrorDetails;
import com.krylov.refound.enums.ErrorCode; import com.krylov.refound.enums.ErrorCode;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -53,4 +56,44 @@ public class GlobalExceptionHandler {
return new ResponseEntity<>(response, status); return new ResponseEntity<>(response, status);
} }
private ResponseEntity<ErrorResponse> buildResponse(
HttpStatus status,
ErrorCode code,
String message,
HttpServletRequest request,
Object details
) {
ErrorResponse response = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(status.value())
.error(code.name())
.message(message)
.path(request.getRequestURI())
.details(details)
.build();
return ResponseEntity
.status(status)
.body(response);
}
@ExceptionHandler(ContentBlockedException.class)
public ResponseEntity<ErrorResponse> handleBlocked(
ContentBlockedException ex,
HttpServletRequest request
) {
return buildResponse(
HttpStatus.BAD_REQUEST,
ErrorCode.CONTENT_BLOCKED,
ex.getReason(),
request,
ModerationErrorDetails.builder()
.labels(ex.getDetectedLabels())
.build()
);
}
} }

View File

@@ -1,5 +1,6 @@
package com.krylov.refound.security; package com.krylov.refound.security;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -12,6 +13,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -23,20 +27,19 @@ public class SecurityConfig {
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/api/v1/auth/**").permitAll() .requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/test/**").permitAll() .requestMatchers("/api/v1/test/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/files/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/files/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/files/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/v1/files/**").permitAll()
.requestMatchers(HttpMethod.DELETE, "/api/v1/files/**").permitAll() .requestMatchers(HttpMethod.DELETE, "/api/v1/files/**").permitAll()
.requestMatchers(org.springframework.http.HttpMethod.GET, "/api/v1/posts/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/posts/**").permitAll()
.requestMatchers("/api/v1/reviews/**").permitAll() .requestMatchers("/api/v1/reviews/**").permitAll()
.requestMatchers("/files/**", "/uploads/**").permitAll() .requestMatchers("/files/**", "/uploads/**").permitAll()
.requestMatchers("/ws/**").permitAll() .requestMatchers("/ws/**").permitAll()
// All other endpoints require authentication
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@@ -44,6 +47,24 @@ public class SecurityConfig {
return http.build(); return http.build();
} }
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(List.of(
"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"
));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();

View File

@@ -0,0 +1,8 @@
package com.krylov.refound.service;
import org.springframework.stereotype.Service;
@Service
public class ModerationService {
}

View File

@@ -1,5 +1,6 @@
package com.krylov.refound.service; package com.krylov.refound.service;
import com.krylov.refound.ai.facade.ContentModerationFacade;
import com.krylov.refound.dto.PostRequest; import com.krylov.refound.dto.PostRequest;
import com.krylov.refound.dto.PostResponse; import com.krylov.refound.dto.PostResponse;
import com.krylov.refound.entity.Image; import com.krylov.refound.entity.Image;
@@ -48,9 +49,12 @@ public class PostService {
private final UserService userService; private final UserService userService;
private final FavoriteRepository favoriteRepository; private final FavoriteRepository favoriteRepository;
private final FileStorageService fileStorageService; private final FileStorageService fileStorageService;
private final ContentModerationFacade contentModerationFacade;
public PostRequest create(PostRequest request, List<MultipartFile> images) { public PostRequest create(PostRequest request, List<MultipartFile> images) {
contentModerationFacade.moderate(request.getTitle(), request.getDescription(), images);
Post post = mapper.toEntity(request); Post post = mapper.toEntity(request);
post.setCreatedAt(LocalDateTime.now()); post.setCreatedAt(LocalDateTime.now());
post.setStatus(PostStatus.ACTIVE); post.setStatus(PostStatus.ACTIVE);
@@ -154,6 +158,8 @@ public class PostService {
HttpStatus.NOT_FOUND HttpStatus.NOT_FOUND
)); ));
contentModerationFacade.moderate(request.getTitle(), request.getDescription(), images);
post.setTitle(request.getTitle()); post.setTitle(request.getTitle());
post.setDescription(request.getDescription()); post.setDescription(request.getDescription());
if (request.getCategory() != null) { if (request.getCategory() != null) {

View File

@@ -36,6 +36,15 @@ minio:
secret-key: uRIPejW6DwSTwdICwxXRA71VuZAAVE9EmHBq7B7K secret-key: uRIPejW6DwSTwdICwxXRA71VuZAAVE9EmHBq7B7K
region: us-east-1 # MinIO не проверяет регион, но SDK требует значение region: us-east-1 # MinIO не проверяет регион, но SDK требует значение
ai:
url: http://localhost:8000
timeout: 5
retry:
max-attempts: 3
delay: 500
multiplier: 2
file: file:
upload-dir: uploads/ upload-dir: uploads/