Add rag-service
This commit is contained in:
74
rag-service/src/main/java/com/balex/rag/RagApplication.java
Normal file
74
rag-service/src/main/java/com/balex/rag/RagApplication.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package com.balex.rag;
|
||||
|
||||
import com.balex.rag.advisors.expansion.ExpansionQueryAdvisor;
|
||||
import com.balex.rag.advisors.rag.RagAdvisor;
|
||||
import com.balex.rag.config.RagDefaultsProperties;
|
||||
import com.balex.rag.config.RagExpansionProperties;
|
||||
import com.balex.rag.repo.ChatRepository;
|
||||
import com.balex.rag.service.PostgresChatMemory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.api.Advisor;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||
import org.springframework.ai.vectorstore.VectorStore;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@SpringBootApplication
|
||||
@RequiredArgsConstructor
|
||||
@EnableConfigurationProperties({RagDefaultsProperties.class, RagExpansionProperties.class})
|
||||
public class RagApplication {
|
||||
|
||||
private final ChatRepository chatRepository;
|
||||
private final VectorStore vectorStore;
|
||||
private final ChatModel chatModel;
|
||||
private final RagExpansionProperties expansionProperties;
|
||||
|
||||
@Bean
|
||||
public ChatClient chatClient(
|
||||
ChatClient.Builder builder,
|
||||
@Value("${rag.rerank-fetch-multiplier}") int rerankFetchMultiplier,
|
||||
RagDefaultsProperties ragDefaults) {
|
||||
return builder
|
||||
.defaultAdvisors(
|
||||
getHistoryAdvisor(0),
|
||||
ExpansionQueryAdvisor.builder(chatModel, expansionProperties).order(1).build(),
|
||||
SimpleLoggerAdvisor.builder().order(2).build(),
|
||||
RagAdvisor.build(vectorStore)
|
||||
.rerankFetchMultiplier(rerankFetchMultiplier)
|
||||
.searchTopK(ragDefaults.searchTopK())
|
||||
.similarityThreshold(ragDefaults.similarityThreshold())
|
||||
.order(3).build(),
|
||||
SimpleLoggerAdvisor.builder().order(4).build()
|
||||
)
|
||||
.defaultOptions(OllamaOptions.builder()
|
||||
.temperature(ragDefaults.temperature())
|
||||
.repeatPenalty(ragDefaults.repeatPenalty())
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Advisor getHistoryAdvisor(int order) {
|
||||
return MessageChatMemoryAdvisor.builder(getChatMemory()).order(order).build();
|
||||
}
|
||||
|
||||
private ChatMemory getChatMemory() {
|
||||
return PostgresChatMemory.builder()
|
||||
.maxMessages(8)
|
||||
.chatMemoryRepository(chatRepository)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(RagApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.balex.rag.advice;
|
||||
|
||||
import com.balex.rag.model.constants.ApiConstants;
|
||||
import com.balex.rag.model.exception.InvalidDataException;
|
||||
import com.balex.rag.model.exception.InvalidPasswordException;
|
||||
import com.balex.rag.model.exception.NotFoundException;
|
||||
import com.balex.rag.service.model.exception.DataExistException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@ControllerAdvice
|
||||
public class CommonControllerAdvice {
|
||||
|
||||
@ExceptionHandler
|
||||
@ResponseBody
|
||||
protected ResponseEntity<String> handleNotFoundException(NotFoundException ex) {
|
||||
logStackTrace(ex);
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.NOT_FOUND)
|
||||
.body(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(DataExistException.class)
|
||||
@ResponseBody
|
||||
protected ResponseEntity<String> handleDataExistException(DataExistException ex) {
|
||||
logStackTrace(ex);
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.CONFLICT)
|
||||
.body(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<Map<String, String>> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
|
||||
logStackTrace(ex);
|
||||
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
for (ObjectError error : ex.getBindingResult().getAllErrors()) {
|
||||
String errorMessage = error.getDefaultMessage();
|
||||
errors.put("error", errorMessage);
|
||||
}
|
||||
|
||||
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
@ResponseBody
|
||||
protected ResponseEntity<String> handleAuthenticationException(AuthenticationException ex) {
|
||||
logStackTrace(ex);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(InvalidDataException.class)
|
||||
@ResponseBody
|
||||
protected ResponseEntity<String> handleInvalidDataException(InvalidDataException ex) {
|
||||
logStackTrace(ex);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(InvalidPasswordException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
@ResponseBody
|
||||
public String handleInvalidPasswordException(InvalidPasswordException ex) {
|
||||
return ex.getMessage();
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseBody
|
||||
protected ResponseEntity<String> handleAccessDeniedException(AccessDeniedException ex) {
|
||||
logStackTrace(ex);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.FORBIDDEN)
|
||||
.body(ex.getMessage());
|
||||
}
|
||||
|
||||
private void logStackTrace(Exception ex) {
|
||||
StringBuilder stackTrace = new StringBuilder();
|
||||
|
||||
stackTrace.append(ApiConstants.ANSI_RED);
|
||||
|
||||
stackTrace.append(ex.getMessage()).append(ApiConstants.BREAK_LINE);
|
||||
|
||||
if (Objects.nonNull(ex.getCause())) {
|
||||
stackTrace.append(ex.getCause().getMessage()).append(ApiConstants.BREAK_LINE);
|
||||
}
|
||||
|
||||
Arrays.stream(ex.getStackTrace())
|
||||
.filter(st -> st.getClassName().startsWith(ApiConstants.TIME_ZONE_PACKAGE_NAME))
|
||||
.forEach(st -> stackTrace
|
||||
.append(st.getClassName())
|
||||
.append(".")
|
||||
.append(st.getMethodName())
|
||||
.append(" (")
|
||||
.append(st.getLineNumber())
|
||||
.append(") ")
|
||||
);
|
||||
|
||||
log.error(stackTrace.append(ApiConstants.ANSI_WHITE).toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.balex.rag.advisors.expansion;
|
||||
|
||||
import com.balex.rag.config.RagExpansionProperties;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.client.ChatClientRequest;
|
||||
import org.springframework.ai.chat.client.ChatClientResponse;
|
||||
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
|
||||
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Builder
|
||||
public class ExpansionQueryAdvisor implements BaseAdvisor {
|
||||
|
||||
|
||||
private static final PromptTemplate template = PromptTemplate.builder()
|
||||
.template("""
|
||||
Expand the search query by adding relevant terms.
|
||||
|
||||
Rules:
|
||||
- Keep all original words
|
||||
- Add up to 5 specific terms
|
||||
- Output ONLY the expanded query, nothing else
|
||||
- No explanations, no formatting, no quotes, no bullet points
|
||||
|
||||
Examples:
|
||||
Question: what is spring
|
||||
Query: what is spring framework Java dependency injection
|
||||
|
||||
Question: how to configure security
|
||||
Query: how to configure security Spring Security authentication authorization filter chain
|
||||
|
||||
Question: {question}
|
||||
Query:
|
||||
""").build();
|
||||
|
||||
|
||||
public static final String ENRICHED_QUESTION = "ENRICHED_QUESTION";
|
||||
public static final String ORIGINAL_QUESTION = "ORIGINAL_QUESTION";
|
||||
public static final String EXPANSION_RATIO = "EXPANSION_RATIO";
|
||||
|
||||
private ChatClient chatClient;
|
||||
|
||||
public static ExpansionQueryAdvisorBuilder builder(ChatModel chatModel, RagExpansionProperties props) {
|
||||
return new ExpansionQueryAdvisorBuilder().chatClient(ChatClient.builder(chatModel)
|
||||
.defaultOptions(OllamaOptions.builder()
|
||||
.temperature(props.temperature())
|
||||
.topK(props.topK())
|
||||
.topP(props.topP())
|
||||
.repeatPenalty(props.repeatPenalty())
|
||||
.build())
|
||||
.build());
|
||||
}
|
||||
|
||||
@Getter
|
||||
private final int order;
|
||||
|
||||
@Override
|
||||
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
|
||||
|
||||
String userQuestion = chatClientRequest.prompt().getUserMessage().getText();
|
||||
String enrichedQuestion = chatClient
|
||||
.prompt()
|
||||
.user(template.render(Map.of("question", userQuestion)))
|
||||
.call()
|
||||
.content();
|
||||
|
||||
double ratio = enrichedQuestion.length() / (double) userQuestion.length();
|
||||
|
||||
return chatClientRequest.mutate()
|
||||
.context(ORIGINAL_QUESTION, userQuestion)
|
||||
.context(ENRICHED_QUESTION, enrichedQuestion)
|
||||
.context(EXPANSION_RATIO, ratio)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
|
||||
|
||||
|
||||
return chatClientResponse;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.balex.rag.advisors.rag;
|
||||
|
||||
import com.balex.rag.model.exception.RerankException;
|
||||
import com.github.pemistahl.lingua.api.Language;
|
||||
import com.github.pemistahl.lingua.api.LanguageDetector;
|
||||
import com.github.pemistahl.lingua.api.LanguageDetectorBuilder;
|
||||
import lombok.Builder;
|
||||
import org.apache.lucene.analysis.Analyzer;
|
||||
import org.apache.lucene.analysis.TokenStream;
|
||||
import org.apache.lucene.analysis.en.EnglishAnalyzer;
|
||||
import org.apache.lucene.analysis.ru.RussianAnalyzer;
|
||||
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
|
||||
import org.springframework.ai.document.Document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.balex.rag.model.constants.ApiErrorMessage.TOKENIZATION_ERROR;
|
||||
|
||||
@Builder
|
||||
public class BM25RerankEngine {
|
||||
@Builder.Default
|
||||
private static final LanguageDetector languageDetector = LanguageDetectorBuilder
|
||||
.fromLanguages(Language.ENGLISH, Language.RUSSIAN)
|
||||
.build();
|
||||
|
||||
// BM25 parameters
|
||||
@Builder.Default
|
||||
private final double K = 1.2;
|
||||
@Builder.Default
|
||||
private final double B = 0.75;
|
||||
|
||||
public List<Document> rerank(List<Document> corpus, String query, int limit) {
|
||||
|
||||
if (corpus == null || corpus.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// Compute corpus statistics
|
||||
CorpusStats stats = computeCorpusStats(corpus);
|
||||
|
||||
// Tokenize query
|
||||
List<String> queryTerms = tokenize(query);
|
||||
|
||||
// Score and sort documents
|
||||
return corpus.stream()
|
||||
.sorted((d1, d2) -> Double.compare(
|
||||
score(queryTerms, d2, stats),
|
||||
score(queryTerms, d1, stats)
|
||||
))
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private CorpusStats computeCorpusStats(List<Document> corpus) {
|
||||
Map<String, Integer> docFreq = new HashMap<>();
|
||||
Map<Document, List<String>> tokenizedDocs = new HashMap<>();
|
||||
int totalLength = 0;
|
||||
int totalDocs = corpus.size();
|
||||
|
||||
// Process each document
|
||||
for (Document doc : corpus) {
|
||||
List<String> tokens = tokenize(doc.getText());
|
||||
tokenizedDocs.put(doc, tokens);
|
||||
totalLength += tokens.size();
|
||||
|
||||
// Update document frequencies
|
||||
Set<String> uniqueTerms = new HashSet<>(tokens);
|
||||
for (String term : uniqueTerms) {
|
||||
docFreq.put(term, docFreq.getOrDefault(term, 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
double avgDocLength = (double) totalLength / totalDocs;
|
||||
|
||||
return new CorpusStats(docFreq, tokenizedDocs, avgDocLength, totalDocs);
|
||||
}
|
||||
|
||||
private double score(List<String> queryTerms, Document doc, CorpusStats stats) {
|
||||
List<String> tokens = stats.tokenizedDocs.get(doc);
|
||||
if (tokens == null) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Calculate term frequencies for this document
|
||||
Map<String, Integer> tfMap = new HashMap<>();
|
||||
for (String token : tokens) {
|
||||
tfMap.put(token, tfMap.getOrDefault(token, 0) + 1);
|
||||
}
|
||||
|
||||
int docLength = tokens.size();
|
||||
double score = 0.0;
|
||||
|
||||
// Calculate BM25 score
|
||||
for (String term : queryTerms) {
|
||||
int tf = tfMap.getOrDefault(term, 0); //просто его count - то есть этого влияет на его вес в документе
|
||||
int df = stats.docFreq.getOrDefault(term, 1);
|
||||
|
||||
// BM25 IDF calculation редкость слова - оно поднимает
|
||||
double idf = Math.log(1 + (stats.totalDocs - df + 0.5) / (df + 0.5));
|
||||
|
||||
// BM25 term score calculation
|
||||
double numerator = tf * (K + 1);
|
||||
double denominator = tf + K * (1 - B + B * docLength / stats.avgDocLength);
|
||||
score += idf * (numerator / denominator);
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private List<String> tokenize(String text) {
|
||||
List<String> tokens = new ArrayList<>();
|
||||
Analyzer analyzer = detectLanguageAnalyzer(text);
|
||||
|
||||
try (TokenStream stream = analyzer.tokenStream(null, text)) {
|
||||
stream.reset();
|
||||
while (stream.incrementToken()) {
|
||||
tokens.add(stream.getAttribute(CharTermAttribute.class).toString());
|
||||
}
|
||||
stream.end();
|
||||
} catch (IOException e) {
|
||||
throw new RerankException(TOKENIZATION_ERROR + e.toString());
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private Analyzer detectLanguageAnalyzer(String text) {
|
||||
Language lang = languageDetector.detectLanguageOf(text);
|
||||
if (lang == Language.ENGLISH) {
|
||||
return new EnglishAnalyzer();
|
||||
} else if (lang == Language.RUSSIAN) {
|
||||
return new RussianAnalyzer();
|
||||
} else {
|
||||
// Fallback to English analyzer for unsupported languages
|
||||
return new EnglishAnalyzer();
|
||||
}
|
||||
}
|
||||
|
||||
// Inner class to hold corpus statistics
|
||||
private static class CorpusStats {
|
||||
final Map<String, Integer> docFreq;
|
||||
final Map<Document, List<String>> tokenizedDocs;
|
||||
final double avgDocLength;
|
||||
final int totalDocs;
|
||||
|
||||
CorpusStats(Map<String, Integer> docFreq,
|
||||
Map<Document, List<String>> tokenizedDocs,
|
||||
double avgDocLength,
|
||||
int totalDocs) {
|
||||
this.docFreq = docFreq;
|
||||
this.tokenizedDocs = tokenizedDocs;
|
||||
this.avgDocLength = avgDocLength;
|
||||
this.totalDocs = totalDocs;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.balex.rag.advisors.rag;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.springframework.ai.chat.client.ChatClientRequest;
|
||||
import org.springframework.ai.chat.client.ChatClientResponse;
|
||||
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
|
||||
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
|
||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.vectorstore.SearchRequest;
|
||||
import org.springframework.ai.vectorstore.VectorStore;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.balex.rag.advisors.expansion.ExpansionQueryAdvisor.ENRICHED_QUESTION;
|
||||
|
||||
|
||||
@Builder
|
||||
public class RagAdvisor implements BaseAdvisor {
|
||||
|
||||
private static final PromptTemplate template = PromptTemplate.builder().template("""
|
||||
CONTEXT: {context}
|
||||
Question: {question}
|
||||
""").build();
|
||||
|
||||
private final int rerankFetchMultiplier;
|
||||
private final int searchTopK;
|
||||
private final double similarityThreshold;
|
||||
private VectorStore vectorStore;
|
||||
|
||||
@Getter
|
||||
private final int order;
|
||||
|
||||
public static RagAdvisorBuilder build(VectorStore vectorStore) {
|
||||
return new RagAdvisorBuilder().vectorStore(vectorStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
|
||||
String originalUserQuestion = chatClientRequest.prompt().getUserMessage().getText();
|
||||
String queryToRag = chatClientRequest.context().getOrDefault(ENRICHED_QUESTION, originalUserQuestion).toString();
|
||||
|
||||
SearchRequest searchRequest = SearchRequest.builder()
|
||||
.query(queryToRag)
|
||||
.topK(searchTopK * rerankFetchMultiplier)
|
||||
.similarityThreshold(similarityThreshold)
|
||||
.build();
|
||||
|
||||
List<Document> documents = vectorStore.similaritySearch(searchRequest);
|
||||
|
||||
if (documents == null || documents.isEmpty()) {
|
||||
return chatClientRequest.mutate().context("CONTEXT", "EMPTY").build();
|
||||
}
|
||||
|
||||
BM25RerankEngine rerankEngine = BM25RerankEngine.builder().build();
|
||||
documents = rerankEngine.rerank(documents, queryToRag, searchTopK);
|
||||
|
||||
String llmContext = documents.stream()
|
||||
.map(Document::getText)
|
||||
.collect(Collectors.joining(System.lineSeparator()));
|
||||
|
||||
String finalUserPrompt = template.render(
|
||||
Map.of("context", llmContext, "question", originalUserQuestion));
|
||||
|
||||
return chatClientRequest.mutate()
|
||||
.prompt(chatClientRequest.prompt().augmentUserMessage(finalUserPrompt))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
|
||||
return chatClientResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.balex.rag.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
|
||||
@ConfigurationProperties(prefix = "rag.defaults")
|
||||
public record RagDefaultsProperties(
|
||||
@DefaultValue("true") boolean onlyContext,
|
||||
@DefaultValue("2") int topK,
|
||||
@DefaultValue("0.7") double topP,
|
||||
@DefaultValue("0.3") double temperature,
|
||||
@DefaultValue("1.1") double repeatPenalty,
|
||||
@DefaultValue("2") int searchTopK,
|
||||
@DefaultValue("0.3") double similarityThreshold
|
||||
) {}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.balex.rag.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
|
||||
@ConfigurationProperties(prefix = "rag.expansion")
|
||||
public record RagExpansionProperties(
|
||||
@DefaultValue("0.0") double temperature,
|
||||
@DefaultValue("1") int topK,
|
||||
@DefaultValue("0.1") double topP,
|
||||
@DefaultValue("1.0") double repeatPenalty
|
||||
) {}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.balex.rag.config;
|
||||
|
||||
import com.balex.rag.security.filter.JwtRequestFilter;
|
||||
import com.balex.rag.security.handler.AccessRestrictionHandler;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
||||
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;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
private final JwtRequestFilter jwtRequestFilter;
|
||||
private final AccessRestrictionHandler accessRestrictionHandler;
|
||||
|
||||
|
||||
// private static final String GET = "GET";
|
||||
// private static final String POST = "POST";
|
||||
// private static final AntPathRequestMatcher[] NOT_SECURED_URLS = new AntPathRequestMatcher[] {
|
||||
// new AntPathRequestMatcher("/auth/login", POST),
|
||||
// new AntPathRequestMatcher("/auth/register", POST),
|
||||
// };
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.cors(Customizer.withDefaults())
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll()
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/auth/refresh/token").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/auth/login", "/auth/register").permitAll()
|
||||
.requestMatchers(
|
||||
"/v3/api-docs/**",
|
||||
"/swagger-ui/**",
|
||||
"/swagger-ui.html",
|
||||
"/webjars/**",
|
||||
"/actuator/**"
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.exceptionHandling(exceptions -> exceptions
|
||||
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
|
||||
.accessDeniedHandler(accessRestrictionHandler)
|
||||
)
|
||||
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DaoAuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService) {
|
||||
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService);
|
||||
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
|
||||
return daoAuthenticationProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
|
||||
return authenticationConfiguration.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowedOrigins(List.of("http://localhost:5173"));
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("*"));
|
||||
config.setAllowCredentials(true);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return source;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.balex.rag.controller;
|
||||
|
||||
import com.balex.rag.model.constants.ApiLogMessage;
|
||||
import com.balex.rag.model.dto.UserProfileDTO;
|
||||
import com.balex.rag.model.request.user.LoginRequest;
|
||||
import com.balex.rag.model.request.user.RegistrationUserRequest;
|
||||
import com.balex.rag.model.response.RagResponse;
|
||||
import com.balex.rag.service.AuthService;
|
||||
import com.balex.rag.utils.ApiUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("${end.points.auth}")
|
||||
public class AuthController {
|
||||
private final AuthService authService;
|
||||
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successful authorization",
|
||||
content = @Content(mediaType = "application/json",
|
||||
examples = @ExampleObject(value = "{ \"token\": \"eyJhbGcIoIJIuz...\" }")))
|
||||
})
|
||||
@PostMapping("${end.points.login}")
|
||||
@Operation(summary = "User login", description = "Authenticates the user and returns an access/refresh token"
|
||||
)
|
||||
public ResponseEntity<?> login(
|
||||
@RequestBody @Valid LoginRequest request,
|
||||
HttpServletResponse response) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
|
||||
RagResponse<UserProfileDTO> result = authService.login(request);
|
||||
Cookie authorizationCookie = ApiUtils.createAuthCookie(result.getPayload().getToken());
|
||||
response.addCookie(authorizationCookie);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("${end.points.refresh.token}")
|
||||
@Operation(summary = "Refresh access token", description = "Generates new access token using provided refresh token"
|
||||
)
|
||||
public ResponseEntity<RagResponse<UserProfileDTO>> refreshToken(
|
||||
@RequestParam(name = "token") String refreshToken,
|
||||
HttpServletResponse response) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
|
||||
RagResponse<UserProfileDTO> result = authService.refreshAccessToken(refreshToken);
|
||||
Cookie authorizationCookie = ApiUtils.createAuthCookie(result.getPayload().getToken());
|
||||
response.addCookie(authorizationCookie);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("${end.points.register}")
|
||||
@Operation(summary = "Register a new user", description = "Creates new user and returns authentication details"
|
||||
)
|
||||
public ResponseEntity<?> register(
|
||||
@RequestBody @Valid RegistrationUserRequest request,
|
||||
HttpServletResponse response) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
|
||||
RagResponse<UserProfileDTO> result = authService.registerUser(request);
|
||||
Cookie authorizationCookie = ApiUtils.createAuthCookie(result.getPayload().getToken());
|
||||
response.addCookie(authorizationCookie);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.balex.rag.controller;
|
||||
|
||||
import com.balex.rag.model.constants.ApiLogMessage;
|
||||
import com.balex.rag.model.entity.Chat;
|
||||
import com.balex.rag.service.ChatService;
|
||||
import com.balex.rag.utils.ApiUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("${end.points.chat}")
|
||||
public class ChatController {
|
||||
|
||||
private final ChatService chatService;
|
||||
|
||||
@GetMapping("")
|
||||
public ResponseEntity<List<Chat>> mainPage() {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
List<Chat> response = chatService.getAllChats();
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/{chatId}")
|
||||
public ResponseEntity<Chat> showChat(@PathVariable Long chatId) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
Chat response = chatService.getChat(chatId);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping("/new")
|
||||
public ResponseEntity<Chat> newChat(@RequestParam String title) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
Chat chat = chatService.createNewChat(title);
|
||||
return ResponseEntity.ok(chat);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{chatId}")
|
||||
public ResponseEntity<Void> deleteChat(@PathVariable Long chatId) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
chatService.deleteChat(chatId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.balex.rag.controller;
|
||||
|
||||
import com.balex.rag.config.RagDefaultsProperties;
|
||||
import com.balex.rag.model.constants.ApiLogMessage;
|
||||
import com.balex.rag.model.dto.UserEntryRequest;
|
||||
import com.balex.rag.model.entity.ChatEntry;
|
||||
import com.balex.rag.service.ChatEntryService;
|
||||
import com.balex.rag.utils.ApiUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("${end.points.entry}")
|
||||
public class ChatEntryController {
|
||||
|
||||
private final ChatEntryService chatEntryService;
|
||||
private final RagDefaultsProperties ragDefaults;
|
||||
|
||||
@PostMapping("/{chatId}")
|
||||
public ResponseEntity<ChatEntry> addUserEntry(
|
||||
@PathVariable Long chatId,
|
||||
@RequestBody UserEntryRequest request) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
|
||||
boolean onlyContext = request.onlyContext() != null ? request.onlyContext() : ragDefaults.onlyContext();
|
||||
int topK = request.topK() != null ? request.topK() : ragDefaults.topK();
|
||||
double topP = request.topP() != null ? request.topP() : ragDefaults.topP();
|
||||
|
||||
ChatEntry entry = chatEntryService.addUserEntry(chatId, request.content(), onlyContext, topK, topP);
|
||||
return ResponseEntity.ok(entry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.balex.rag.controller;
|
||||
|
||||
import com.balex.rag.service.UserDocumentService;
|
||||
import com.balex.rag.utils.ApiUtils;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("${end.points.document}")
|
||||
public class DocumentUploadStreamController {
|
||||
|
||||
private final UserDocumentService userDocumentService;
|
||||
private final ApiUtils apiUtils;
|
||||
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Document processing progress stream",
|
||||
content = @Content(mediaType = "text/event-stream",
|
||||
examples = @ExampleObject(
|
||||
value = "data: {\"percent\": 33, \"processedFiles\": 1, \"totalFiles\": 3, \"currentFile\": \"doc1.txt\"}\n\n"
|
||||
)))
|
||||
})
|
||||
@PostMapping(value = "/upload-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter uploadDocumentsWithProgress(
|
||||
@RequestPart("files") @Valid List<MultipartFile> files
|
||||
) {
|
||||
Integer userId = apiUtils.getUserIdFromAuthentication();
|
||||
return userDocumentService.processUploadedFilesWithSse(files, userId.longValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.balex.rag.controller;
|
||||
|
||||
import com.balex.rag.model.constants.ApiLogMessage;
|
||||
import com.balex.rag.model.dto.UserDTO;
|
||||
import com.balex.rag.model.dto.UserSearchDTO;
|
||||
import com.balex.rag.model.entity.UserInfo;
|
||||
import com.balex.rag.model.request.user.NewUserRequest;
|
||||
import com.balex.rag.model.request.user.UpdateUserRequest;
|
||||
import com.balex.rag.model.response.PaginationResponse;
|
||||
import com.balex.rag.model.response.RagResponse;
|
||||
import com.balex.rag.service.UserService;
|
||||
import com.balex.rag.utils.ApiUtils;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("${end.points.users}")
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
|
||||
@GetMapping("${end.points.id}")
|
||||
public ResponseEntity<RagResponse<UserDTO>> getUserById(
|
||||
@PathVariable(name = "id") Integer userId) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
|
||||
RagResponse<UserDTO> response = userService.getById(userId);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping("${end.points.create}")
|
||||
public ResponseEntity<RagResponse<UserDTO>> createUser(
|
||||
@RequestBody @Valid NewUserRequest request) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
|
||||
RagResponse<UserDTO> createdUser = userService.createUser(request);
|
||||
return ResponseEntity.ok(createdUser);
|
||||
}
|
||||
|
||||
@GetMapping("${end.points.userinfo}")
|
||||
public ResponseEntity<RagResponse<UserInfo>> getUserInfo(
|
||||
@RequestHeader("Authorization") String token) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
|
||||
RagResponse<UserInfo> userInfo = userService.getUserInfo(token);
|
||||
|
||||
return ResponseEntity.ok(userInfo);
|
||||
}
|
||||
|
||||
@DeleteMapping("${end.points.userinfo}")
|
||||
public ResponseEntity<RagResponse<Integer>> deleteUserDocuments(
|
||||
@RequestHeader("Authorization") String token) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
|
||||
RagResponse<Integer> deletedCount = userService.deleteUserDocuments(token);
|
||||
|
||||
return ResponseEntity.ok(deletedCount);
|
||||
}
|
||||
|
||||
|
||||
@PutMapping("${end.points.id}")
|
||||
public ResponseEntity<RagResponse<UserDTO>> updateUserById(
|
||||
@PathVariable(name = "id") Integer userId,
|
||||
@RequestBody @Valid UpdateUserRequest request) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
|
||||
RagResponse<UserDTO> updatedPost = userService.updateUser(userId, request);
|
||||
return ResponseEntity.ok(updatedPost);
|
||||
}
|
||||
|
||||
@DeleteMapping("${end.points.id}")
|
||||
public ResponseEntity<Void> softDeleteUser(
|
||||
@PathVariable(name = "id") Integer userId
|
||||
) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
|
||||
userService.softDeleteUser(userId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@GetMapping("${end.points.all}")
|
||||
public ResponseEntity<RagResponse<PaginationResponse<UserSearchDTO>>> getAllUsers(
|
||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "limit", defaultValue = "10") int limit) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
|
||||
|
||||
Pageable pageable = PageRequest.of(page, limit);
|
||||
RagResponse<PaginationResponse<UserSearchDTO>> response = userService.findAllUsers(pageable);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.balex.rag.mapper;
|
||||
|
||||
import com.balex.rag.model.dto.UserDTO;
|
||||
import com.balex.rag.model.dto.UserProfileDTO;
|
||||
import com.balex.rag.model.dto.UserSearchDTO;
|
||||
import com.balex.rag.model.entity.User;
|
||||
import com.balex.rag.model.enums.RegistrationStatus;
|
||||
import com.balex.rag.model.request.user.NewUserRequest;
|
||||
import com.balex.rag.model.request.user.RegistrationUserRequest;
|
||||
import com.balex.rag.model.request.user.UpdateUserRequest;
|
||||
import org.hibernate.type.descriptor.DateTimeUtils;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.NullValuePropertyMappingStrategy;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Mapper(
|
||||
componentModel = "spring",
|
||||
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
|
||||
imports = {RegistrationStatus.class, Objects.class, DateTimeUtils.class}
|
||||
)
|
||||
public interface UserMapper {
|
||||
|
||||
UserDTO toDto(User user);
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "created", ignore = true)
|
||||
@Mapping(target = "registrationStatus", expression = "java(RegistrationStatus.ACTIVE)")
|
||||
User createUser(NewUserRequest request);
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "created", ignore = true)
|
||||
void updateUser(@MappingTarget User user, UpdateUserRequest request);
|
||||
|
||||
@Mapping(source = "deleted", target = "isDeleted")
|
||||
UserSearchDTO toUserSearchDto(User user);
|
||||
|
||||
@Mapping(target = "username", source = "user.username")
|
||||
@Mapping(target = "email", source = "user.email")
|
||||
@Mapping(target = "token", source = "token")
|
||||
@Mapping(target = "refreshToken", source = "refreshToken")
|
||||
UserProfileDTO toUserProfileDto(User user, String token, String refreshToken);
|
||||
|
||||
@Mapping(target = "password", ignore = true)
|
||||
@Mapping(target = "registrationStatus", expression = "java(RegistrationStatus.ACTIVE)")
|
||||
User fromDto(RegistrationUserRequest request);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.balex.rag.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class LoadedDocument {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String filename;
|
||||
private String contentHash;
|
||||
private String documentType;
|
||||
private int chunkCount;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime loadedAt;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.balex.rag.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UploadProgress {
|
||||
private int percent;
|
||||
private int processedFiles;
|
||||
private int totalFiles;
|
||||
private String currentFile;
|
||||
private String status; // "processing", "completed", "error"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.balex.rag.model.constants;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class ApiConstants {
|
||||
|
||||
public static final String UNDEFINED = "undefined";
|
||||
public static final String EMPTY_FILENAME = "unknown";
|
||||
public static final String NO_NEW_DOCUMENTS_UPLOADED = "No new documents uploaded";
|
||||
public static final String DOCUMENTS_UPLOADED = "Documents uploaded: ";
|
||||
public static final String ANSI_RED = "\u001B[31m";
|
||||
public static final String ANSI_WHITE = "\u001B[37m";
|
||||
public static final String BREAK_LINE = "\n";
|
||||
public static final String TIME_ZONE_PACKAGE_NAME = "java.time.zone";
|
||||
public static final String DASH = "-";
|
||||
public static final String PASSWORD_ALL_CHARACTERS =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?";
|
||||
public static final String PASSWORD_LETTERS_UPPER_CASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
public static final String PASSWORD_LETTERS_LOWER_CASE = "abcdefghijklmnopqrstuvwxyz";
|
||||
public static final String PASSWORD_DIGITS = "0123456789";
|
||||
public static final String PASSWORD_CHARACTERS = "~`!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?";
|
||||
public static final Integer REQUIRED_MIN_PASSWORD_LENGTH = 8;
|
||||
public static final Integer REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD = 1;
|
||||
public static final Integer REQUIRED_MIN_DIGITS_NUMBER_IN_PASSWORD = 1;
|
||||
public static final Integer REQUIRED_MIN_CHARACTERS_NUMBER_IN_PASSWORD = 1;
|
||||
public static final String USER_ROLE = "USER_ROLE";
|
||||
public static final Integer MAX_FILES_ALLOWED_FOR_LOAD = 10;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.balex.rag.model.constants;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public enum ApiErrorMessage {
|
||||
POST_NOT_FOUND_BY_ID("Post with ID: %s was not found"),
|
||||
POST_ALREADY_EXISTS("Post with Title: %s already exists"),
|
||||
USER_NOT_FOUND_BY_ID("User with ID: %s was not found"),
|
||||
USERNAME_ALREADY_EXISTS("Username: %s already exists"),
|
||||
USERNAME_NOT_FOUND("Username: %s was not found"),
|
||||
EMAIL_ALREADY_EXISTS("Email: %s already exists"),
|
||||
EMAIL_NOT_FOUND("Email: %s was not found"),
|
||||
USER_ROLE_NOT_FOUND("Role was not found"),
|
||||
COMMENT_NOT_FOUND_BY_ID("Comment with ID: %s was not found"),
|
||||
|
||||
TOKENIZATION_ERROR("Tokenization failed"),
|
||||
|
||||
UPLOADED_FILENAME_EMPTY("Filename is empty"),
|
||||
UPLOAD_FILE_READ_ERROR("Failed to read file"),
|
||||
|
||||
INVALID_TOKEN_SIGNATURE("Invalid token signature"),
|
||||
ERROR_DURING_JWT_PROCESSING("An unexpected error occurred during JWT processing"),
|
||||
TOKEN_EXPIRED("Token expired."),
|
||||
UNEXPECTED_ERROR_OCCURRED("An unexpected error occurred. Please try again later."),
|
||||
|
||||
AUTHENTICATION_FAILED_FOR_USER("Authentication failed for user: {}. "),
|
||||
INVALID_USER_OR_PASSWORD("Invalid email or password. Try again"),
|
||||
INVALID_USER_REGISTRATION_STATUS("Invalid user registration status: %s. "),
|
||||
NOT_FOUND_REFRESH_TOKEN("Refresh token not found."),
|
||||
|
||||
MISMATCH_PASSWORDS("Password does not match"),
|
||||
INVALID_PASSWORD("Invalid password. It must have: "
|
||||
+ "length at least " + ApiConstants.REQUIRED_MIN_PASSWORD_LENGTH + ", including "
|
||||
+ ApiConstants.REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD + " letter(s) in upper and lower cases, "
|
||||
+ ApiConstants.REQUIRED_MIN_CHARACTERS_NUMBER_IN_PASSWORD + " character(s), "
|
||||
+ ApiConstants.REQUIRED_MIN_DIGITS_NUMBER_IN_PASSWORD + " digit(s). "),
|
||||
HAVE_NO_ACCESS("You don't have the necessary permissions"),
|
||||
KAFKA_SEND_FAILED("Kafka message didn't send."),
|
||||
;
|
||||
|
||||
private final String message;
|
||||
|
||||
public String getMessage(Object... args) {
|
||||
return String.format(message, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.balex.rag.model.constants;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public enum ApiLogMessage {
|
||||
NAME_OF_CURRENT_METHOD("Current method: {}");
|
||||
private final String value;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.balex.rag.model.constants;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public enum ApiMessage {
|
||||
TOKEN_CREATED_OR_UPDATED("User's token has been created or updated"),
|
||||
;
|
||||
|
||||
private final String message;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.balex.rag.model.dto;
|
||||
|
||||
import com.balex.rag.model.enums.RegistrationStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class UserDTO implements Serializable {
|
||||
|
||||
private Integer id;
|
||||
private String username;
|
||||
private String email;
|
||||
private LocalDateTime created;
|
||||
private LocalDateTime lastLogin;
|
||||
|
||||
private RegistrationStatus registrationStatus;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.balex.rag.model.dto;
|
||||
|
||||
public record UserEntryRequest(
|
||||
String content,
|
||||
Boolean onlyContext,
|
||||
Integer topK,
|
||||
Double topP
|
||||
) {}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.balex.rag.model.dto;
|
||||
|
||||
import com.balex.rag.model.enums.RegistrationStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class UserProfileDTO implements Serializable {
|
||||
private Integer id;
|
||||
private String username;
|
||||
private String email;
|
||||
|
||||
private RegistrationStatus registrationStatus;
|
||||
private LocalDateTime lastLogin;
|
||||
|
||||
private String token;
|
||||
private String refreshToken;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.balex.rag.model.dto;
|
||||
|
||||
import com.balex.rag.model.enums.RegistrationStatus;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class UserSearchDTO implements Serializable {
|
||||
|
||||
private Integer id;
|
||||
private String username;
|
||||
private String email;
|
||||
private LocalDateTime created;
|
||||
private Boolean isDeleted;
|
||||
|
||||
private RegistrationStatus registrationStatus;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.balex.rag.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Chat {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String title;
|
||||
|
||||
@Column(name = "id_owner", nullable = false)
|
||||
private Long idOwner;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@OrderBy("createdAt ASC")
|
||||
@OneToMany(mappedBy = "chat", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<ChatEntry> history = new ArrayList<>();
|
||||
|
||||
|
||||
public void addChatEntry(ChatEntry entry) {
|
||||
history.add(entry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.balex.rag.model.entity;
|
||||
|
||||
import com.balex.rag.model.enums.Role;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChatEntry {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String content;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Role role;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "chat_id")
|
||||
@JsonIgnore
|
||||
private Chat chat;
|
||||
|
||||
public static ChatEntry toChatEntry(Message message) {
|
||||
return ChatEntry.builder()
|
||||
.role(Role.getRole(message.getMessageType().getValue()))
|
||||
.content(message.getText())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
public Message toMessage() {
|
||||
return role.getMessage(content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.balex.rag.model.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class LoadedDocumentInfo implements Serializable {
|
||||
Long id;
|
||||
String fileName;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.balex.rag.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@Table(name = "refresh_token")
|
||||
public class RefreshToken {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String token;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime created;
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private User user;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.balex.rag.model.entity;
|
||||
|
||||
import com.balex.rag.model.enums.RegistrationStatus;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class User {
|
||||
public static final String ID_FIELD = "id";
|
||||
public static final String USERNAME_NAME_FIELD = "username";
|
||||
public static final String EMAIL_NAME_FIELD = "email";
|
||||
public static final String DELETED_FIELD = "deleted";
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Size(max = 30)
|
||||
@Column(nullable = false, length = 30)
|
||||
private String username;
|
||||
|
||||
@Size(max = 80)
|
||||
@Column(nullable = false, length = 80)
|
||||
private String password;
|
||||
|
||||
@Size(max = 50)
|
||||
@Column(nullable = false, length = 50)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false, updatable = false)
|
||||
private LocalDateTime created = LocalDateTime.now();
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime updated = LocalDateTime.now();
|
||||
|
||||
@Column(name = "last_login")
|
||||
private LocalDateTime lastLogin;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean deleted = false;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "registration_status", nullable = false)
|
||||
private RegistrationStatus registrationStatus;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.balex.rag.model.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import static com.balex.rag.model.constants.ApiConstants.MAX_FILES_ALLOWED_FOR_LOAD;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class UserInfo implements Serializable {
|
||||
|
||||
Integer id;
|
||||
|
||||
String username;
|
||||
|
||||
String email;
|
||||
|
||||
List<LoadedDocumentInfo> loadedFiles;
|
||||
|
||||
Integer maxLoadedFiles = MAX_FILES_ALLOWED_FOR_LOAD;
|
||||
|
||||
public UserInfo(Integer id, String username, String email, List<LoadedDocumentInfo> loadedFiles) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
this.loadedFiles = loadedFiles;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.balex.rag.model.enums;
|
||||
|
||||
public enum RegistrationStatus {
|
||||
ACTIVE,
|
||||
INACTIVE
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.balex.rag.model.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public enum ResponseStyle {
|
||||
CONCISE("Answer briefly."),
|
||||
DETAILED("Provide detailed explanations.");
|
||||
|
||||
private final String instruction;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.balex.rag.model.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public enum Role {
|
||||
|
||||
USER("user") {
|
||||
@Override
|
||||
public Message getMessage(String message) {
|
||||
return new UserMessage(message);
|
||||
}
|
||||
}, ASSISTANT("assistant") {
|
||||
@Override
|
||||
public Message getMessage(String message) {
|
||||
return new AssistantMessage(message);
|
||||
}
|
||||
}, SYSTEM("system") {
|
||||
@Override
|
||||
public Message getMessage(String prompt) {
|
||||
return new SystemMessage(prompt);
|
||||
}
|
||||
};
|
||||
|
||||
private final String role;
|
||||
|
||||
|
||||
public static Role getRole(String roleName) {
|
||||
return Arrays.stream(Role.values()).filter(role -> role.role.equals(roleName)).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
public abstract Message getMessage(String prompt);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.balex.rag.model.exception;
|
||||
|
||||
public class DataExistException extends RuntimeException {
|
||||
|
||||
public DataExistException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.balex.rag.model.exception;
|
||||
|
||||
public class InvalidDataException extends RuntimeException {
|
||||
|
||||
public InvalidDataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.balex.rag.model.exception;
|
||||
|
||||
public class InvalidPasswordException extends RuntimeException {
|
||||
|
||||
public InvalidPasswordException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.balex.rag.model.exception;
|
||||
|
||||
public class InvalidTokenException extends RuntimeException {
|
||||
|
||||
public InvalidTokenException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.balex.rag.model.exception;
|
||||
|
||||
public class NotFoundException extends RuntimeException {
|
||||
|
||||
public NotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.balex.rag.model.exception;
|
||||
|
||||
public class RerankException extends RuntimeException {
|
||||
|
||||
public RerankException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.balex.rag.model.exception;
|
||||
|
||||
public class UploadException extends RuntimeException {
|
||||
|
||||
public UploadException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UploadException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.balex.rag.model.request.user;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginRequest implements Serializable {
|
||||
|
||||
@Email
|
||||
@NotNull
|
||||
private String email;
|
||||
|
||||
@NotEmpty
|
||||
private String password;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.balex.rag.model.request.user;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class NewUserRequest {
|
||||
|
||||
@NotBlank(message = "Username cannot be empty")
|
||||
@Size(max = 30)
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "Password cannot be empty")
|
||||
@Size(max = 50)
|
||||
private String password;
|
||||
|
||||
@NotBlank(message = "Email cannot be empty")
|
||||
@Size(max = 50)
|
||||
private String email;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.balex.rag.model.request.user;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class RegistrationUserRequest implements Serializable {
|
||||
|
||||
@NotBlank
|
||||
private String username;
|
||||
|
||||
@Email
|
||||
@NotNull
|
||||
private String email;
|
||||
|
||||
@NotEmpty
|
||||
private String password;
|
||||
|
||||
@NotEmpty
|
||||
private String confirmPassword;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.balex.rag.model.request.user;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class UpdateUserRequest implements Serializable {
|
||||
|
||||
private String username;
|
||||
private String email;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.balex.rag.model.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PaginationResponse<T> implements Serializable {
|
||||
private List<T> content;
|
||||
private Pagination pagination;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Pagination implements Serializable {
|
||||
private long total;
|
||||
private int limit;
|
||||
private int page;
|
||||
private int pages;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.balex.rag.model.response;
|
||||
|
||||
import com.balex.rag.model.constants.ApiMessage;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
|
||||
public class RagResponse<P extends Serializable> implements Serializable {
|
||||
private String message;
|
||||
private P payload;
|
||||
private boolean success;
|
||||
|
||||
public static <P extends Serializable> RagResponse<P> createSuccessful(P payload) {
|
||||
return new RagResponse<>(StringUtils.EMPTY, payload, true);
|
||||
}
|
||||
|
||||
public static <P extends Serializable> RagResponse<P> createSuccessfulWithNewToken(P payload) {
|
||||
return new RagResponse<>(ApiMessage.TOKEN_CREATED_OR_UPDATED.getMessage(), payload, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.balex.rag.repo;
|
||||
|
||||
import com.balex.rag.model.entity.ChatEntry;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface ChatEntryRepository extends JpaRepository<ChatEntry, Long> {
|
||||
|
||||
List<ChatEntry> findByChatIdOrderByCreatedAtAsc(Long chatId);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.balex.rag.repo;
|
||||
|
||||
import com.balex.rag.model.entity.Chat;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ChatRepository extends JpaRepository<Chat, Long> {
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.balex.rag.repo;
|
||||
|
||||
import com.balex.rag.model.LoadedDocument;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface DocumentRepository extends JpaRepository<LoadedDocument, Long> {
|
||||
|
||||
boolean existsByFilenameAndContentHash(String filename, String contentHash);
|
||||
|
||||
List<LoadedDocument> findByUserId(Integer userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.balex.rag.repo;
|
||||
|
||||
import com.balex.rag.model.entity.RefreshToken;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Integer> {
|
||||
|
||||
Optional<RefreshToken> findByToken(String token);
|
||||
|
||||
Optional<RefreshToken> findByUserId(Integer userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.balex.rag.repo;
|
||||
|
||||
import com.balex.rag.model.entity.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User> {
|
||||
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
Optional<User> findByIdAndDeletedFalse (Integer id);
|
||||
|
||||
Optional<User> findUserByEmailAndDeletedFalse(String email);
|
||||
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.balex.rag.repo;
|
||||
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface VectorStoreRepository {
|
||||
|
||||
void deleteBySourceIn(List<String> sources);
|
||||
|
||||
void deleteByUserId(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.balex.rag.repo.impl;
|
||||
|
||||
import com.balex.rag.repo.VectorStoreRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class VectorStoreRepositoryImpl implements VectorStoreRepository {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public void deleteBySourceIn(List<String> sources) {
|
||||
if (sources == null || sources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String placeholders = String.join(",", sources.stream()
|
||||
.map(s -> "?")
|
||||
.toList());
|
||||
|
||||
String sql = "DELETE FROM vector_store WHERE metadata->>'source' IN (" + placeholders + ")";
|
||||
|
||||
jdbcTemplate.update(sql, sources.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByUserId(Long userId) {
|
||||
String sql = "DELETE FROM vector_store WHERE (metadata->>'user_id')::bigint = ?";
|
||||
jdbcTemplate.update(sql, userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.balex.rag.security;
|
||||
|
||||
import com.balex.rag.model.entity.User;
|
||||
import com.balex.rag.service.model.AuthenticationConstants;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.io.Decoders;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
private final SecretKey secretKey;
|
||||
private final Long jwtValidityInMilliseconds;
|
||||
|
||||
public JwtTokenProvider(@Value("${jwt.secret}") String secret,
|
||||
@Value("${jwt.expiration:3600000}") long jwtValidityInMilliseconds) {
|
||||
this.secretKey = getKey(secret);
|
||||
this.jwtValidityInMilliseconds = jwtValidityInMilliseconds;
|
||||
}
|
||||
|
||||
public String generateToken(@NonNull User user) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put(AuthenticationConstants.USER_ID, user.getId());
|
||||
claims.put(AuthenticationConstants.USERNAME, user.getUsername());
|
||||
claims.put(AuthenticationConstants.USER_EMAIL, user.getEmail());
|
||||
claims.put(AuthenticationConstants.USER_REGISTRATION_STATUS, user.getRegistrationStatus().name());
|
||||
claims.put(AuthenticationConstants.LAST_UPDATE, LocalDateTime.now().toString());
|
||||
|
||||
return createToken(claims, user.getEmail());
|
||||
}
|
||||
|
||||
public String refreshToken(String token) {
|
||||
Claims claims = getAllClaimsFromToken(token);
|
||||
return createToken(claims, claims.getSubject());
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
Jws<Claims> claims = Jwts.parserBuilder()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token);
|
||||
return !claims.getBody().getExpiration().before(new Date());
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public String getUsername(String token) {
|
||||
Claims claims = getAllClaimsFromToken(token);
|
||||
return claims.get(AuthenticationConstants.USERNAME).toString();
|
||||
}
|
||||
|
||||
public String getUserId(String token) {
|
||||
Claims claims = getAllClaimsFromToken(token);
|
||||
return String.valueOf(claims.get(AuthenticationConstants.USER_ID));
|
||||
}
|
||||
|
||||
|
||||
private Claims getAllClaimsFromToken(String token) {
|
||||
try {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
} catch (ExpiredJwtException e) {
|
||||
return e.getClaims();
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKey getKey(String secretKey64) {
|
||||
byte[] decode64 = Decoders.BASE64.decode(secretKey64);
|
||||
return Keys.hmacShaKeyFor(decode64);
|
||||
}
|
||||
|
||||
private String createToken(Map<String, Object> claims, String subject) {
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(new Date())
|
||||
.setExpiration(new Date(System.currentTimeMillis() + jwtValidityInMilliseconds))
|
||||
.signWith(secretKey, SignatureAlgorithm.HS512)
|
||||
.compact();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.balex.rag.security.filter;
|
||||
|
||||
import com.balex.rag.model.constants.ApiErrorMessage;
|
||||
import com.balex.rag.security.JwtTokenProvider;
|
||||
import io.jsonwebtoken.ExpiredJwtException;
|
||||
import io.jsonwebtoken.MalformedJwtException;
|
||||
import io.jsonwebtoken.SignatureException;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.balex.rag.model.constants.ApiConstants.USER_ROLE;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtRequestFilter extends OncePerRequestFilter {
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
private static final String LOGIN_PATH = "/auth/login";
|
||||
private static final String REGISTER_PATH = "/auth/register";
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NotNull HttpServletRequest request,
|
||||
@NotNull HttpServletResponse response,
|
||||
@NotNull FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
log.info("JWT filter: method={}, uri={}, header={}",
|
||||
request.getMethod(),
|
||||
request.getRequestURI(),
|
||||
request.getHeader(AUTHORIZATION_HEADER));
|
||||
|
||||
Optional<String> authHeader = Optional.ofNullable(request.getHeader(AUTHORIZATION_HEADER));
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
if (authHeader.isPresent() && authHeader.get().startsWith(BEARER_PREFIX)) {
|
||||
String jwt = authHeader.get().substring(BEARER_PREFIX.length());
|
||||
try {
|
||||
if (!jwtTokenProvider.validateToken(jwt)) {
|
||||
throw new ExpiredJwtException(null, null, ApiErrorMessage.TOKEN_EXPIRED.getMessage());
|
||||
}
|
||||
|
||||
Optional<String> emailOpt = Optional.ofNullable(jwtTokenProvider.getUsername(jwt));
|
||||
Optional<String> userIdOpt = Optional.ofNullable(jwtTokenProvider.getUserId(jwt));
|
||||
|
||||
if (emailOpt.isPresent() && userIdOpt.isPresent()) {
|
||||
if (SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
List<SimpleGrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority(USER_ROLE));
|
||||
|
||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||
emailOpt.get(),
|
||||
jwt,
|
||||
authorities
|
||||
);
|
||||
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (ExpiredJwtException e) {
|
||||
handleTokenExpiration(requestURI, jwt, response);
|
||||
return;
|
||||
} catch (SignatureException | MalformedJwtException e) {
|
||||
handleSignatureException(response);
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
handleUnexpectedException(response, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private void handleTokenExpiration(String requestURI, String jwt, HttpServletResponse response) throws IOException {
|
||||
if (isAuthEndpoint(requestURI)) {
|
||||
String refreshedToken = jwtTokenProvider.refreshToken(jwt);
|
||||
response.setHeader(AUTHORIZATION_HEADER, BEARER_PREFIX + refreshedToken);
|
||||
} else {
|
||||
sendErrorResponse(response, HttpStatus.UNAUTHORIZED, ApiErrorMessage.TOKEN_EXPIRED.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSignatureException(HttpServletResponse response) throws IOException {
|
||||
sendErrorResponse(response, HttpStatus.UNAUTHORIZED, ApiErrorMessage.INVALID_TOKEN_SIGNATURE.getMessage());
|
||||
}
|
||||
|
||||
private void handleUnexpectedException(HttpServletResponse response, Exception e) throws IOException {
|
||||
log.error(ApiErrorMessage.ERROR_DURING_JWT_PROCESSING.getMessage(), e);
|
||||
sendErrorResponse(response, HttpStatus.INTERNAL_SERVER_ERROR, ApiErrorMessage.UNEXPECTED_ERROR_OCCURRED.getMessage());
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpServletResponse response, HttpStatus status, String message) throws IOException {
|
||||
response.setStatus(status.value());
|
||||
response.getWriter().write(message);
|
||||
}
|
||||
|
||||
private boolean isAuthEndpoint(String uri) {
|
||||
return uri.equals(LOGIN_PATH) || uri.equals(REGISTER_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.balex.rag.security.handler;
|
||||
|
||||
import com.balex.rag.model.constants.ApiErrorMessage;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AccessRestrictionHandler implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void handle(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AccessDeniedException accessDeniedException) {
|
||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
response.getWriter().write(ApiErrorMessage.HAVE_NO_ACCESS.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.balex.rag.security.validation;
|
||||
|
||||
import com.balex.rag.model.constants.ApiErrorMessage;
|
||||
import com.balex.rag.model.entity.User;
|
||||
import com.balex.rag.model.exception.InvalidDataException;
|
||||
import com.balex.rag.model.exception.InvalidPasswordException;
|
||||
import com.balex.rag.model.exception.NotFoundException;
|
||||
import com.balex.rag.repo.UserRepository;
|
||||
import com.balex.rag.service.model.exception.DataExistException;
|
||||
import com.balex.rag.utils.ApiUtils;
|
||||
import com.balex.rag.utils.PasswordUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.file.AccessDeniedException;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AccessValidator {
|
||||
private final UserRepository userRepository;
|
||||
private final ApiUtils apiUtils;
|
||||
|
||||
public void validateNewUser(String username, String email, String password, String confirmPassword) {
|
||||
userRepository.findByUsername(username).ifPresent(existingUser -> {
|
||||
throw new DataExistException(ApiErrorMessage.USERNAME_ALREADY_EXISTS.getMessage(username));
|
||||
});
|
||||
|
||||
userRepository.findByEmail(email).ifPresent(existingUser -> {
|
||||
throw new DataExistException(ApiErrorMessage.EMAIL_ALREADY_EXISTS.getMessage(email));
|
||||
});
|
||||
|
||||
if (!password.equals(confirmPassword)) {
|
||||
throw new InvalidDataException(ApiErrorMessage.MISMATCH_PASSWORDS.getMessage());
|
||||
}
|
||||
|
||||
if (PasswordUtils.isNotValidPassword(password)) {
|
||||
throw new InvalidPasswordException(ApiErrorMessage.INVALID_PASSWORD.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void validateOwnerAccess(Integer ownerId) {
|
||||
Integer currentUserId = apiUtils.getUserIdFromAuthentication();
|
||||
|
||||
if (!currentUserId.equals(ownerId)) {
|
||||
throw new AccessDeniedException(ApiErrorMessage.HAVE_NO_ACCESS.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.balex.rag.security.validation;
|
||||
|
||||
import com.balex.rag.model.request.user.RegistrationUserRequest;
|
||||
import com.balex.rag.utils.PasswordMatches;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, RegistrationUserRequest> {
|
||||
|
||||
@Override
|
||||
public boolean isValid(RegistrationUserRequest request, ConstraintValidatorContext constraintValidatorContext) {
|
||||
return request.getPassword().equals(request.getConfirmPassword());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.balex.rag.service;
|
||||
|
||||
import com.balex.rag.model.dto.UserProfileDTO;
|
||||
import com.balex.rag.model.request.user.LoginRequest;
|
||||
import com.balex.rag.model.request.user.RegistrationUserRequest;
|
||||
import com.balex.rag.model.response.RagResponse;
|
||||
|
||||
public interface AuthService {
|
||||
|
||||
RagResponse<UserProfileDTO> login(LoginRequest request);
|
||||
|
||||
RagResponse<UserProfileDTO> refreshAccessToken(String refreshToken);
|
||||
|
||||
RagResponse<UserProfileDTO> registerUser(RegistrationUserRequest request);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.balex.rag.service;
|
||||
|
||||
import com.balex.rag.model.entity.ChatEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ChatEntryService {
|
||||
|
||||
List<ChatEntry> getEntriesByChatId(Long chatId);
|
||||
|
||||
ChatEntry addUserEntry(Long chatId, String content, boolean onlyContext, int topK, double topP);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.balex.rag.service;
|
||||
|
||||
import com.balex.rag.model.entity.Chat;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ChatService {
|
||||
|
||||
Chat createNewChat(String title);
|
||||
|
||||
List<Chat> getAllChats();
|
||||
|
||||
Chat getChat(Long chatId);
|
||||
|
||||
void deleteChat(Long chatId);
|
||||
|
||||
SseEmitter proceedInteractionWithStreaming(Long chatId, String userPrompt);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.balex.rag.service;
|
||||
|
||||
import com.balex.rag.model.entity.Chat;
|
||||
import com.balex.rag.model.entity.ChatEntry;
|
||||
import com.balex.rag.repo.ChatRepository;
|
||||
import lombok.Builder;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Builder
|
||||
public class PostgresChatMemory implements ChatMemory {
|
||||
|
||||
private ChatRepository chatMemoryRepository;
|
||||
|
||||
private int maxMessages;
|
||||
|
||||
@Override
|
||||
public void add(String conversationId, List<Message> messages) {
|
||||
// Chat chat = chatMemoryRepository.findById(Long.valueOf(conversationId)).orElseThrow();
|
||||
// for (Message message : messages) {
|
||||
// chat.addChatEntry(ChatEntry.toChatEntry(message));
|
||||
// }
|
||||
// chatMemoryRepository.save(chat);
|
||||
|
||||
// No-op: messages are saved manually in ChatEntryServiceImpl
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<Message> get(String conversationId) {
|
||||
Chat chat = chatMemoryRepository.findById(Long.valueOf(conversationId)).orElseThrow();
|
||||
Long messagesToSkip= (long) Math.max(0, chat.getHistory().size() - maxMessages);
|
||||
return chat.getHistory().stream()
|
||||
.skip(messagesToSkip)
|
||||
//.sorted(Comparator.comparing(ChatEntry::getCreatedAt))
|
||||
//.sorted(Comparator.comparing(ChatEntry::getCreatedAt).reversed())
|
||||
.map(ChatEntry::toMessage)
|
||||
.limit(maxMessages)
|
||||
.toList();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear(String conversationId) {
|
||||
//not implemented
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.balex.rag.service;
|
||||
|
||||
import com.balex.rag.model.entity.RefreshToken;
|
||||
import com.balex.rag.model.entity.User;
|
||||
|
||||
public interface RefreshTokenService {
|
||||
|
||||
RefreshToken generateOrUpdateRefreshToken(User user);
|
||||
|
||||
RefreshToken validateAndRefreshToken(String refreshToken);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.balex.rag.service;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface UserDocumentService {
|
||||
SseEmitter processUploadedFilesWithSse(List<MultipartFile> files, Long userId);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.balex.rag.service;
|
||||
|
||||
import com.balex.rag.model.dto.UserDTO;
|
||||
import com.balex.rag.model.dto.UserSearchDTO;
|
||||
import com.balex.rag.model.entity.UserInfo;
|
||||
import com.balex.rag.model.request.user.NewUserRequest;
|
||||
import com.balex.rag.model.request.user.UpdateUserRequest;
|
||||
import com.balex.rag.model.response.PaginationResponse;
|
||||
import com.balex.rag.model.response.RagResponse;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
|
||||
public interface UserService extends UserDetailsService {
|
||||
|
||||
RagResponse<UserDTO> getById(@NotNull Integer userId);
|
||||
|
||||
RagResponse<UserDTO> createUser(@NotNull NewUserRequest request);
|
||||
|
||||
RagResponse<UserDTO> updateUser(@NotNull Integer postId, @NotNull UpdateUserRequest request);
|
||||
|
||||
void softDeleteUser(Integer userId);
|
||||
|
||||
RagResponse<PaginationResponse<UserSearchDTO>> findAllUsers(Pageable pageable);
|
||||
|
||||
RagResponse<UserInfo> getUserInfo(String token);
|
||||
|
||||
RagResponse<Integer> deleteUserDocuments(String token);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//package com.balex.rag.service.autostart;
|
||||
//
|
||||
//import com.balex.rag.model.LoadedDocument;
|
||||
//import com.balex.rag.repo.DocumentRepository;
|
||||
//import lombok.SneakyThrows;
|
||||
//import org.springframework.ai.document.Document;
|
||||
//import org.springframework.ai.reader.TextReader;
|
||||
//import org.springframework.ai.transformer.splitter.TokenTextSplitter;
|
||||
//import org.springframework.ai.vectorstore.VectorStore;
|
||||
//import org.springframework.beans.factory.annotation.Autowired;
|
||||
//import org.springframework.boot.CommandLineRunner;
|
||||
//import org.springframework.core.io.Resource;
|
||||
//import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
//import org.springframework.data.util.Pair;
|
||||
//import org.springframework.stereotype.Service;
|
||||
//import org.springframework.util.DigestUtils;
|
||||
//
|
||||
//import java.util.Arrays;
|
||||
//import java.util.List;
|
||||
//
|
||||
//@Service
|
||||
//public class DocumentLoaderService implements CommandLineRunner {
|
||||
//
|
||||
// @Autowired
|
||||
// private DocumentRepository documentRepository;
|
||||
//
|
||||
// @Autowired
|
||||
// private ResourcePatternResolver resolver;
|
||||
//
|
||||
// @Autowired
|
||||
// private VectorStore vectorStore;
|
||||
//
|
||||
//
|
||||
// @SneakyThrows
|
||||
// public void loadDocuments() {
|
||||
// List<Resource> resources = Arrays.stream(resolver.getResources("classpath:/knowledgebase/**/*.txt")).toList();
|
||||
//
|
||||
// resources.stream()
|
||||
// .map(r -> Pair.of(r, calcContentHash(r)))
|
||||
// .filter(p -> !documentRepository.existsByFilenameAndContentHash(p.getFirst().getFilename(), p.getSecond()))
|
||||
// .forEach(p -> {
|
||||
// Resource resource = p.getFirst();
|
||||
// List<Document> docs = new TextReader(resource).get();
|
||||
// TokenTextSplitter splitter = TokenTextSplitter.builder().withChunkSize(200).build();
|
||||
// List<Document> chunks = splitter.apply(docs);
|
||||
//
|
||||
// for (Document c : chunks) {
|
||||
// acceptWithRetry(vectorStore, List.of(c), 3, 1500);
|
||||
// }
|
||||
//
|
||||
// LoadedDocument loaded = LoadedDocument.builder()
|
||||
// .documentType("txt")
|
||||
// .chunkCount(chunks.size())
|
||||
// .filename(resource.getFilename())
|
||||
// .contentHash(p.getSecond())
|
||||
// .build();
|
||||
// documentRepository.save(loaded);
|
||||
// });
|
||||
//
|
||||
// }
|
||||
//
|
||||
// private static void acceptWithRetry(VectorStore vs, List<Document> part, int attempts, long sleepMs) {
|
||||
// RuntimeException last = null;
|
||||
// for (int i = 0; i < attempts; i++) {
|
||||
// try {
|
||||
// vs.accept(part);
|
||||
// return;
|
||||
// } catch (RuntimeException e) {
|
||||
// last = e;
|
||||
// try { Thread.sleep(sleepMs); } catch (InterruptedException ignored) {}
|
||||
// }
|
||||
// }
|
||||
// throw last;
|
||||
// }
|
||||
//
|
||||
// @SneakyThrows
|
||||
// private String calcContentHash(Resource resource) {
|
||||
// return DigestUtils.md5DigestAsHex(resource.getInputStream());
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void run(String... args) {
|
||||
// loadDocuments();
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.balex.rag.service.impl;
|
||||
|
||||
import com.balex.rag.mapper.UserMapper;
|
||||
import com.balex.rag.model.constants.ApiErrorMessage;
|
||||
import com.balex.rag.model.dto.UserProfileDTO;
|
||||
import com.balex.rag.model.entity.RefreshToken;
|
||||
import com.balex.rag.model.entity.User;
|
||||
import com.balex.rag.model.exception.InvalidDataException;
|
||||
import com.balex.rag.model.request.user.LoginRequest;
|
||||
import com.balex.rag.model.request.user.RegistrationUserRequest;
|
||||
import com.balex.rag.model.response.RagResponse;
|
||||
import com.balex.rag.repo.UserRepository;
|
||||
import com.balex.rag.security.JwtTokenProvider;
|
||||
import com.balex.rag.security.validation.AccessValidator;
|
||||
import com.balex.rag.service.AuthService;
|
||||
import com.balex.rag.service.RefreshTokenService;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
private final UserRepository userRepository;
|
||||
private final UserMapper userMapper;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final RefreshTokenService refreshTokenService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final AccessValidator accessValidator;
|
||||
|
||||
|
||||
@Override
|
||||
public RagResponse<UserProfileDTO> login(@NotNull LoginRequest request) {
|
||||
try {
|
||||
authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())
|
||||
);
|
||||
} catch (BadCredentialsException e) {
|
||||
throw new InvalidDataException(ApiErrorMessage.INVALID_USER_OR_PASSWORD.getMessage());
|
||||
}
|
||||
|
||||
User user = userRepository.findUserByEmailAndDeletedFalse(request.getEmail())
|
||||
.orElseThrow(() -> new InvalidDataException(ApiErrorMessage.INVALID_USER_OR_PASSWORD.getMessage()));
|
||||
|
||||
RefreshToken refreshToken = refreshTokenService.generateOrUpdateRefreshToken(user);
|
||||
String token = jwtTokenProvider.generateToken(user);
|
||||
UserProfileDTO userProfileDTO = userMapper.toUserProfileDto(user, token, refreshToken.getToken());
|
||||
userProfileDTO.setToken(token);
|
||||
|
||||
return RagResponse.createSuccessfulWithNewToken(userProfileDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RagResponse<UserProfileDTO> refreshAccessToken(String refreshTokenValue) {
|
||||
RefreshToken refreshToken = refreshTokenService.validateAndRefreshToken(refreshTokenValue);
|
||||
User user = refreshToken.getUser();
|
||||
|
||||
String accessToken = jwtTokenProvider.generateToken(user);
|
||||
|
||||
return RagResponse.createSuccessfulWithNewToken(
|
||||
userMapper.toUserProfileDto(user, accessToken, refreshToken.getToken())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RagResponse<UserProfileDTO> registerUser(@NotNull RegistrationUserRequest request) {
|
||||
|
||||
accessValidator.validateNewUser(
|
||||
request.getUsername(),
|
||||
request.getEmail(),
|
||||
request.getPassword(),
|
||||
request.getConfirmPassword()
|
||||
);
|
||||
|
||||
User newUser = userMapper.fromDto(request);
|
||||
newUser.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
userRepository.save(newUser);
|
||||
|
||||
RefreshToken refreshToken = refreshTokenService.generateOrUpdateRefreshToken(newUser);
|
||||
String token = jwtTokenProvider.generateToken(newUser);
|
||||
UserProfileDTO userProfileDTO = userMapper.toUserProfileDto(newUser, token, refreshToken.getToken());
|
||||
userProfileDTO.setToken(token);
|
||||
|
||||
return RagResponse.createSuccessfulWithNewToken(userProfileDTO);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.balex.rag.service.impl;
|
||||
|
||||
import com.balex.rag.model.entity.Chat;
|
||||
import com.balex.rag.model.entity.ChatEntry;
|
||||
import com.balex.rag.model.enums.Role;
|
||||
import com.balex.rag.repo.ChatEntryRepository;
|
||||
import com.balex.rag.repo.ChatRepository;
|
||||
import com.balex.rag.service.ChatEntryService;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ChatEntryServiceImpl implements ChatEntryService {
|
||||
|
||||
private final ChatEntryRepository chatEntryRepository;
|
||||
private final ChatRepository chatRepository;
|
||||
private final ChatClient chatClient;
|
||||
|
||||
@Override
|
||||
public List<ChatEntry> getEntriesByChatId(Long chatId) {
|
||||
return chatEntryRepository.findByChatIdOrderByCreatedAtAsc(chatId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ChatEntry addUserEntry(Long chatId, String content, boolean onlyContext, int topK, double topP) {
|
||||
Chat chat = chatRepository.findById(chatId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Chat not found with id: " + chatId));
|
||||
|
||||
ChatEntry userEntry = ChatEntry.builder()
|
||||
.chat(chat)
|
||||
.content(content)
|
||||
.role(Role.USER)
|
||||
.build();
|
||||
chatEntryRepository.save(userEntry);
|
||||
|
||||
String systemPrompt = onlyContext
|
||||
? """
|
||||
The question may be about a CONSEQUENCE of a fact from Context.
|
||||
ALWAYS connect: Context fact → question.
|
||||
No connection, even indirect = answer ONLY: "The request is not related to the uploaded context."
|
||||
Connection exists = answer using ONLY the context.
|
||||
Do NOT use any knowledge outside the provided context.
|
||||
"""
|
||||
: """
|
||||
The question may be about a CONSEQUENCE of a fact from Context.
|
||||
ALWAYS connect: Context fact → question.
|
||||
If context contains relevant information, use it in your answer.
|
||||
If context does not contain relevant information, answer using your general knowledge.
|
||||
""";
|
||||
|
||||
String response = chatClient.prompt()
|
||||
.system(systemPrompt)
|
||||
.user(content)
|
||||
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, String.valueOf(chatId)))
|
||||
.options(OllamaOptions.builder()
|
||||
.topK(topK)
|
||||
.topP(topP)
|
||||
.build())
|
||||
.call()
|
||||
.content();
|
||||
|
||||
ChatEntry assistantEntry = ChatEntry.builder()
|
||||
.chat(chat)
|
||||
.content(response)
|
||||
.role(Role.ASSISTANT)
|
||||
.build();
|
||||
|
||||
return chatEntryRepository.save(assistantEntry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.balex.rag.service.impl;
|
||||
|
||||
import com.balex.rag.model.entity.Chat;
|
||||
import com.balex.rag.repo.ChatRepository;
|
||||
import com.balex.rag.service.ChatService;
|
||||
import com.balex.rag.utils.ApiUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ChatServiceImpl implements ChatService {
|
||||
|
||||
private final ChatRepository chatRepo;
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
private final ApiUtils apiUtils;
|
||||
|
||||
public List<Chat> getAllChats() {
|
||||
return chatRepo.findAll(Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
}
|
||||
|
||||
public Chat createNewChat(String title) {
|
||||
Long ownerId = apiUtils.getUserIdFromAuthentication().longValue();
|
||||
Chat chat = Chat.builder()
|
||||
.title(title)
|
||||
.idOwner(ownerId)
|
||||
.build();
|
||||
chatRepo.save(chat);
|
||||
return chat;
|
||||
}
|
||||
|
||||
public Chat getChat(Long chatId) {
|
||||
return chatRepo.findById(chatId).orElseThrow();
|
||||
}
|
||||
|
||||
public void deleteChat(Long chatId) {
|
||||
chatRepo.deleteById(chatId);
|
||||
}
|
||||
|
||||
|
||||
public SseEmitter proceedInteractionWithStreaming(Long chatId, String userPrompt) {
|
||||
|
||||
SseEmitter sseEmitter = new SseEmitter(0L);
|
||||
final StringBuilder answer = new StringBuilder();
|
||||
|
||||
chatClient
|
||||
.prompt(userPrompt)
|
||||
.advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, chatId))
|
||||
.stream()
|
||||
.chatResponse()
|
||||
.subscribe(
|
||||
(ChatResponse response) -> processToken(response, sseEmitter, answer),
|
||||
sseEmitter::completeWithError,
|
||||
sseEmitter::complete);
|
||||
return sseEmitter;
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
private static void processToken(ChatResponse response, SseEmitter emitter, StringBuilder answer) {
|
||||
var token = response.getResult().getOutput();
|
||||
emitter.send(token);
|
||||
answer.append(token.getText());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.balex.rag.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* Helper component to handle transactional file processing.
|
||||
*
|
||||
* This separate bean is necessary because Spring's @Transactional relies on proxies,
|
||||
* and self-invocation within the same class bypasses the proxy, causing transactions
|
||||
* to not be applied.
|
||||
*/
|
||||
@Component
|
||||
public class DocumentTransactionalHelper {
|
||||
|
||||
/**
|
||||
* Processes a single file within a transaction boundary.
|
||||
*
|
||||
* @param file the file to process
|
||||
* @param userId the user ID
|
||||
* @param filename the filename
|
||||
* @param processor the processing function to execute
|
||||
* @return true if file was processed, false if skipped
|
||||
*/
|
||||
@Transactional
|
||||
public boolean processFileInTransaction(MultipartFile file,
|
||||
Long userId,
|
||||
String filename,
|
||||
FileProcessor processor) {
|
||||
return processor.process(file, userId, filename);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface FileProcessor {
|
||||
boolean process(MultipartFile file, Long userId, String filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.balex.rag.service.impl;
|
||||
|
||||
import com.balex.rag.model.constants.ApiErrorMessage;
|
||||
import com.balex.rag.model.entity.RefreshToken;
|
||||
import com.balex.rag.model.entity.User;
|
||||
import com.balex.rag.model.exception.NotFoundException;
|
||||
import com.balex.rag.repo.RefreshTokenRepository;
|
||||
import com.balex.rag.service.RefreshTokenService;
|
||||
import com.balex.rag.utils.ApiUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RefreshTokenServiceImpl implements RefreshTokenService {
|
||||
private final RefreshTokenRepository refreshTokenRepository;
|
||||
|
||||
@Override
|
||||
public RefreshToken generateOrUpdateRefreshToken(User user) {
|
||||
return refreshTokenRepository.findByUserId(user.getId())
|
||||
.map(refreshToken -> {
|
||||
refreshToken.setCreated(LocalDateTime.now());
|
||||
refreshToken.setToken(ApiUtils.generateUuidWithoutDash());
|
||||
return refreshTokenRepository.save(refreshToken);
|
||||
})
|
||||
.orElseGet(() -> {
|
||||
RefreshToken newToken = new RefreshToken();
|
||||
newToken.setUser(user);
|
||||
newToken.setCreated(LocalDateTime.now());
|
||||
newToken.setToken(ApiUtils.generateUuidWithoutDash());
|
||||
return refreshTokenRepository.save(newToken);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefreshToken validateAndRefreshToken(String requestRefreshToken) {
|
||||
RefreshToken refreshToken = refreshTokenRepository.findByToken(requestRefreshToken)
|
||||
.orElseThrow(() -> new NotFoundException(ApiErrorMessage.NOT_FOUND_REFRESH_TOKEN.getMessage()));
|
||||
|
||||
refreshToken.setCreated(LocalDateTime.now());
|
||||
refreshToken.setToken(ApiUtils.generateUuidWithoutDash());
|
||||
return refreshTokenRepository.save(refreshToken);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
package com.balex.rag.service.impl;
|
||||
|
||||
import com.balex.rag.model.LoadedDocument;
|
||||
import com.balex.rag.model.UploadProgress;
|
||||
import com.balex.rag.model.constants.ApiLogMessage;
|
||||
import com.balex.rag.model.exception.UploadException;
|
||||
import com.balex.rag.repo.DocumentRepository;
|
||||
import com.balex.rag.service.UserDocumentService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.reader.TextReader;
|
||||
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
|
||||
import org.springframework.ai.vectorstore.VectorStore;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.retry.annotation.Backoff;
|
||||
import org.springframework.retry.annotation.Retryable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static com.balex.rag.model.constants.ApiConstants.EMPTY_FILENAME;
|
||||
import static com.balex.rag.model.constants.ApiErrorMessage.UPLOADED_FILENAME_EMPTY;
|
||||
import static com.balex.rag.model.constants.ApiErrorMessage.UPLOAD_FILE_READ_ERROR;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserDocumentServiceImpl implements UserDocumentService {
|
||||
|
||||
private final DocumentRepository documentRepository;
|
||||
private final VectorStore vectorStore;
|
||||
private final DocumentTransactionalHelper transactionalHelper;
|
||||
|
||||
private static final String TXT_EXTENSION = "txt";
|
||||
private static final String USER_ID_FIELD_NAME = "user_id";
|
||||
|
||||
private static final String STATUS_PROCESSING = "processing";
|
||||
private static final String STATUS_COMPLETED = "completed";
|
||||
private static final String STATUS_SKIPPED = "skipped";
|
||||
private static final Long SSE_EMITTER_TIMEOUT_IN_MILLIS = 120000L;
|
||||
|
||||
@Value("${app.document.chunk-size:200}")
|
||||
private int chunkSize;
|
||||
|
||||
public SseEmitter processUploadedFilesWithSse(List<MultipartFile> files, Long userId) {
|
||||
SseEmitter emitter = new SseEmitter(SSE_EMITTER_TIMEOUT_IN_MILLIS);
|
||||
|
||||
AtomicBoolean isCompleted = new AtomicBoolean(false);
|
||||
|
||||
emitter.onCompletion(() -> {
|
||||
log.debug("SSE completed");
|
||||
isCompleted.set(true);
|
||||
});
|
||||
emitter.onTimeout(() -> {
|
||||
log.debug("SSE timeout");
|
||||
isCompleted.set(true);
|
||||
});
|
||||
emitter.onError(e -> {
|
||||
log.debug("SSE client disconnected: {}", e.getMessage());
|
||||
isCompleted.set(true);
|
||||
});
|
||||
|
||||
List<MultipartFile> validFiles = files.stream()
|
||||
.filter(f -> !f.isEmpty())
|
||||
.toList();
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
int totalFiles = validFiles.size();
|
||||
int processedCount = 0;
|
||||
|
||||
for (MultipartFile file : validFiles) {
|
||||
if (isCompleted.get()) {
|
||||
log.debug("Upload cancelled, stopping at file: {}", processedCount);
|
||||
return;
|
||||
}
|
||||
|
||||
String filename = getFilename(file);
|
||||
|
||||
sendProgress(emitter, isCompleted, processedCount, totalFiles, filename, STATUS_PROCESSING);
|
||||
|
||||
if (isCompleted.get()) return; // Проверка после отправки
|
||||
|
||||
boolean processed = transactionalHelper.processFileInTransaction(
|
||||
file, userId, filename, this::processFileInternal);
|
||||
|
||||
processedCount++;
|
||||
String status = processed ? STATUS_PROCESSING : STATUS_SKIPPED;
|
||||
sendProgress(emitter, isCompleted, processedCount, totalFiles, filename, status);
|
||||
}
|
||||
|
||||
if (!isCompleted.get()) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event()
|
||||
.data(UploadProgress.builder()
|
||||
.percent(100)
|
||||
.processedFiles(processedCount)
|
||||
.totalFiles(totalFiles)
|
||||
.currentFile("")
|
||||
.status(STATUS_COMPLETED)
|
||||
.build()));
|
||||
emitter.complete();
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
log.debug("Could not send completion: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
if (!isCompleted.get()) {
|
||||
log.error("SSE processing error", e);
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private void sendProgress(SseEmitter emitter, AtomicBoolean isCompleted,
|
||||
int processed, int total, String filename, String status) {
|
||||
if (isCompleted.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int percent = total > 0 ? (int) Math.round((double) processed / total * 100) : 0;
|
||||
|
||||
emitter.send(SseEmitter.event()
|
||||
.data(UploadProgress.builder()
|
||||
.percent(percent)
|
||||
.processedFiles(processed)
|
||||
.totalFiles(total)
|
||||
.currentFile(filename)
|
||||
.status(status)
|
||||
.build()));
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
// Client disconnected - this is normal for cancel
|
||||
log.debug("Client disconnected: {}", e.getMessage());
|
||||
isCompleted.set(true);
|
||||
}
|
||||
}
|
||||
boolean processFileInternal(MultipartFile file, Long userId, String filename) {
|
||||
byte[] content;
|
||||
try {
|
||||
content = file.getBytes();
|
||||
} catch (IOException e) {
|
||||
throw new UploadException(UPLOAD_FILE_READ_ERROR + filename, e);
|
||||
}
|
||||
|
||||
String contentHash = computeSha256Hash(content);
|
||||
|
||||
if (documentRepository.existsByFilenameAndContentHash(filename, contentHash)) {
|
||||
log.debug("Skipping duplicate file: {} with hash: {}", filename, contentHash);
|
||||
return false;
|
||||
}
|
||||
|
||||
processTextAndStore(userId, filename, content, contentHash);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void processTextAndStore(Long userId, String filename, byte[] content, String contentHash) {
|
||||
Resource resource = new ByteArrayResource(content) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
};
|
||||
|
||||
List<Document> docs = new TextReader(resource).get();
|
||||
|
||||
TokenTextSplitter splitter = TokenTextSplitter.builder()
|
||||
.withChunkSize(chunkSize)
|
||||
.build();
|
||||
List<Document> chunks = splitter.apply(docs);
|
||||
|
||||
for (Document chunk : chunks) {
|
||||
chunk.getMetadata().put(USER_ID_FIELD_NAME, userId);
|
||||
}
|
||||
|
||||
storeInVectorDb(chunks);
|
||||
|
||||
LoadedDocument loaded = LoadedDocument.builder()
|
||||
.documentType(getExtensionOrTxt(filename))
|
||||
.chunkCount(chunks.size())
|
||||
.filename(filename)
|
||||
.contentHash(contentHash)
|
||||
.userId(userId)
|
||||
.build();
|
||||
|
||||
documentRepository.save(loaded);
|
||||
|
||||
log.info("Successfully processed file: {} with {} chunks for user: {}",
|
||||
filename, chunks.size(), userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores documents in vector store with retry via Spring Retry.
|
||||
*/
|
||||
@Retryable(
|
||||
retryFor = RuntimeException.class,
|
||||
maxAttemptsExpression = "${app.document.retry.max-attempts:3}",
|
||||
backoff = @Backoff(
|
||||
delayExpression = "${app.document.retry.delay-ms:1500}",
|
||||
multiplier = 2
|
||||
)
|
||||
)
|
||||
public void storeInVectorDb(List<Document> chunks) {
|
||||
vectorStore.accept(chunks);
|
||||
}
|
||||
|
||||
private String getFilename(MultipartFile file) {
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null || filename.isBlank()) {
|
||||
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), UPLOADED_FILENAME_EMPTY);
|
||||
return EMPTY_FILENAME;
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
private String getExtensionOrTxt(String filename) {
|
||||
int idx = filename.lastIndexOf('.');
|
||||
if (idx == -1 || idx == filename.length() - 1) {
|
||||
return TXT_EXTENSION;
|
||||
}
|
||||
return filename.substring(idx + 1).toLowerCase();
|
||||
}
|
||||
|
||||
private String computeSha256Hash(byte[] content) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(content);
|
||||
return HexFormat.of().formatHex(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 algorithm not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package com.balex.rag.service.impl;
|
||||
|
||||
import com.balex.rag.mapper.UserMapper;
|
||||
import com.balex.rag.model.LoadedDocument;
|
||||
import com.balex.rag.model.constants.ApiErrorMessage;
|
||||
import com.balex.rag.model.dto.UserDTO;
|
||||
import com.balex.rag.model.dto.UserSearchDTO;
|
||||
import com.balex.rag.model.entity.LoadedDocumentInfo;
|
||||
import com.balex.rag.model.entity.User;
|
||||
import com.balex.rag.model.entity.UserInfo;
|
||||
import com.balex.rag.model.exception.InvalidTokenException;
|
||||
import com.balex.rag.model.exception.NotFoundException;
|
||||
import com.balex.rag.model.request.user.NewUserRequest;
|
||||
import com.balex.rag.model.request.user.UpdateUserRequest;
|
||||
import com.balex.rag.model.response.PaginationResponse;
|
||||
import com.balex.rag.model.response.RagResponse;
|
||||
import com.balex.rag.repo.DocumentRepository;
|
||||
import com.balex.rag.repo.UserRepository;
|
||||
import com.balex.rag.repo.VectorStoreRepository;
|
||||
import com.balex.rag.security.JwtTokenProvider;
|
||||
import com.balex.rag.security.validation.AccessValidator;
|
||||
import com.balex.rag.service.UserService;
|
||||
import com.balex.rag.service.model.exception.DataExistException;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.balex.rag.model.constants.ApiConstants.USER_ROLE;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserServiceImpl implements UserService {
|
||||
private final UserRepository userRepository;
|
||||
private final UserMapper userMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final AccessValidator accessValidator;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final DocumentRepository documentRepository;
|
||||
private final VectorStoreRepository vectorStoreRepository;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public RagResponse<UserDTO> getById(@NotNull Integer userId) {
|
||||
User user = userRepository.findByIdAndDeletedFalse(userId)
|
||||
.orElseThrow(() -> new NotFoundException(ApiErrorMessage.USER_NOT_FOUND_BY_ID.getMessage(userId)));
|
||||
|
||||
return RagResponse.createSuccessful(userMapper.toDto(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public RagResponse<UserDTO> createUser(@NotNull NewUserRequest request) {
|
||||
if (userRepository.existsByEmail(request.getEmail())) {
|
||||
throw new DataExistException(ApiErrorMessage.EMAIL_ALREADY_EXISTS.getMessage(request.getEmail()));
|
||||
}
|
||||
|
||||
if (userRepository.existsByUsername(request.getUsername())) {
|
||||
throw new DataExistException(ApiErrorMessage.USERNAME_ALREADY_EXISTS.getMessage(request.getUsername()));
|
||||
}
|
||||
|
||||
User user = userMapper.createUser(request);
|
||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
User savedUser = userRepository.save(user);
|
||||
|
||||
return RagResponse.createSuccessful(userMapper.toDto(savedUser));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public RagResponse<UserDTO> updateUser(@NotNull Integer userId, UpdateUserRequest request) {
|
||||
User user = userRepository.findByIdAndDeletedFalse(userId)
|
||||
.orElseThrow(() -> new NotFoundException(ApiErrorMessage.USER_NOT_FOUND_BY_ID.getMessage(userId)));
|
||||
|
||||
if (!user.getUsername().equals(request.getUsername()) && userRepository.existsByUsername(request.getUsername())) {
|
||||
throw new DataExistException(ApiErrorMessage.USERNAME_ALREADY_EXISTS.getMessage(request.getUsername()));
|
||||
}
|
||||
|
||||
if (!user.getEmail().equals(request.getEmail()) && userRepository.existsByEmail(request.getEmail())) {
|
||||
throw new DataExistException(ApiErrorMessage.EMAIL_ALREADY_EXISTS.getMessage(request.getEmail()));
|
||||
}
|
||||
|
||||
userMapper.updateUser(user, request);
|
||||
user = userRepository.save(user);
|
||||
|
||||
return RagResponse.createSuccessful(userMapper.toDto(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void softDeleteUser(Integer userId) {
|
||||
User user = userRepository.findByIdAndDeletedFalse(userId)
|
||||
.orElseThrow(() -> new NotFoundException(ApiErrorMessage.USER_NOT_FOUND_BY_ID.getMessage(userId)));
|
||||
|
||||
accessValidator.validateOwnerAccess(userId);
|
||||
|
||||
user.setDeleted(true);
|
||||
userRepository.save(user);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public RagResponse<PaginationResponse<UserSearchDTO>> findAllUsers(Pageable pageable) {
|
||||
Page<UserSearchDTO> users = userRepository.findAll(pageable)
|
||||
.map(userMapper::toUserSearchDto);
|
||||
|
||||
PaginationResponse<UserSearchDTO> paginationResponse = new PaginationResponse<>(
|
||||
users.getContent(),
|
||||
new PaginationResponse.Pagination(
|
||||
users.getTotalElements(),
|
||||
pageable.getPageSize(),
|
||||
users.getNumber() + 1,
|
||||
users.getTotalPages()
|
||||
)
|
||||
);
|
||||
|
||||
return RagResponse.createSuccessful(paginationResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
|
||||
return getUserDetails(email, userRepository);
|
||||
}
|
||||
|
||||
static UserDetails getUserDetails(String email, UserRepository userRepository) {
|
||||
User user = userRepository.findByEmail(email)
|
||||
.orElseThrow(() -> new NotFoundException(ApiErrorMessage.EMAIL_NOT_FOUND.getMessage(email)));
|
||||
|
||||
user.setLastLogin(LocalDateTime.now());
|
||||
userRepository.save(user);
|
||||
return new org.springframework.security.core.userdetails.User(
|
||||
user.getEmail(),
|
||||
user.getPassword(),
|
||||
Collections.singletonList(new SimpleGrantedAuthority(USER_ROLE))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public RagResponse<UserInfo> getUserInfo(String token) {
|
||||
User user = getUserInfoFromToken(token);
|
||||
|
||||
List<LoadedDocumentInfo> loadedFiles = documentRepository
|
||||
.findByUserId(user.getId())
|
||||
.stream()
|
||||
.map(doc -> new LoadedDocumentInfo(doc.getId(), doc.getFilename()))
|
||||
.toList();
|
||||
|
||||
UserInfo userInfo = new UserInfo(user.getId(),
|
||||
user.getUsername(),
|
||||
user.getEmail(),
|
||||
loadedFiles);
|
||||
|
||||
return RagResponse.createSuccessful(userInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public RagResponse<Integer> deleteUserDocuments(String token) {
|
||||
getUserInfoFromToken(token);
|
||||
User user = getUserInfoFromToken(token);
|
||||
|
||||
List<LoadedDocument> documents = documentRepository.findByUserId(user.getId());
|
||||
|
||||
if (documents.isEmpty()) {
|
||||
return RagResponse.createSuccessful(0);
|
||||
}
|
||||
|
||||
// Удаляем чанки по user_id
|
||||
vectorStoreRepository.deleteByUserId(user.getId().longValue());
|
||||
|
||||
// Удаляем записи из loaded_document
|
||||
documentRepository.deleteAll(documents);
|
||||
|
||||
return RagResponse.createSuccessful(documents.size());
|
||||
}
|
||||
|
||||
private User getUserInfoFromToken(String token) {
|
||||
if (token == null || token.isBlank()) {
|
||||
throw new InvalidTokenException("Token is empty or null");
|
||||
}
|
||||
|
||||
String cleanToken = token.startsWith("Bearer ")
|
||||
? token.substring(7)
|
||||
: token;
|
||||
|
||||
String username = jwtTokenProvider.getUsername(cleanToken);
|
||||
|
||||
return userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new InvalidTokenException("User not found: " + username));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.balex.rag.service.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class AuthenticationConstants {
|
||||
|
||||
public static final String USER_ID = "userId";
|
||||
public static final String USERNAME = "username";
|
||||
public static final String USER_EMAIL = "email";
|
||||
public static final String USER_REGISTRATION_STATUS = "userRegistrationStatus";
|
||||
public static final String LAST_UPDATE = "lastUpdate";
|
||||
public static final String ACCESS_KEY_HEADER_NAME = "key";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.balex.rag.service.model.exception;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class DataExistException extends RuntimeException {
|
||||
|
||||
public DataExistException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
53
rag-service/src/main/java/com/balex/rag/utils/ApiUtils.java
Normal file
53
rag-service/src/main/java/com/balex/rag/utils/ApiUtils.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.balex.rag.utils;
|
||||
|
||||
import com.balex.rag.model.constants.ApiConstants;
|
||||
import com.balex.rag.security.JwtTokenProvider;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ApiUtils {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
|
||||
public static String getMethodName() {
|
||||
try {
|
||||
return new Throwable().getStackTrace()[1].getMethodName();
|
||||
} catch (Exception cause) {
|
||||
return ApiConstants.UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
public static Cookie createAuthCookie(String value) {
|
||||
Cookie authorizationCookie = new Cookie(HttpHeaders.AUTHORIZATION, value);
|
||||
authorizationCookie.setHttpOnly(true);
|
||||
authorizationCookie.setSecure(true);
|
||||
authorizationCookie.setPath("/");
|
||||
authorizationCookie.setMaxAge(300);
|
||||
return authorizationCookie;
|
||||
}
|
||||
|
||||
public static String generateUuidWithoutDash() {
|
||||
return UUID.randomUUID().toString().replace(ApiConstants.DASH, StringUtils.EMPTY);
|
||||
}
|
||||
|
||||
|
||||
public static String getCurrentUsername() {
|
||||
return SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
}
|
||||
|
||||
public Integer getUserIdFromAuthentication() {
|
||||
String jwtToken = SecurityContextHolder.getContext().getAuthentication().getCredentials().toString();
|
||||
return Integer.parseInt(jwtTokenProvider.getUserId(jwtToken));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.balex.rag.utils;
|
||||
|
||||
import com.balex.rag.security.validation.PasswordMatchesValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Constraint(validatedBy = PasswordMatchesValidator.class)
|
||||
@Target({ ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PasswordMatches {
|
||||
String message() default "Passwords do not match";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.balex.rag.utils;
|
||||
|
||||
import com.balex.rag.model.constants.ApiConstants;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Random;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class PasswordUtils {
|
||||
private static final Random RND = new Random();
|
||||
|
||||
public static boolean isNotValidPassword(String password) {
|
||||
if (password == null || password.isEmpty() || password.trim().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
String trim = password.trim();
|
||||
if (trim.length() < ApiConstants.REQUIRED_MIN_PASSWORD_LENGTH) {
|
||||
return true;
|
||||
}
|
||||
int charactersNumber = ApiConstants.REQUIRED_MIN_CHARACTERS_NUMBER_IN_PASSWORD;
|
||||
int lettersUCaseNumber = ApiConstants.REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD;
|
||||
int lettersLCaseNumber = ApiConstants.REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD;
|
||||
int digitsNumber = ApiConstants.REQUIRED_MIN_DIGITS_NUMBER_IN_PASSWORD;
|
||||
for (int i = 0; i < trim.length(); i++) {
|
||||
String currentLetter = String.valueOf(trim.charAt(i));
|
||||
if (!ApiConstants.PASSWORD_ALL_CHARACTERS.contains(currentLetter)) {
|
||||
return true;
|
||||
}
|
||||
charactersNumber -= ApiConstants.PASSWORD_CHARACTERS.contains(currentLetter) ? 1 : 0;
|
||||
lettersUCaseNumber -= ApiConstants.PASSWORD_LETTERS_UPPER_CASE.contains(currentLetter) ? 1 : 0;
|
||||
lettersLCaseNumber -= ApiConstants.PASSWORD_LETTERS_LOWER_CASE.contains(currentLetter) ? 1 : 0;
|
||||
digitsNumber -= ApiConstants.PASSWORD_DIGITS.contains(currentLetter) ? 1 : 0;
|
||||
}
|
||||
return ((charactersNumber > 0) || (lettersUCaseNumber > 0) || (lettersLCaseNumber > 0) || (digitsNumber > 0));
|
||||
}
|
||||
|
||||
private static String randomFromChars(int count, String chars) {
|
||||
final SecureRandom RANDOM = new SecureRandom();
|
||||
StringBuilder sb = new StringBuilder(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
int idx = RANDOM.nextInt(chars.length());
|
||||
sb.append(chars.charAt(idx));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String generatePassword() {
|
||||
int charactersNumber = ApiConstants.REQUIRED_MIN_CHARACTERS_NUMBER_IN_PASSWORD;
|
||||
int digitsNumber = ApiConstants.REQUIRED_MIN_DIGITS_NUMBER_IN_PASSWORD;
|
||||
int lettersUCaseNumber = ApiConstants.REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD;
|
||||
int lettersLCaseNumber = ApiConstants.REQUIRED_MIN_PASSWORD_LENGTH
|
||||
- charactersNumber - digitsNumber - lettersUCaseNumber;
|
||||
// String characters = RandomStringUtils.random(charactersNumber, ApiConstants.PASSWORD_CHARACTERS);
|
||||
// String digits = RandomStringUtils.random(digitsNumber, ApiConstants.PASSWORD_DIGITS);
|
||||
// String lettersUCase = RandomStringUtils.random(lettersUCaseNumber, ApiConstants.PASSWORD_LETTERS_UPPER_CASE);
|
||||
// String lettersLCase = RandomStringUtils.random(lettersLCaseNumber, ApiConstants.PASSWORD_LETTERS_LOWER_CASE);
|
||||
String characters = randomFromChars(charactersNumber, ApiConstants.PASSWORD_CHARACTERS);
|
||||
String digits = randomFromChars(digitsNumber, ApiConstants.PASSWORD_DIGITS);
|
||||
String lettersUCase = randomFromChars(lettersUCaseNumber, ApiConstants.PASSWORD_LETTERS_UPPER_CASE);
|
||||
String lettersLCase = randomFromChars(lettersLCaseNumber, ApiConstants.PASSWORD_LETTERS_LOWER_CASE);
|
||||
|
||||
|
||||
ArrayList<Character> randomPasswordCharacters = new ArrayList<>();
|
||||
for (char character : (characters + digits + lettersUCase + lettersLCase).toCharArray()) {
|
||||
randomPasswordCharacters.add(character);
|
||||
}
|
||||
|
||||
StringBuilder password = new StringBuilder();
|
||||
int length = randomPasswordCharacters.size();
|
||||
for (int i = 0; i < length; i++) {
|
||||
int randomPosition = RND.nextInt((randomPasswordCharacters.size()));
|
||||
password.append(randomPasswordCharacters.get(randomPosition));
|
||||
randomPasswordCharacters.remove(randomPosition);
|
||||
}
|
||||
|
||||
return password.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
37
rag-service/src/main/resources/application.properties
Normal file
37
rag-service/src/main/resources/application.properties
Normal file
@@ -0,0 +1,37 @@
|
||||
spring.application.name=rag
|
||||
spring.ai.ollama.base-url=http://localhost:11431
|
||||
#spring.ai.ollama.chat.model=gemma3:4b-it-q4_K_M
|
||||
spring.ai.ollama.chat.model=llama3.1:8b-instruct-q4_K_M
|
||||
jwt.secret=ywfI6dBznYmHbokihB/OBzZz6E0Fj+6PiqrM8dQ5c3t0HeYarblCbOGM8vQtOt472AtQ+MsCH7OVIKHOzjrPsQ==
|
||||
jwt.expiration=103600000
|
||||
spring.datasource.url=jdbc:postgresql://localhost:5432/ragdb
|
||||
spring.datasource.username=postgres
|
||||
spring.datasource.password=postgres
|
||||
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
|
||||
logging.level.org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping=DEBUG
|
||||
logging.level.org.springframework.web=DEBUG
|
||||
logging.level.org.flywaydb=DEBUG
|
||||
logging.level.com.balex.rag.controller=DEBUG
|
||||
app.document.chunk-size=200
|
||||
#spring.main.allow-circular-references=true
|
||||
server.compression.enabled=false
|
||||
server.tomcat.connection-timeout=60000
|
||||
spring.mvc.async.request-timeout=60000
|
||||
#spring.main.web-application-type=reactive
|
||||
end.points.users=/users
|
||||
end.points.id=/{id}
|
||||
end.points.all=/all
|
||||
end.points.create=/create
|
||||
end.points.userinfo=/userinfo
|
||||
end.points.refresh.token=/refresh/token
|
||||
end.points.auth=/auth
|
||||
end.points.login=/login
|
||||
end.points.register=/register
|
||||
end.points.chat=/chat
|
||||
end.points.entry=/entry
|
||||
end.points.document=/documents
|
||||
rag.rerank-fetch-multiplier = 2
|
||||
#Swagger
|
||||
swagger.servers.first=http://localhost:8080
|
||||
springdoc.swagger-ui.path=/swagger-ui.html
|
||||
springdoc.api-docs.path=/v3/api-docs
|
||||
Reference in New Issue
Block a user