diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 5b3c744..f1577c2 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -16,7 +16,8 @@ public class HandlerConfig { LoginRequestHandler login, FileUploadHandler upload, FileEntityHandler file, - LogoutRequestHandler logoutHandler + LogoutRequestHandler logoutHandler, + AuthRequestHandler authRequestHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -24,6 +25,7 @@ public class HandlerConfig { map.put(20004, upload); map.put(20005, file); map.put(20006, logoutHandler); + map.put(20007, authRequestHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index dbbe9df..9e5d257 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -19,12 +19,11 @@ import ru.soune.nocopy.dto.file.ChunkUploadResponse; import ru.soune.nocopy.dto.file.CompleteUploadResponse; import ru.soune.nocopy.dto.file.FileEntityResponse; import ru.soune.nocopy.dto.file.UploadProgress; -import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.exception.*; import ru.soune.nocopy.handler.*; -import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileUploadService; @@ -48,7 +47,7 @@ public class ApiController { private final FileEntityService fileEntityService; - private final AuthTokenRepository authTokenRepository; + private final AuthService authService; @PostMapping("/v{version}/data") public ResponseEntity handlePostRequest(@RequestBody BaseRequest request, @@ -71,6 +70,9 @@ public class ApiController { return ResponseEntity.ok().body(response); } catch (ValidationException e) { return createValidationErrorResponse(e.getBindingResult(), e.getMsgId()); + } catch (NotFoundAuthToken e) { + return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), new HashMap<>())); } catch (NotValidFieldException e) { throw e; } catch (Exception e) { @@ -257,7 +259,7 @@ public class ApiController { errorData)); } - Long userId = getUserIdFromToken(tokenHeader); + Long userId = authService.useUserAuthToken(tokenHeader); FileEntityResponse entityResponse = fileEntityService.getById(fileId, version); if (!entityResponse.getUserId().equals(userId)) { @@ -379,11 +381,4 @@ public class ApiController { mimeType.startsWith("video") || mimeType.startsWith("audio"); } - - private Long getUserIdFromToken(String tokenHeader) { - String token = tokenHeader.replace("Bearer ", ""); - AuthToken authToken = authTokenRepository.findByToken(token) - .orElseThrow(() -> new NotFoundAuthToken("Token not found")); - return authToken.getUser().getId(); - } } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index b945f64..36d8d9f 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -18,6 +18,7 @@ public enum MessageCode { AUTH_EMAIL_NOT_FOUND(4, "Email not found"), AUTH_EMAIL_OR_TOKEN_NOT_FOUND(4, "Email or Token not found "), AUTH_TOKEN_MISMATCH(4, "Token mismatch"), + AUTH_TOKEN_NOT_FOUND(4, "Token not found"), FILE_NOT_FOUND(4, "File not found"), AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match"); diff --git a/src/main/java/ru/soune/nocopy/dto/TokenProcessRequest.java b/src/main/java/ru/soune/nocopy/dto/TokenProcessRequest.java new file mode 100644 index 0000000..646f9c6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/TokenProcessRequest.java @@ -0,0 +1,11 @@ +package ru.soune.nocopy.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class TokenProcessRequest { + + @JsonProperty("token") + private String token; +} diff --git a/src/main/java/ru/soune/nocopy/entity/AuthToken.java b/src/main/java/ru/soune/nocopy/entity/AuthToken.java index 63cf34a..6fa1a9c 100644 --- a/src/main/java/ru/soune/nocopy/entity/AuthToken.java +++ b/src/main/java/ru/soune/nocopy/entity/AuthToken.java @@ -30,7 +30,7 @@ public class AuthToken { private String token; @Column(name = "expires_at", nullable = false) - private LocalDateTime expiresAt = LocalDateTime.now().plusDays(30); + private LocalDateTime expiresAt = LocalDateTime.now().plusHours(1); @CreatedDate @Column(name = "created_at", updatable = false, nullable = false) @@ -41,8 +41,4 @@ public class AuthToken { @Column(name = "is_active") private Boolean isActive = true; - - public boolean isValid() { - return Boolean.TRUE.equals(isActive) && expiresAt.isAfter(LocalDateTime.now()); - } } diff --git a/src/main/java/ru/soune/nocopy/handler/AuthRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/AuthRequestHandler.java new file mode 100644 index 0000000..c79ec06 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/AuthRequestHandler.java @@ -0,0 +1,37 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.TokenProcessRequest; +import ru.soune.nocopy.exception.NotFoundAuthToken; +import ru.soune.nocopy.service.auth.AuthService; + +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AuthRequestHandler implements RequestHandler { + + private final ObjectMapper objectMapper; + + private final AuthService authService; + + @Override + public BaseResponse handle(BaseRequest request) throws NotFoundAuthToken { + TokenProcessRequest tokenProcessRequest = objectMapper.convertValue(request.getMessageBody(), + TokenProcessRequest.class); + + authService.useUserAuthToken(tokenProcessRequest.getToken()); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Token processed successfully", + Map.of("token", tokenProcessRequest.getToken())); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 968a3b7..526ff9f 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -6,13 +6,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.file.*; -import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.file.FileEntityService; import java.util.Arrays; @@ -24,6 +24,8 @@ import java.util.List; public class FileEntityHandler implements RequestHandler { private final FileEntityService fileEntityService; + private final AuthService authService; + private final AuthTokenRepository authTokenRepository; private final ObjectMapper objectMapper; @@ -75,7 +77,7 @@ public class FileEntityHandler implements RequestHandler { private BaseResponse handleGetFileInfo(BaseRequest request, FileEntityRequest fileRequest) { try { - Long userId = getUserIdFromToken(fileRequest.getToken()); + Long userId = authService.useUserAuthToken(fileRequest.getToken()); FileEntityResponse fileInfo = fileEntityService.getById(fileRequest.getFileId(), request.getVersion()); if (!fileInfo.getUserId().equals(userId)) { @@ -106,7 +108,7 @@ public class FileEntityHandler implements RequestHandler { private BaseResponse handleGetFileBySession(BaseRequest request, FileEntityRequest fileRequest) { try { - Long userId = getUserIdFromToken(fileRequest.getToken()); + Long userId = authService.useUserAuthToken(fileRequest.getToken()); FileEntityResponse fileInfo = fileEntityService.getByUploadSessionId(fileRequest.getUploadSessionId(), request.getVersion()); @@ -138,7 +140,7 @@ public class FileEntityHandler implements RequestHandler { private BaseResponse handleGetUserFiles(BaseRequest request, FileEntityRequest fileRequest) { try { - Long userId = getUserIdFromToken(fileRequest.getToken()); + Long userId = authService.useUserAuthToken(fileRequest.getToken()); int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; @@ -165,7 +167,7 @@ public class FileEntityHandler implements RequestHandler { private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) { try { - Long userId = getUserIdFromToken(fileRequest.getToken()); + Long userId = authService.useUserAuthToken(fileRequest.getToken()); int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; @@ -212,7 +214,7 @@ public class FileEntityHandler implements RequestHandler { private BaseResponse handleGetStorageUsage(BaseRequest request, FileEntityRequest fileRequest) { try { - Long userId = getUserIdFromToken(fileRequest.getToken()); + Long userId = authService.useUserAuthToken(fileRequest.getToken()); long usage = fileEntityService.getUserStorageUsed(userId); StorageUsageResponse response = StorageUsageResponse.builder() @@ -242,7 +244,7 @@ public class FileEntityHandler implements RequestHandler { private BaseResponse handleDeleteFile(BaseRequest request, FileEntityRequest fileRequest) { try { - Long userId = getUserIdFromToken(fileRequest.getToken()); + Long userId = authService.useUserAuthToken(fileRequest.getToken()); String fileId = fileRequest.getFileId(); FileEntityResponse fileInfo = fileEntityService.getById(fileId, request.getVersion()); DeleteFileResponse response; @@ -289,20 +291,6 @@ public class FileEntityHandler implements RequestHandler { } } - private Long getUserIdFromToken(String token) { - if (token == null || token.isBlank()) { - throw new NotFoundAuthToken("Token is required"); - } - - if (token.startsWith("Bearer ")) { - token = token.replace("Bearer ", ""); - } - - AuthToken authToken = authTokenRepository.findByToken(token) - .orElseThrow(() -> new NotFoundAuthToken("Token not found")); - return authToken.getUser().getId(); - } - private String formatFileSize(long size) { if (size < 1024) return size + " B"; int exp = (int) (Math.log(size) / Math.log(1024)); diff --git a/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java index 4cb33b5..d2bbf4c 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java @@ -17,6 +17,7 @@ import ru.soune.nocopy.exception.*; import ru.soune.nocopy.handler.validator.FileUploadRequestValidator; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.FileUploadSessionRepository; +import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.file.FileUploadService; import java.util.*; @@ -28,6 +29,8 @@ import java.util.stream.Collectors; public class FileUploadHandler implements RequestHandler { private final FileUploadService fileUploadService; + private final AuthService authService; + private final AuthTokenRepository authTokenRepository; private final FileUploadSessionRepository fileUploadSessionRepository; @@ -86,10 +89,9 @@ public class FileUploadHandler implements RequestHandler { MessageCode.INVALID_TOKEN.getDescription(), Map.of("token", token)); } - AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token not found")); - + Long userId = authService.useUserAuthToken(token); BindingResult bindingResult = new BeanPropertyBindingResult(fileRequest, "fileRequest"); - fileUploadRequestValidator.validate(fileRequest, bindingResult); + fileUploadRequestValidator.validate(fileRequest, bindingResult); if (bindingResult.hasErrors()) { Map fieldErrors = bindingResult.getFieldErrors() @@ -105,7 +107,7 @@ public class FileUploadHandler implements RequestHandler { } FileUploadSession session = fileUploadService.initUpload( - authToken.getUser().getId(), + userId, fileRequest.getFileName(), fileRequest.getFileType(), fileRequest.getExtension(), diff --git a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java index 95abf63..6add2a0 100644 --- a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java @@ -7,7 +7,7 @@ import ru.soune.nocopy.dto.*; import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.UserRepository; -import ru.soune.nocopy.service.AuthService; +import ru.soune.nocopy.service.auth.AuthService; import java.util.Arrays; import java.util.Map; @@ -17,7 +17,9 @@ import java.util.Map; public class LoginRequestHandler implements RequestHandler { private final UserRepository userRepository; + private final AuthService authService; + private final ObjectMapper objectMapper; @Override @@ -34,9 +36,12 @@ public class LoginRequestHandler implements RequestHandler { } AuthToken authToken = authService.login(loginRequest); + String token = authToken.getToken(); LoginAnswer loginAnswer = new LoginAnswer(); - loginAnswer.setToken(authToken.getToken()); + loginAnswer.setToken(token); + + authService.useUserAuthToken(token); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), loginAnswer); diff --git a/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java index 44fa253..e12284c 100644 --- a/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java @@ -9,7 +9,7 @@ import ru.soune.nocopy.entity.User; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.UserRepository; -import ru.soune.nocopy.service.AuthService; +import ru.soune.nocopy.service.auth.AuthService; import java.util.Objects; import java.util.Optional; diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 349f0a7..86fcbc7 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -12,7 +12,7 @@ import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.ValidationException; import ru.soune.nocopy.handler.validator.RegRequestValidator; import ru.soune.nocopy.repository.UserRepository; -import ru.soune.nocopy.service.AuthService; +import ru.soune.nocopy.service.auth.AuthService; import java.util.Arrays; import java.util.Map; @@ -52,9 +52,11 @@ public class RegRequestHandler implements RequestHandler { } AuthToken authToken = authService.register(regRequest); + String token = authToken.getToken(); + authService.useUserAuthToken(token); RegAnswer regAnswer = new RegAnswer(); - regAnswer.setToken(authToken.getToken()); + regAnswer.setToken(token); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), regAnswer); diff --git a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java index 74826fd..9328753 100644 --- a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java @@ -4,11 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.AuthToken; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Optional; @Repository public interface AuthTokenRepository extends JpaRepository { - Optional findByTokenAndExpiresAtAfter(String token, LocalDate expiresAtAfter); + Optional findByLastUsedAtAndExpiresAtBefore(LocalDateTime lastUsedAt, LocalDateTime expiresAtBefore); + Optional findByLastUsedAtBefore(LocalDateTime lastUsedAt); Optional findByToken(String token); } diff --git a/src/main/java/ru/soune/nocopy/service/AuthService.java b/src/main/java/ru/soune/nocopy/service/auth/AuthService.java similarity index 81% rename from src/main/java/ru/soune/nocopy/service/AuthService.java rename to src/main/java/ru/soune/nocopy/service/auth/AuthService.java index 2377b2b..cf8b18c 100644 --- a/src/main/java/ru/soune/nocopy/service/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/auth/AuthService.java @@ -1,13 +1,13 @@ -package ru.soune.nocopy.service; +package ru.soune.nocopy.service.auth; import lombok.RequiredArgsConstructor; -import org.springframework.context.MessageSource; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.dto.*; import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.UserRepository; @@ -29,8 +29,6 @@ public class AuthService { private final PasswordEncoder passwordEncoder; - private final MessageSource messageSource; - private final SecureRandom secureRandom = new SecureRandom(); @Transactional @@ -81,6 +79,25 @@ public class AuthService { return authTokenRepository.save(authToken); } + @Transactional + public Long useUserAuthToken(String token) { + if (token == null || token.isBlank()) { + throw new NotFoundAuthToken("Token is required"); + } + + if (token.startsWith("Bearer ")) { + token = token.replace("Bearer ", ""); + } + + AuthToken authToken = authTokenRepository.findByToken(token) + .orElseThrow(() -> new NotFoundAuthToken("Token not found")); + authToken.setLastUsedAt(LocalDateTime.now()); + + authTokenRepository.save(authToken); + + return authToken.getUser().getId(); + } + @Transactional public void logout(String token) { authTokenRepository.findByToken(token) diff --git a/src/main/java/ru/soune/nocopy/service/auth/CleanupTokenSessionsService.java b/src/main/java/ru/soune/nocopy/service/auth/CleanupTokenSessionsService.java new file mode 100644 index 0000000..c18cda3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/auth/CleanupTokenSessionsService.java @@ -0,0 +1,38 @@ +package ru.soune.nocopy.service.auth; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.repository.AuthTokenRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CleanupTokenSessionsService { + @Autowired + private AuthTokenRepository authTokenRepository; + + @Value("${file.storage.auth-token-life-hours}") + private int authTokenLife; + + @Transactional + @Scheduled(fixedDelay = 30000) + public void cleanupExpiredFiles() { + LocalDateTime hourBefore = LocalDateTime.now().minusHours(authTokenLife); + + List tokensForDelete = authTokenRepository.findByLastUsedAtBefore(hourBefore).stream().toList(); + List tokensDeleteWithNullUpdate = authTokenRepository.findByLastUsedAtAndExpiresAtBefore( + null, hourBefore).stream().toList(); + + authTokenRepository.deleteAll(tokensForDelete); + authTokenRepository.deleteAll(tokensDeleteWithNullUpdate); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 1fd4fab..68701ca 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -30,6 +30,7 @@ file: max-retry-attempts: ${MAX_RETRY_ATTEMPTS:3} chunk-timeout-ms: ${CHUNK_TIMEOUT_MS:300000} # 5 минут temp-ttl-hours: ${TEMP_TTL_HOURS:72} # 3 дня + auth-token-life-hours: ${AUTH_TOKEN_LIFE:1} # 1 час session-expiry-hours: ${SESSION_EXPIRY_HOURS:24} security: diff --git a/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java b/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java index 499d694..a16af7d 100644 --- a/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java +++ b/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java @@ -15,6 +15,7 @@ import ru.soune.nocopy.exception.NotValidationPasswordException; import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.auth.AuthService; import java.util.Optional;