work version vk auth without buisness acc #13

Open
backdev wants to merge 1 commits from NCBACK-6 into dev
20 changed files with 438 additions and 26 deletions
Showing only changes of commit f4e5ea2976 - Show all commits
@@ -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.*;
@@ -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;
@@ -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<String, Object> authMap = vkOAuthService.authTokenMap(code, state, codeVerifier, deviceId);
ObjectMapper mapper = new ObjectMapper();
String bodyJson = (String) authMap.get("body");
Map<String, Object> 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<String, Object> vkUserInfo = vkOAuthService.userVkInfo(accessToken);
log.info("vkUserInfo {}", vkUserInfo);
String userInfoBodyJson = (String) vkUserInfo.get("body");
Map<String, Object> userInfoBody = mapper.readValue(userInfoBodyJson, Map.class);
Map<String, Object> user = (Map<String, Object>) 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<Map<String, String>> errors = new ArrayList<>();
Map<String, String> 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);
}
}
}
@@ -1,10 +0,0 @@
package ru.soune.nocopy.dto;
import java.time.LocalDateTime;
public record AuthResponse (
boolean success,
String message,
String token,
LocalDateTime expiresAt
) {}
+17 -3
View File
@@ -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<ImageProtection> imageProtections = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@ToString.Exclude
private List<ExternalAuthProvider> externalAuthProviders = new ArrayList<>();
public Optional<ExternalAuthProvider> getProvider(ExternalAuthProvider.ProviderType providerType) {
return externalAuthProviders.stream()
.filter(p -> p.getProviderType() == providerType)
.findFirst();
}
}
@@ -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)
@@ -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();
}
}
@@ -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;
@@ -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.*;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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<ExternalAuthProvider, Long> {
Optional<ExternalAuthProvider> findByProviderTypeAndProviderUserId(
ExternalAuthProvider.ProviderType providerType,
String providerUserId
);
List<ExternalAuthProvider> findByUser(User user);
boolean existsByProviderTypeAndProviderUserId(
ExternalAuthProvider.ProviderType providerType,
String providerUserId
);
}
@@ -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<User, Long> {
Optional<User> 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<User> findByExternalProvider(
@Param("providerType") ExternalAuthProvider.ProviderType providerType,
@Param("providerUserId") String providerUserId
);
}
@@ -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);
@@ -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<String, Object> authTokenMap(String code, String state, String codeVerifier, String deviceId) {
MultiValueMap<String, String> 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<String, Object> userVkInfo(String accessToken) {
MultiValueMap<String, String> 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<String, Object> sendRequest(MultiValueMap<String, String> params, HttpHeaders headers, String url) {
Map<String, Object> info = new HashMap<>();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> 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;
}
}
@@ -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<User> 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<User> 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<ExternalAuthProvider> 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();
}
}
+7
View File
@@ -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: "*"
@@ -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;