4 Commits
Author SHA1 Message Date
vladp 6797a47df5 check unique phone
Test Workflow / test (push) Successful in 3s
2025-12-16 16:43:20 +07:00
vladp fade754c7c fix check email
Test Workflow / test (push) Successful in 2s
2025-12-16 12:41:55 +07:00
vladp f164370989 add check adress
Test Workflow / test (push) Successful in 4s
2025-12-12 13:07:46 +07:00
vladp 015f2f516c delete need full name
Test Workflow / test (push) Successful in 2s
2025-12-11 20:53:28 +07:00
11 changed files with 70 additions and 155 deletions
+7 -1
View File
@@ -29,13 +29,19 @@ 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,7 +15,6 @@ 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;
@@ -37,11 +36,9 @@ 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(20001, loginHandler);
handlers.put(20002, regHandler); handlers.put(20002, regHandler);
handlers.put(20003, logoutHandler); handlers.put(20001, loginHandler);
} }
@PostMapping("/v{version}/data") @PostMapping("/v{version}/data")
@@ -0,0 +1,37 @@
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,11 +4,14 @@ 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 String email; private List<Map<String, String>> fieldErrors;
} }
@@ -1,19 +0,0 @@
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,13 +3,12 @@ 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,6 +42,7 @@ 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)
@@ -1,56 +0,0 @@
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,13 +33,15 @@ 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())) { if (userRepository.existsByEmail(regRequest.getEmail()) || userRepository.existsByPhone(regRequest.getPhone())) {
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(), throw new NotValidFieldException("User already exists with email:" + regRequest.getEmail() + " or phone: " +
new BaseResponse(request.getMsgId(), MessageCode.REG_EMAIL_EXISTS.getCode(), regRequest.getPhone(), new BaseResponse(request.getMsgId(),
MessageCode.REG_EMAIL_EXISTS.getDescription(), regAnswer)); MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getCode(),
MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer));
} }
BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest");
@@ -1,10 +1,14 @@
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
@@ -52,27 +56,6 @@ 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) {
@@ -110,64 +93,24 @@ 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", errors.rejectValue("email", "email.is.empty", "Email must not be empty");
"Email must not be empty");
return; return;
} }
String trimmedEmail = email.trim().toLowerCase(); String trimmedEmail = email.trim().toLowerCase();
if (trimmedEmail.length() > 128) { if (trimmedEmail.length() > 254) {
errors.rejectValue("email", "email.too.long", errors.rejectValue("email", "email.too.long", "Email is too long");
"Email must be less than 128 characters");
return; return;
} }
if (!trimmedEmail.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) { EmailValidator emailValidator = EmailValidator.getInstance(true, true);
errors.rejectValue("email", "email.invalid.format", if (!emailValidator.isValid(trimmedEmail)) {
"Invalid email format"); errors.rejectValue("email", "email.invalid", "Invalid email address");
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,5 +1,6 @@
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;
@@ -8,4 +9,5 @@ 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);
} }