Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d34a304763 |
+1
-7
@@ -29,19 +29,13 @@ dependencies {
|
|||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
implementation 'org.springframework.security:spring-security-crypto:6.5.3'
|
implementation 'org.springframework.security:spring-security-crypto:6.5.3'
|
||||||
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
|
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
|
||||||
implementation 'commons-validator:commons-validator:1.7'
|
|
||||||
|
|
||||||
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
|
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
|
|
||||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||||
|
|
||||||
runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc'
|
runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc'
|
||||||
runtimeOnly 'com.mysql:mysql-connector-j'
|
runtimeOnly 'com.mysql:mysql-connector-j'
|
||||||
runtimeOnly 'org.postgresql:postgresql'
|
runtimeOnly 'org.postgresql:postgresql'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
testImplementation 'org.mockito:mockito-core:5.3.1'
|
testImplementation 'org.mockito:mockito-core:5.3.1'
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import ru.soune.nocopy.dto.RegAnswer;
|
|||||||
import ru.soune.nocopy.exception.NotValidFieldException;
|
import ru.soune.nocopy.exception.NotValidFieldException;
|
||||||
import ru.soune.nocopy.exception.ValidationException;
|
import ru.soune.nocopy.exception.ValidationException;
|
||||||
import ru.soune.nocopy.handler.LoginRequestHandler;
|
import ru.soune.nocopy.handler.LoginRequestHandler;
|
||||||
|
import ru.soune.nocopy.handler.LogoutRequestHandler;
|
||||||
import ru.soune.nocopy.handler.RegRequestHandler;
|
import ru.soune.nocopy.handler.RegRequestHandler;
|
||||||
import ru.soune.nocopy.handler.RequestHandler;
|
import ru.soune.nocopy.handler.RequestHandler;
|
||||||
|
|
||||||
@@ -36,9 +37,11 @@ public class ApiController {
|
|||||||
public void init() {
|
public void init() {
|
||||||
RegRequestHandler regHandler = applicationContext.getBean(RegRequestHandler.class);
|
RegRequestHandler regHandler = applicationContext.getBean(RegRequestHandler.class);
|
||||||
LoginRequestHandler loginHandler = applicationContext.getBean(LoginRequestHandler.class);
|
LoginRequestHandler loginHandler = applicationContext.getBean(LoginRequestHandler.class);
|
||||||
|
LogoutRequestHandler logoutHandler = applicationContext.getBean(LogoutRequestHandler.class);
|
||||||
|
|
||||||
handlers.put(20002, regHandler);
|
|
||||||
handlers.put(20001, loginHandler);
|
handlers.put(20001, loginHandler);
|
||||||
|
handlers.put(20002, regHandler);
|
||||||
|
handlers.put(20003, logoutHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/v{version}/data")
|
@PostMapping("/v{version}/data")
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package ru.soune.nocopy.controller;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import ru.soune.nocopy.entity.AuthToken;
|
|
||||||
import ru.soune.nocopy.exception.TokenNotFoundException;
|
|
||||||
import ru.soune.nocopy.repository.AuthTokenRepository;
|
|
||||||
import ru.soune.nocopy.service.AuthService;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("v1/api/auth")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AuthController {
|
|
||||||
|
|
||||||
private final AuthService authService;
|
|
||||||
|
|
||||||
private final AuthTokenRepository authTokenRepository;
|
|
||||||
|
|
||||||
@PostMapping("/logout")
|
|
||||||
public ResponseEntity<?> logout(@RequestHeader("Authorization") String tokenHeader) {
|
|
||||||
String token = tokenHeader.replace("Bearer ", "");
|
|
||||||
|
|
||||||
Optional<AuthToken> opToken = authTokenRepository.findByToken(token);
|
|
||||||
|
|
||||||
if (opToken.isEmpty()) {
|
|
||||||
throw new TokenNotFoundException("Token not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
authService.logout(token);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("success", true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,14 +4,11 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class LogOutAnswer {
|
public class LogOutAnswer {
|
||||||
private String Token;
|
private String token;
|
||||||
|
|
||||||
private List<Map<String, String>> fieldErrors;
|
private String email;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ru.soune.nocopy.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class LogoutRequest {
|
||||||
|
@NotBlank(message = "error.not.blank") @Email(message = "error.not.email") @Size(max = 128)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@NotBlank(message = "error.not.blank")
|
||||||
|
private String token;
|
||||||
|
}
|
||||||
@@ -3,12 +3,13 @@ package ru.soune.nocopy.dto;
|
|||||||
public enum MessageCode {
|
public enum MessageCode {
|
||||||
SUCCESS(0, "Operation successful"),
|
SUCCESS(0, "Operation successful"),
|
||||||
REG_EMAIL_EXISTS(1, "Email already registered"),
|
REG_EMAIL_EXISTS(1, "Email already registered"),
|
||||||
REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"),
|
|
||||||
INVALID_FIELD(2, "Invalid field"),
|
INVALID_FIELD(2, "Invalid field"),
|
||||||
INVALID_JSON_BODY(2, "Invalid fields in JSON object"),
|
INVALID_JSON_BODY(2, "Invalid fields in JSON object"),
|
||||||
MSG_ID_NOT_FOUND(4, "Message id not found"),
|
MSG_ID_NOT_FOUND(4, "Message id not found"),
|
||||||
|
|
||||||
AUTH_EMAIL_NOT_FOUND(4, "Email not found"),
|
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_PASSWORD_NOT_MATCHES(2, "Password does not match");
|
AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match");
|
||||||
|
|
||||||
private final Integer code;
|
private final Integer code;
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ public class User {
|
|||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@Size(min = 11, max = 14)
|
@Size(min = 11, max = 14)
|
||||||
@Column(unique = true)
|
|
||||||
private String phone;
|
private String phone;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package ru.soune.nocopy.handler;
|
||||||
|
|
||||||
|
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.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 java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LogoutRequestHandler implements RequestHandler {
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
|
private final AuthTokenRepository authTokenRepository;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BaseResponse handle(BaseRequest request) {
|
||||||
|
LogoutRequest logoutRequest = objectMapper.convertValue(request.getMessageBody(), LogoutRequest.class);
|
||||||
|
|
||||||
|
Optional<AuthToken> opToken = authTokenRepository.findByToken(logoutRequest.getToken());
|
||||||
|
Optional<User> user = userRepository.findByEmail(logoutRequest.getEmail());
|
||||||
|
|
||||||
|
if (opToken.isEmpty() || user.isEmpty()) {
|
||||||
|
throw new NotValidFieldException("User with email or token not found",
|
||||||
|
new BaseResponse(request.getMsgId(), MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getCode(),
|
||||||
|
MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getDescription(),
|
||||||
|
new LogoutRequest(logoutRequest.getEmail(), logoutRequest.getToken())));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Objects.equals(opToken.get().getUser().getId(), user.get().getId())) {
|
||||||
|
throw new NotValidFieldException("User token mismatch with user",
|
||||||
|
new BaseResponse(request.getMsgId(), MessageCode.AUTH_TOKEN_MISMATCH.getCode(),
|
||||||
|
MessageCode.AUTH_TOKEN_MISMATCH.getDescription(),
|
||||||
|
new LogoutRequest(logoutRequest.getEmail(), logoutRequest.getToken())));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
authService.logout(logoutRequest.getToken());
|
||||||
|
|
||||||
|
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(),
|
||||||
|
new LogOutAnswer(logoutRequest.getEmail(), logoutRequest.getToken()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,15 +33,13 @@ public class RegRequestHandler implements RequestHandler {
|
|||||||
public BaseResponse handle(BaseRequest request) throws ValidationException {
|
public BaseResponse handle(BaseRequest request) throws ValidationException {
|
||||||
RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class);
|
RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class);
|
||||||
|
|
||||||
if (userRepository.existsByEmail(regRequest.getEmail()) || userRepository.existsByPhone(regRequest.getPhone())) {
|
if (userRepository.existsByEmail(regRequest.getEmail())) {
|
||||||
RegAnswer regAnswer = new RegAnswer();
|
RegAnswer regAnswer = new RegAnswer();
|
||||||
regAnswer.setFieldErrors(Arrays.asList(Map.of("email", regRequest.getEmail())));
|
regAnswer.setFieldErrors(Arrays.asList(Map.of("email", regRequest.getEmail())));
|
||||||
regAnswer.setFieldErrors(Arrays.asList(Map.of("phone", regRequest.getPhone())));
|
|
||||||
|
|
||||||
throw new NotValidFieldException("User already exists with email:" + regRequest.getEmail() + " or phone: " +
|
throw new NotValidFieldException("User already exists with email: " + regRequest.getEmail(),
|
||||||
regRequest.getPhone(), new BaseResponse(request.getMsgId(),
|
new BaseResponse(request.getMsgId(), MessageCode.REG_EMAIL_EXISTS.getCode(),
|
||||||
MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getCode(),
|
MessageCode.REG_EMAIL_EXISTS.getDescription(), regAnswer));
|
||||||
MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest");
|
BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest");
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
package ru.soune.nocopy.handler.validator;
|
package ru.soune.nocopy.handler.validator;
|
||||||
|
|
||||||
import org.apache.commons.validator.routines.DomainValidator;
|
|
||||||
import org.apache.commons.validator.routines.EmailValidator;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.validation.Errors;
|
import org.springframework.validation.Errors;
|
||||||
import org.springframework.validation.Validator;
|
import org.springframework.validation.Validator;
|
||||||
import ru.soune.nocopy.dto.RegRequest;
|
import ru.soune.nocopy.dto.RegRequest;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -56,6 +52,27 @@ public class RegRequestValidator implements Validator {
|
|||||||
errors.rejectValue("fullName", "fullName.invalid.chars",
|
errors.rejectValue("fullName", "fullName.invalid.chars",
|
||||||
"Name can only contain letters, spaces, hyphens and apostrophes");
|
"Name can only contain letters, spaces, hyphens and apostrophes");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String[] nameParts = trimmedName.split("\\s+");
|
||||||
|
if (nameParts.length < 2) {
|
||||||
|
errors.rejectValue("fullName", "fullName.missing.parts",
|
||||||
|
"Please enter both first and last name");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < nameParts.length; i++) {
|
||||||
|
String part = nameParts[i];
|
||||||
|
if (part.length() < 2 && part.length() > 0) {
|
||||||
|
errors.rejectValue("fullName", "fullName.part.too.short",
|
||||||
|
"Each name part must be at least 2 characters");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Character.isUpperCase(part.charAt(0))) {
|
||||||
|
errors.rejectValue("fullName", "fullName.capitalization",
|
||||||
|
"Each name part should start with capital letter");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateCompanyName(String companyName, Errors errors) {
|
private void validateCompanyName(String companyName, Errors errors) {
|
||||||
@@ -93,24 +110,64 @@ public class RegRequestValidator implements Validator {
|
|||||||
|
|
||||||
private void validateEmail(String email, Errors errors) {
|
private void validateEmail(String email, Errors errors) {
|
||||||
if (email == null || email.trim().isEmpty()) {
|
if (email == null || email.trim().isEmpty()) {
|
||||||
errors.rejectValue("email", "email.is.empty", "Email must not be empty");
|
errors.rejectValue("email", "email.is.empty",
|
||||||
|
"Email must not be empty");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String trimmedEmail = email.trim().toLowerCase();
|
String trimmedEmail = email.trim().toLowerCase();
|
||||||
|
|
||||||
if (trimmedEmail.length() > 254) {
|
if (trimmedEmail.length() > 128) {
|
||||||
errors.rejectValue("email", "email.too.long", "Email is too long");
|
errors.rejectValue("email", "email.too.long",
|
||||||
|
"Email must be less than 128 characters");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
EmailValidator emailValidator = EmailValidator.getInstance(true, true);
|
if (!trimmedEmail.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) {
|
||||||
if (!emailValidator.isValid(trimmedEmail)) {
|
errors.rejectValue("email", "email.invalid.format",
|
||||||
errors.rejectValue("email", "email.invalid", "Invalid email address");
|
"Invalid email format");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String[] parts = trimmedEmail.split("@");
|
||||||
|
if (parts.length != 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String localPart = parts[0];
|
||||||
|
String domain = parts[1];
|
||||||
|
|
||||||
|
if (localPart.length() > 64) {
|
||||||
|
errors.rejectValue("email", "email.local.too.long",
|
||||||
|
"Email username too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localPart.matches("^[a-z0-9+_.-]+$")) {
|
||||||
|
errors.rejectValue("email", "email.local.invalid.chars",
|
||||||
|
"Email contains invalid characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
validateEmailDomain(domain, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateEmailDomain(String domain, Errors errors) {
|
||||||
|
if (domain.length() > 255) {
|
||||||
|
errors.rejectValue("email", "email.domain.too.long",
|
||||||
|
"Email domain too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] domainParts = domain.split("\\.");
|
||||||
|
if (domainParts.length < 2) {
|
||||||
|
errors.rejectValue("email", "email.domain.invalid",
|
||||||
|
"Invalid domain name");
|
||||||
|
}
|
||||||
|
|
||||||
|
String tld = domainParts[domainParts.length - 1];
|
||||||
|
if (tld.length() < 2) {
|
||||||
|
errors.rejectValue("email", "email.tld.invalid",
|
||||||
|
"Invalid domain extension");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void validatePhone(String phone, Errors errors) {
|
private void validatePhone(String phone, Errors errors) {
|
||||||
if (phone == null || phone.trim().isEmpty()) {
|
if (phone == null || phone.trim().isEmpty()) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package ru.soune.nocopy.repository;
|
package ru.soune.nocopy.repository;
|
||||||
|
|
||||||
import jakarta.validation.constraints.Size;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import ru.soune.nocopy.entity.User;
|
import ru.soune.nocopy.entity.User;
|
||||||
|
|
||||||
@@ -9,5 +8,4 @@ import java.util.Optional;
|
|||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
Optional<User> findByEmail(String email);
|
Optional<User> findByEmail(String email);
|
||||||
boolean existsByEmail(String email);
|
boolean existsByEmail(String email);
|
||||||
boolean existsByPhone(@Size(min = 11, max = 14) String phone);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user