diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index dbbe9df..e76c54a 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -19,7 +19,7 @@ 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.auth.AuthToken; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.exception.*; diff --git a/src/main/java/ru/soune/nocopy/controller/UserController.java b/src/main/java/ru/soune/nocopy/controller/UserController.java index 39efe8e..55e25a5 100644 --- a/src/main/java/ru/soune/nocopy/controller/UserController.java +++ b/src/main/java/ru/soune/nocopy/controller/UserController.java @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.*; import ru.soune.nocopy.dto.ChangePasswordRequest; import ru.soune.nocopy.dto.UserDTO; import ru.soune.nocopy.dto.UserRequest; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.auth.AuthToken; import ru.soune.nocopy.entity.User; import ru.soune.nocopy.exception.InvalidUserEmail; import ru.soune.nocopy.exception.NotFoundAuthToken; diff --git a/src/main/java/ru/soune/nocopy/controller/VkAuthController.java b/src/main/java/ru/soune/nocopy/controller/VkAuthController.java new file mode 100644 index 0000000..e4ce5af --- /dev/null +++ b/src/main/java/ru/soune/nocopy/controller/VkAuthController.java @@ -0,0 +1,93 @@ +package ru.soune.nocopy.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.LoginAnswer; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.entity.auth.AuthToken; +import ru.soune.nocopy.service.AuthService; +import ru.soune.nocopy.service.VkOAuthService; +import ru.soune.nocopy.service.auth.ExternalAuthUserService; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class VkAuthController { + + private final VkOAuthService vkOAuthService; + + private final AuthService authService; + + private final ExternalAuthUserService externalAuthUserService; + + @GetMapping("/v{version}/vk/authorization/{code}/{state}/{codeVerifier}/{deviceId}") + public BaseResponse vkAuth(@PathVariable("code") String code, + @PathVariable("state") String state, + @PathVariable("codeVerifier") String codeVerifier, + @PathVariable("deviceId") String deviceId) { + try { + Map authMap = vkOAuthService.authTokenMap(code, state, codeVerifier, deviceId); + ObjectMapper mapper = new ObjectMapper(); + + String bodyJson = (String) authMap.get("body"); + Map body = mapper.readValue(bodyJson, Map.class); + + String accessToken = (String) body.get("access_token"); + long vkUserId = ((Number) body.get("user_id")).longValue(); + Integer expiresIn = (Integer) body.get("expires_in"); + String refreshToken = (String) body.get("refresh_token"); + + Map vkUserInfo = vkOAuthService.userVkInfo(accessToken); + log.info("vkUserInfo {}", vkUserInfo); + + String userInfoBodyJson = (String) vkUserInfo.get("body"); + Map userInfoBody = mapper.readValue(userInfoBodyJson, Map.class); + Map user = (Map) userInfoBody.get("user"); + + String firstName = (String) user.get("first_name"); + String lastName = (String) user.get("last_name"); + String fullName = firstName + " " + lastName; + + User userEntity = externalAuthUserService.findOrCreateUserFromVK(Long.toString(vkUserId), fullName, + null, accessToken, refreshToken, expiresIn); + + AuthToken ourToken = authService.createTokenForUser(userEntity); + + LoginAnswer loginAnswer = new LoginAnswer(); + loginAnswer.setToken(ourToken.getToken()); + loginAnswer.setFieldErrors(new ArrayList<>()); + + return new BaseResponse(20009, MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), loginAnswer); + } catch (Exception e) { + log.error("VK auth error: ", e); + + LoginAnswer loginAnswer = new LoginAnswer(); + loginAnswer.setToken(null); + + List> errors = new ArrayList<>(); + Map error = new HashMap<>(); + error.put("field", "vk_auth"); + error.put("message", e.getMessage()); + errors.add(error); + + loginAnswer.setFieldErrors(errors); + + return new BaseResponse(20009, MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), loginAnswer); + } + } +} + diff --git a/src/main/java/ru/soune/nocopy/dto/AuthResponse.java b/src/main/java/ru/soune/nocopy/dto/AuthResponse.java deleted file mode 100644 index 168e09a..0000000 --- a/src/main/java/ru/soune/nocopy/dto/AuthResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.soune.nocopy.dto; - -import java.time.LocalDateTime; - -public record AuthResponse ( - boolean success, - String message, - String token, - LocalDateTime expiresAt -) {} diff --git a/src/main/java/ru/soune/nocopy/entity/User.java b/src/main/java/ru/soune/nocopy/entity/User.java index 0841027..f2b981b 100644 --- a/src/main/java/ru/soune/nocopy/entity/User.java +++ b/src/main/java/ru/soune/nocopy/entity/User.java @@ -6,11 +6,14 @@ import jakarta.validation.constraints.Size; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.auth.AuthToken; +import ru.soune.nocopy.entity.auth.ExternalAuthProvider; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Optional; @Entity @Table(name = "users") @@ -19,18 +22,19 @@ import java.util.List; @NoArgsConstructor @AllArgsConstructor @ToString +@Builder @EntityListeners(AuditingEntityListener.class) public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long Id; + private Long id; @Column(name = "full_name", nullable = false) private String fullName; - @Size(max = 1024) - @Column(name = "email", nullable = false, length = 1024, unique = true) + @Size(max = 255) + @Column(name = "email", nullable = false, length = 255, unique = true) private String email; @Column(name = "company") @@ -90,4 +94,14 @@ public class User { @JsonIgnore @ToString.Exclude private List imageProtections = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @ToString.Exclude + private List externalAuthProviders = new ArrayList<>(); + + public Optional getProvider(ExternalAuthProvider.ProviderType providerType) { + return externalAuthProviders.stream() + .filter(p -> p.getProviderType() == providerType) + .findFirst(); + } } diff --git a/src/main/java/ru/soune/nocopy/entity/AuthToken.java b/src/main/java/ru/soune/nocopy/entity/auth/AuthToken.java similarity index 88% rename from src/main/java/ru/soune/nocopy/entity/AuthToken.java rename to src/main/java/ru/soune/nocopy/entity/auth/AuthToken.java index 63cf34a..be59877 100644 --- a/src/main/java/ru/soune/nocopy/entity/AuthToken.java +++ b/src/main/java/ru/soune/nocopy/entity/auth/AuthToken.java @@ -1,9 +1,10 @@ -package ru.soune.nocopy.entity; +package ru.soune.nocopy.entity.auth; import jakarta.persistence.*; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.User; import java.time.LocalDateTime; @@ -30,7 +31,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) diff --git a/src/main/java/ru/soune/nocopy/entity/auth/ExternalAuthProvider.java b/src/main/java/ru/soune/nocopy/entity/auth/ExternalAuthProvider.java new file mode 100644 index 0000000..9375423 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/auth/ExternalAuthProvider.java @@ -0,0 +1,89 @@ +package ru.soune.nocopy.entity.auth; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "external_auth_providers", + uniqueConstraints = @UniqueConstraint( + columnNames = {"provider_type", "provider_user_id"} + ), + indexes = { + @Index(name = "idx_provider_type_user_id", + columnList = "provider_type, provider_user_id"), + @Index(name = "idx_user_id_provider_type", + columnList = "user_id, provider_type") + }) +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EntityListeners(AuditingEntityListener.class) +public class ExternalAuthProvider { + + public enum ProviderType { + VK, YANDEX + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_extauth_user")) + @ToString.Exclude + private User user; + + @Enumerated(EnumType.STRING) + @Column(name = "provider_type", nullable = false, length = 20) + private ProviderType providerType; + + @Column(name = "provider_user_id", nullable = false, length = 255) + private String providerUserId; + + @Column(name = "access_token", length = 2048) + private String accessToken; + + @Column(name = "refresh_token", length = 2048) + private String refreshToken; + + @Column(name = "token_expires_at") + private LocalDateTime tokenExpiresAt; + + @Column(name = "email", length = 255) + private String email; + + @Column(name = "phone", length = 20) + private String phoneNumber; + + @CreatedDate + @Column(name = "linked_at", updatable = false) + private LocalDateTime linkedAt; + + @Column(name = "last_used_at") + private LocalDateTime lastUsedAt; + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; + + @PrePersist + public void prePersist() { + if (this.linkedAt == null) { + this.linkedAt = LocalDateTime.now(); + } + if (this.lastUsedAt == null) { + this.lastUsedAt = LocalDateTime.now(); + } + } + + @PreUpdate + public void preUpdate() { + this.lastUsedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 9bddff0..7aebbab 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -6,10 +6,9 @@ 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.auth.AuthToken; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; -import ru.soune.nocopy.entity.file.FileType; import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.repository.AuthTokenRepository; diff --git a/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java index 4cb33b5..4ee09bc 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java @@ -10,7 +10,7 @@ import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.file.*; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.auth.AuthToken; import ru.soune.nocopy.entity.file.FileType; import ru.soune.nocopy.entity.file.FileUploadSession; import ru.soune.nocopy.exception.*; diff --git a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java index 95abf63..1d1facd 100644 --- a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.auth.AuthToken; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.AuthService; diff --git a/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java index 44fa253..9ccbc05 100644 --- a/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.auth.AuthToken; import ru.soune.nocopy.entity.User; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.AuthTokenRepository; diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 349f0a7..1e813b1 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -7,7 +7,7 @@ import org.springframework.stereotype.Component; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; import ru.soune.nocopy.dto.*; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.auth.AuthToken; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.ValidationException; import ru.soune.nocopy.handler.validator.RegRequestValidator; diff --git a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java index 74826fd..21885a3 100644 --- a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java @@ -2,7 +2,7 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.auth.AuthToken; import java.time.LocalDate; import java.util.Optional; diff --git a/src/main/java/ru/soune/nocopy/repository/ExternalAuthProviderRepository.java b/src/main/java/ru/soune/nocopy/repository/ExternalAuthProviderRepository.java new file mode 100644 index 0000000..59bd67c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ExternalAuthProviderRepository.java @@ -0,0 +1,25 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.entity.auth.ExternalAuthProvider; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ExternalAuthProviderRepository extends JpaRepository { + + Optional findByProviderTypeAndProviderUserId( + ExternalAuthProvider.ProviderType providerType, + String providerUserId + ); + + List findByUser(User user); + + boolean existsByProviderTypeAndProviderUserId( + ExternalAuthProvider.ProviderType providerType, + String providerUserId + ); +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/repository/UserRepository.java b/src/main/java/ru/soune/nocopy/repository/UserRepository.java index f674629..850aa3c 100644 --- a/src/main/java/ru/soune/nocopy/repository/UserRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/UserRepository.java @@ -2,7 +2,10 @@ package ru.soune.nocopy.repository; import jakarta.validation.constraints.Size; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.entity.auth.ExternalAuthProvider; import java.util.Optional; @@ -10,4 +13,10 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); boolean existsByEmail(String email); boolean existsByPhone(@Size(min = 11, max = 14) String phone); + @Query("SELECT u FROM User u JOIN u.externalAuthProviders p " + + "WHERE p.providerType = :providerType AND p.providerUserId = :providerUserId") + Optional findByExternalProvider( + @Param("providerType") ExternalAuthProvider.ProviderType providerType, + @Param("providerUserId") String providerUserId + ); } diff --git a/src/main/java/ru/soune/nocopy/service/AuthService.java b/src/main/java/ru/soune/nocopy/service/AuthService.java index 2377b2b..b339d63 100644 --- a/src/main/java/ru/soune/nocopy/service/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/AuthService.java @@ -6,7 +6,7 @@ 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.auth.AuthToken; import ru.soune.nocopy.entity.User; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.AuthTokenRepository; @@ -87,6 +87,20 @@ public class AuthService { .ifPresent(authTokenRepository::delete); } + @Transactional + public AuthToken createTokenForUser(User user) { + AuthToken authToken = new AuthToken(); + authToken.setToken(generateAuthToken()); + authToken.setUser(user); + authToken.setExpiresAt(LocalDateTime.now().plusHours(1)); + authToken.setIsActive(true); + + user.setLastLoginAt(LocalDateTime.now()); + userRepository.save(user); + + return authTokenRepository.save(authToken); + } + private String generateAuthToken() { byte[] bytes = new byte[32]; secureRandom.nextBytes(bytes); diff --git a/src/main/java/ru/soune/nocopy/service/VkOAuthService.java b/src/main/java/ru/soune/nocopy/service/VkOAuthService.java new file mode 100644 index 0000000..84fbb8c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/VkOAuthService.java @@ -0,0 +1,69 @@ +package ru.soune.nocopy.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class VkOAuthService { + @Value("${auth.vk.oauth2}") + String oauthVkPath; + + @Value("${auth.vk.user_info}") + String vkUserInfo; + + @Value("${auth.vk.client_id}") + String clientId; + + @Value("${auth.vk.redirect_uri}") + String redirectUri; + + public Map authTokenMap(String code, String state, String codeVerifier, String deviceId) { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("code_verifier", codeVerifier); + params.add("redirect_uri", redirectUri); + params.add("code", code); + params.add("client_id", clientId); + params.add("device_id", deviceId); + params.add("state", state); + params.add("scope", "email,phone"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + return sendRequest(params, headers,oauthVkPath); + } + + public Map userVkInfo(String accessToken) { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("access_token", accessToken); + params.add("client_id", clientId); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + + return sendRequest(params, headers, vkUserInfo); + } + + private Map sendRequest(MultiValueMap params, HttpHeaders headers, String url) { + Map info = new HashMap<>(); + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, + new HttpEntity<>(params, headers), String.class); + + info.put("status", response.getStatusCodeValue()); + info.put("body", response.getBody()); + info.put("headers", response.getHeaders()); + info.put("success", response.getStatusCode().is2xxSuccessful()); + + return info; + } +} diff --git a/src/main/java/ru/soune/nocopy/service/auth/ExternalAuthUserService.java b/src/main/java/ru/soune/nocopy/service/auth/ExternalAuthUserService.java new file mode 100644 index 0000000..6fec9f6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/auth/ExternalAuthUserService.java @@ -0,0 +1,102 @@ +package ru.soune.nocopy.service.auth; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.entity.auth.ExternalAuthProvider; +import ru.soune.nocopy.repository.ExternalAuthProviderRepository; +import ru.soune.nocopy.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class ExternalAuthUserService { + private final UserRepository userRepository; + + private final ExternalAuthProviderRepository externalAuthProviderRepository; + + @Transactional + public User findOrCreateUserFromVK(String vkUserId, String fullName, String email, + String vkAccessToken, String vkRefreshToken, Integer expiresIn) { + + Optional existingByVkId = userRepository.findByExternalProvider(ExternalAuthProvider.ProviderType.VK, + vkUserId); + + if (existingByVkId.isPresent()) { + User user = existingByVkId.get(); + updateVKProvider(user, vkAccessToken, vkRefreshToken, expiresIn, email); + + return user; + } + + if (email != null && !email.isBlank()) { + Optional existingByEmail = userRepository.findByEmail(email); + + if (existingByEmail.isPresent()) { + User user = existingByEmail.get(); + linkVKProvider(user, vkUserId, vkAccessToken, vkRefreshToken, expiresIn, email); + + return user; + } + } + + return createNewUserWithVK(vkUserId, fullName, email, vkAccessToken, vkRefreshToken, expiresIn); + } + + private User createNewUserWithVK(String vkUserId, String fullName, String email, + String vkAccessToken, String vkRefreshToken, Integer expiresIn) { + User newUser = User.builder() + .fullName(fullName) + .email(email) + .password("generateRandomPassword()") + .isActive(true) + .build(); + + User savedUser = userRepository.save(newUser); + + linkVKProvider(savedUser, vkUserId, vkAccessToken, vkRefreshToken, expiresIn, email); + + return savedUser; + } + + private void linkVKProvider(User user, String vkUserId, String vkAccessToken, String vkRefreshToken, + Integer expiresIn, String email) { + ExternalAuthProvider provider = ExternalAuthProvider.builder() + .user(user) + .providerType(ExternalAuthProvider.ProviderType.VK) + .providerUserId(vkUserId) + .accessToken(vkAccessToken) + .refreshToken(vkRefreshToken) + .tokenExpiresAt(LocalDateTime.now().plusSeconds(expiresIn)) + .email(email) + .isActive(true) + .build(); + + externalAuthProviderRepository.save(provider); + + user.getExternalAuthProviders().add(provider); + } + + private void updateVKProvider(User user, String vkAccessToken, String vkRefreshToken, + Integer expiresIn, String email) { + Optional existingProvider = user.getProvider(ExternalAuthProvider.ProviderType.VK); + + if (existingProvider.isPresent()) { + ExternalAuthProvider provider = existingProvider.get(); + provider.setAccessToken(vkAccessToken); + provider.setTokenExpiresAt(LocalDateTime.now().plusSeconds(expiresIn)); + provider.setEmail(email); + provider.setRefreshToken(vkRefreshToken); + provider.setLastUsedAt(LocalDateTime.now()); + + externalAuthProviderRepository.save(provider); + } + } + + private String generateRandomPassword() { + return java.util.UUID.randomUUID().toString(); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 1fd4fab..31ef992 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -32,6 +32,13 @@ file: temp-ttl-hours: ${TEMP_TTL_HOURS:72} # 3 дня session-expiry-hours: ${SESSION_EXPIRY_HOURS:24} +auth: + vk: + oauth2: "https://id.vk.ru/oauth2/auth" + user_info: "https://id.vk.ru/oauth2/user_info" + client_id: "54389559" + redirect_uri: "http://localhost" + security: cors: allowed-origins: "*" diff --git a/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java b/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java index 499d694..fb7292d 100644 --- a/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java +++ b/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java @@ -9,7 +9,7 @@ import org.mockito.MockitoAnnotations; import org.springframework.context.MessageSource; import org.springframework.security.crypto.password.PasswordEncoder; import ru.soune.nocopy.dto.LoginRequest; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.auth.AuthToken; import ru.soune.nocopy.entity.User; import ru.soune.nocopy.exception.NotValidationPasswordException; import ru.soune.nocopy.exception.UserNotFoundException;