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;
+ }
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/model/response/RagResponse.java b/rag-service/src/main/java/com/balex/rag/model/response/RagResponse.java
new file mode 100644
index 0000000..148d6b9
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/model/response/RagResponse.java
@@ -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 implements Serializable {
+ private String message;
+ private P payload;
+ private boolean success;
+
+ public static
RagResponse
createSuccessful(P payload) {
+ return new RagResponse<>(StringUtils.EMPTY, payload, true);
+ }
+
+ public static
RagResponse
createSuccessfulWithNewToken(P payload) {
+ return new RagResponse<>(ApiMessage.TOKEN_CREATED_OR_UPDATED.getMessage(), payload, true);
+ }
+
+}
+
+
diff --git a/rag-service/src/main/java/com/balex/rag/repo/ChatEntryRepository.java b/rag-service/src/main/java/com/balex/rag/repo/ChatEntryRepository.java
new file mode 100644
index 0000000..96cb33b
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/repo/ChatEntryRepository.java
@@ -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 {
+
+ List findByChatIdOrderByCreatedAtAsc(Long chatId);
+}
diff --git a/rag-service/src/main/java/com/balex/rag/repo/ChatRepository.java b/rag-service/src/main/java/com/balex/rag/repo/ChatRepository.java
new file mode 100644
index 0000000..78adbcf
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/repo/ChatRepository.java
@@ -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 {
+
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/repo/DocumentRepository.java b/rag-service/src/main/java/com/balex/rag/repo/DocumentRepository.java
new file mode 100644
index 0000000..a7ea5fd
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/repo/DocumentRepository.java
@@ -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 {
+
+ boolean existsByFilenameAndContentHash(String filename, String contentHash);
+
+ List findByUserId(Integer userId);
+
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/repo/RefreshTokenRepository.java b/rag-service/src/main/java/com/balex/rag/repo/RefreshTokenRepository.java
new file mode 100644
index 0000000..9ba5031
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/repo/RefreshTokenRepository.java
@@ -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 {
+
+ Optional findByToken(String token);
+
+ Optional findByUserId(Integer userId);
+
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/repo/UserRepository.java b/rag-service/src/main/java/com/balex/rag/repo/UserRepository.java
new file mode 100644
index 0000000..3db7437
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/repo/UserRepository.java
@@ -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, JpaSpecificationExecutor {
+
+ boolean existsByEmail(String email);
+
+ boolean existsByUsername(String username);
+
+ Optional findByIdAndDeletedFalse (Integer id);
+
+ Optional findUserByEmailAndDeletedFalse(String email);
+
+ Optional findByEmail(String email);
+
+ Optional findByUsername(String username);
+
+}
+
+
diff --git a/rag-service/src/main/java/com/balex/rag/repo/VectorStoreRepository.java b/rag-service/src/main/java/com/balex/rag/repo/VectorStoreRepository.java
new file mode 100644
index 0000000..89d4c37
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/repo/VectorStoreRepository.java
@@ -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 sources);
+
+ void deleteByUserId(Long userId);
+}
\ No newline at end of file
diff --git a/rag-service/src/main/java/com/balex/rag/repo/impl/VectorStoreRepositoryImpl.java b/rag-service/src/main/java/com/balex/rag/repo/impl/VectorStoreRepositoryImpl.java
new file mode 100644
index 0000000..48c8826
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/repo/impl/VectorStoreRepositoryImpl.java
@@ -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 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);
+ }
+}
\ No newline at end of file
diff --git a/rag-service/src/main/java/com/balex/rag/security/JwtTokenProvider.java b/rag-service/src/main/java/com/balex/rag/security/JwtTokenProvider.java
new file mode 100644
index 0000000..8d1c2c4
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/security/JwtTokenProvider.java
@@ -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 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 = 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 claims, String subject) {
+ return Jwts.builder()
+ .setClaims(claims)
+ .setSubject(subject)
+ .setIssuedAt(new Date())
+ .setExpiration(new Date(System.currentTimeMillis() + jwtValidityInMilliseconds))
+ .signWith(secretKey, SignatureAlgorithm.HS512)
+ .compact();
+ }
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/security/filter/JwtRequestFilter.java b/rag-service/src/main/java/com/balex/rag/security/filter/JwtRequestFilter.java
new file mode 100644
index 0000000..2d8b5b7
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/security/filter/JwtRequestFilter.java
@@ -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 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 emailOpt = Optional.ofNullable(jwtTokenProvider.getUsername(jwt));
+ Optional userIdOpt = Optional.ofNullable(jwtTokenProvider.getUserId(jwt));
+
+ if (emailOpt.isPresent() && userIdOpt.isPresent()) {
+ if (SecurityContextHolder.getContext().getAuthentication() == null) {
+ List 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);
+ }
+}
+
+
diff --git a/rag-service/src/main/java/com/balex/rag/security/handler/AccessRestrictionHandler.java b/rag-service/src/main/java/com/balex/rag/security/handler/AccessRestrictionHandler.java
new file mode 100644
index 0000000..4bfeb02
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/security/handler/AccessRestrictionHandler.java
@@ -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());
+ }
+
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/security/validation/AccessValidator.java b/rag-service/src/main/java/com/balex/rag/security/validation/AccessValidator.java
new file mode 100644
index 0000000..90b692d
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/security/validation/AccessValidator.java
@@ -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());
+ }
+ }
+
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/security/validation/PasswordMatchesValidator.java b/rag-service/src/main/java/com/balex/rag/security/validation/PasswordMatchesValidator.java
new file mode 100644
index 0000000..7b58c2c
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/security/validation/PasswordMatchesValidator.java
@@ -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 {
+
+ @Override
+ public boolean isValid(RegistrationUserRequest request, ConstraintValidatorContext constraintValidatorContext) {
+ return request.getPassword().equals(request.getConfirmPassword());
+ }
+
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/service/AuthService.java b/rag-service/src/main/java/com/balex/rag/service/AuthService.java
new file mode 100644
index 0000000..bb88578
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/AuthService.java
@@ -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 login(LoginRequest request);
+
+ RagResponse refreshAccessToken(String refreshToken);
+
+ RagResponse registerUser(RegistrationUserRequest request);
+
+}
diff --git a/rag-service/src/main/java/com/balex/rag/service/ChatEntryService.java b/rag-service/src/main/java/com/balex/rag/service/ChatEntryService.java
new file mode 100644
index 0000000..4d77213
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/ChatEntryService.java
@@ -0,0 +1,12 @@
+package com.balex.rag.service;
+
+import com.balex.rag.model.entity.ChatEntry;
+
+import java.util.List;
+
+public interface ChatEntryService {
+
+ List getEntriesByChatId(Long chatId);
+
+ ChatEntry addUserEntry(Long chatId, String content, boolean onlyContext, int topK, double topP);
+}
\ No newline at end of file
diff --git a/rag-service/src/main/java/com/balex/rag/service/ChatService.java b/rag-service/src/main/java/com/balex/rag/service/ChatService.java
new file mode 100644
index 0000000..bc7e48d
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/ChatService.java
@@ -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 getAllChats();
+
+ Chat getChat(Long chatId);
+
+ void deleteChat(Long chatId);
+
+ SseEmitter proceedInteractionWithStreaming(Long chatId, String userPrompt);
+
+}
diff --git a/rag-service/src/main/java/com/balex/rag/service/PostgresChatMemory.java b/rag-service/src/main/java/com/balex/rag/service/PostgresChatMemory.java
new file mode 100644
index 0000000..3076d1b
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/PostgresChatMemory.java
@@ -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 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 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
+ }
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/service/RefreshTokenService.java b/rag-service/src/main/java/com/balex/rag/service/RefreshTokenService.java
new file mode 100644
index 0000000..1e7b71c
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/RefreshTokenService.java
@@ -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);
+
+}
diff --git a/rag-service/src/main/java/com/balex/rag/service/UserDocumentService.java b/rag-service/src/main/java/com/balex/rag/service/UserDocumentService.java
new file mode 100644
index 0000000..a789393
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/UserDocumentService.java
@@ -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 files, Long userId);
+}
\ No newline at end of file
diff --git a/rag-service/src/main/java/com/balex/rag/service/UserService.java b/rag-service/src/main/java/com/balex/rag/service/UserService.java
new file mode 100644
index 0000000..cd2ab88
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/UserService.java
@@ -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 getById(@NotNull Integer userId);
+
+ RagResponse createUser(@NotNull NewUserRequest request);
+
+ RagResponse updateUser(@NotNull Integer postId, @NotNull UpdateUserRequest request);
+
+ void softDeleteUser(Integer userId);
+
+ RagResponse> findAllUsers(Pageable pageable);
+
+ RagResponse getUserInfo(String token);
+
+ RagResponse deleteUserDocuments(String token);
+
+}
+
+
+
diff --git a/rag-service/src/main/java/com/balex/rag/service/autostart/DocumentLoaderService.java b/rag-service/src/main/java/com/balex/rag/service/autostart/DocumentLoaderService.java
new file mode 100644
index 0000000..b779d7a
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/autostart/DocumentLoaderService.java
@@ -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 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 docs = new TextReader(resource).get();
+// TokenTextSplitter splitter = TokenTextSplitter.builder().withChunkSize(200).build();
+// List 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 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();
+// }
+//}
diff --git a/rag-service/src/main/java/com/balex/rag/service/impl/AuthServiceImpl.java b/rag-service/src/main/java/com/balex/rag/service/impl/AuthServiceImpl.java
new file mode 100644
index 0000000..baf93df
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/impl/AuthServiceImpl.java
@@ -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 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 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 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);
+ }
+
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/service/impl/ChatEntryServiceImpl.java b/rag-service/src/main/java/com/balex/rag/service/impl/ChatEntryServiceImpl.java
new file mode 100644
index 0000000..4f2ebaf
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/impl/ChatEntryServiceImpl.java
@@ -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 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);
+ }
+}
\ No newline at end of file
diff --git a/rag-service/src/main/java/com/balex/rag/service/impl/ChatServiceImpl.java b/rag-service/src/main/java/com/balex/rag/service/impl/ChatServiceImpl.java
new file mode 100644
index 0000000..6e323e9
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/impl/ChatServiceImpl.java
@@ -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 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());
+ }
+
+}
diff --git a/rag-service/src/main/java/com/balex/rag/service/impl/DocumentTransactionalHelper.java b/rag-service/src/main/java/com/balex/rag/service/impl/DocumentTransactionalHelper.java
new file mode 100644
index 0000000..af3523d
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/impl/DocumentTransactionalHelper.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/rag-service/src/main/java/com/balex/rag/service/impl/RefreshTokenServiceImpl.java b/rag-service/src/main/java/com/balex/rag/service/impl/RefreshTokenServiceImpl.java
new file mode 100644
index 0000000..3cd536b
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/impl/RefreshTokenServiceImpl.java
@@ -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);
+ }
+
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/service/impl/UserDocumentServiceImpl.java b/rag-service/src/main/java/com/balex/rag/service/impl/UserDocumentServiceImpl.java
new file mode 100644
index 0000000..7a8cc78
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/impl/UserDocumentServiceImpl.java
@@ -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 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 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 docs = new TextReader(resource).get();
+
+ TokenTextSplitter splitter = TokenTextSplitter.builder()
+ .withChunkSize(chunkSize)
+ .build();
+ List 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 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);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/rag-service/src/main/java/com/balex/rag/service/impl/UserServiceImpl.java b/rag-service/src/main/java/com/balex/rag/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..24aed75
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/impl/UserServiceImpl.java
@@ -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 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 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 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> findAllUsers(Pageable pageable) {
+ Page users = userRepository.findAll(pageable)
+ .map(userMapper::toUserSearchDto);
+
+ PaginationResponse 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 getUserInfo(String token) {
+ User user = getUserInfoFromToken(token);
+
+ List 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 deleteUserDocuments(String token) {
+ getUserInfoFromToken(token);
+ User user = getUserInfoFromToken(token);
+
+ List 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));
+ }
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/service/model/AuthenticationConstants.java b/rag-service/src/main/java/com/balex/rag/service/model/AuthenticationConstants.java
new file mode 100644
index 0000000..c6bafcd
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/model/AuthenticationConstants.java
@@ -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";
+
+}
diff --git a/rag-service/src/main/java/com/balex/rag/service/model/exception/DataExistException.java b/rag-service/src/main/java/com/balex/rag/service/model/exception/DataExistException.java
new file mode 100644
index 0000000..c3fd09c
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/service/model/exception/DataExistException.java
@@ -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);
+ }
+}
+
+
diff --git a/rag-service/src/main/java/com/balex/rag/utils/ApiUtils.java b/rag-service/src/main/java/com/balex/rag/utils/ApiUtils.java
new file mode 100644
index 0000000..a6ef40b
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/utils/ApiUtils.java
@@ -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));
+ }
+
+}
+
diff --git a/rag-service/src/main/java/com/balex/rag/utils/PasswordMatches.java b/rag-service/src/main/java/com/balex/rag/utils/PasswordMatches.java
new file mode 100644
index 0000000..1f1ae9e
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/utils/PasswordMatches.java
@@ -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 {};
+}
+
+
diff --git a/rag-service/src/main/java/com/balex/rag/utils/PasswordUtils.java b/rag-service/src/main/java/com/balex/rag/utils/PasswordUtils.java
new file mode 100644
index 0000000..ebc73df
--- /dev/null
+++ b/rag-service/src/main/java/com/balex/rag/utils/PasswordUtils.java
@@ -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 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();
+ }
+
+}
+
diff --git a/rag-service/src/main/resources/application.properties b/rag-service/src/main/resources/application.properties
new file mode 100644
index 0000000..217b255
--- /dev/null
+++ b/rag-service/src/main/resources/application.properties
@@ -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
\ No newline at end of file