NCBACK-9 #7
@@ -1,7 +1,6 @@
|
||||
package ru.soune.no_copy.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -13,17 +12,14 @@ import org.springframework.web.bind.annotation.*;
|
||||
import ru.soune.no_copy.dto.BaseRequest;
|
||||
import ru.soune.no_copy.dto.MessageCode;
|
||||
import ru.soune.no_copy.dto.RegRequest;
|
||||
import ru.soune.no_copy.entity.AuthToken;
|
||||
import ru.soune.no_copy.exception.UserAlreadyExistsException;
|
||||
import ru.soune.no_copy.handler.RegRequestValidator;
|
||||
import ru.soune.no_copy.repository.UserRepository;
|
||||
import ru.soune.no_copy.service.AuthService;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@@ -42,11 +38,9 @@ public class ApiController {
|
||||
private AuthService authService;
|
||||
|
||||
@PostMapping("/v{version}/data")
|
||||
public ResponseEntity<?> handlePostRequest(
|
||||
@RequestBody BaseRequest request,
|
||||
@PathVariable("version") int version) {
|
||||
public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request, @PathVariable("version") int version) {
|
||||
Integer msgId = request.getMsgId();
|
||||
Map<String, ? extends Serializable> response = new HashMap<>();
|
||||
Map<String, Object> response;
|
||||
|
||||
switch (msgId) {
|
||||
case 20002:
|
||||
@@ -65,30 +59,24 @@ public class ApiController {
|
||||
return createValidationErrorResponse(bindingResult, msgId);
|
||||
} else {
|
||||
authService.register(regRequest);
|
||||
|
||||
response = Map.of(
|
||||
"msg_id", msgId,
|
||||
"message_code", MessageCode.SUCCESS.getCode(),
|
||||
"message_desc", MessageCode.SUCCESS.getDescription());
|
||||
response = fillResponseInfo(msgId, MessageCode.SUCCESS.getCode(),
|
||||
MessageCode.SUCCESS.getDescription());
|
||||
}
|
||||
break;
|
||||
case 20001:
|
||||
default:
|
||||
return ResponseEntity.ok().body(Map.of(
|
||||
"msg_id", msgId,
|
||||
"message_code", MessageCode.MSG_ID_NOT_FOUND.getCode(),
|
||||
"message_desc", MessageCode.MSG_ID_NOT_FOUND.getDescription()));
|
||||
return ResponseEntity.ok().body(fillResponseInfo(msgId, MessageCode.MSG_ID_NOT_FOUND.getCode(),
|
||||
MessageCode.MSG_ID_NOT_FOUND.getDescription()));
|
||||
}
|
||||
|
||||
return ResponseEntity.status(MessageCode.SUCCESS.getCode()).body(response);
|
||||
return ResponseEntity.ok().body(response);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> createValidationErrorResponse(
|
||||
BindingResult bindingResult, Integer msgId) {
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("msg_id", msgId);
|
||||
response.put("message_code", MessageCode.INVALID_FIELD.getCode());
|
||||
response.put("message_desc", MessageCode.INVALID_FIELD.getDescription());
|
||||
Map<String, Object> response = fillResponseInfo(msgId, MessageCode.INVALID_FIELD.getCode(),
|
||||
MessageCode.INVALID_FIELD.getDescription());
|
||||
|
||||
List<Map<String, String>> fieldErrors = bindingResult.getFieldErrors()
|
||||
.stream()
|
||||
@@ -113,4 +101,13 @@ public class ApiController {
|
||||
|
||||
return errorDetail;
|
||||
}
|
||||
|
||||
private Map<String, Object> fillResponseInfo(Integer msgId, Integer messageCode, String messageDesc) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("msg_id", msgId);
|
||||
response.put("message_code", messageCode);
|
||||
response.put("message_desc", messageDesc);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,6 @@ public class AuthController {
|
||||
|
||||
private final AuthTokenRepository authTokenRepository;
|
||||
|
||||
// @PostMapping("/register")
|
||||
// public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest registerRequest) {
|
||||
// AuthToken authToken = authService.register(registerRequest);
|
||||
//
|
||||
// return ResponseEntity.ok(new AuthResponse(true, "success.user.register", authToken.getToken(),
|
||||
// authToken.getExpiresAt()));
|
||||
// }
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
AuthToken login = authService.login(request);
|
||||
|
||||
@@ -5,8 +5,13 @@ import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.Validator;
|
||||
import ru.soune.no_copy.dto.RegRequest;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Component
|
||||
public class RegRequestValidator implements Validator {
|
||||
private static final String COMPANY_REGEX = "^[a-zA-Zа-яА-ЯёЁ0-9\\s\\-&.,'()]{0,200}$";
|
||||
|
||||
private static final String NAME_REGEX = "^[a-zA-Zа-яА-ЯёЁ\\s\\-'.]{2,100}$";
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> clazz) {
|
||||
@@ -18,6 +23,150 @@ public class RegRequestValidator implements Validator {
|
||||
RegRequest request = (RegRequest) target;
|
||||
|
||||
validatePhone(request.getPhone(), errors);
|
||||
validatePassword(request.getPassword(), errors);
|
||||
validateEmail(request.getEmail(), errors);
|
||||
validateCompanyName(request.getCompanyName(), errors);
|
||||
validateFullName(request.getFullName(), errors);
|
||||
}
|
||||
|
||||
private void validateFullName(String fullName, Errors errors) {
|
||||
if (fullName == null || fullName.trim().isEmpty()) {
|
||||
errors.rejectValue("fullName", "fullName.required",
|
||||
"Full name is required");
|
||||
return;
|
||||
}
|
||||
|
||||
String trimmedName = fullName.trim();
|
||||
|
||||
if (trimmedName.length() < 2) {
|
||||
errors.rejectValue("fullName", "fullName.too.short",
|
||||
"Full name must be at least 2 characters");
|
||||
}
|
||||
|
||||
if (trimmedName.length() > 100) {
|
||||
errors.rejectValue("fullName", "fullName.too.long",
|
||||
"Full name must be less than 100 characters");
|
||||
}
|
||||
|
||||
if (!trimmedName.matches(NAME_REGEX)) {
|
||||
errors.rejectValue("fullName", "fullName.invalid.chars",
|
||||
"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) {
|
||||
if (companyName == null || companyName.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String trimmedCompany = companyName.trim();
|
||||
|
||||
if (trimmedCompany.length() > 200) {
|
||||
errors.rejectValue("companyName", "companyName.too.long",
|
||||
"Company name must be less than 200 characters");
|
||||
}
|
||||
|
||||
if (!trimmedCompany.matches(COMPANY_REGEX)) {
|
||||
errors.rejectValue("companyName", "companyName.invalid.chars",
|
||||
"Company name contains invalid characters");
|
||||
}
|
||||
|
||||
validateForbiddenWords(trimmedCompany, "companyName", errors,
|
||||
Arrays.asList("admin", "root", "system", "test"));
|
||||
}
|
||||
|
||||
private void validateForbiddenWords(String text, String fieldName,
|
||||
Errors errors, List<String> forbiddenWords) {
|
||||
String lowerText = text.toLowerCase();
|
||||
for (String word : forbiddenWords) {
|
||||
if (lowerText.contains(word)) {
|
||||
errors.rejectValue(fieldName, fieldName + ".forbidden.word",
|
||||
"Contains forbidden word: " + word);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateEmail(String email, Errors errors) {
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
errors.rejectValue("email", "email.is.empty",
|
||||
"Email must not be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
String trimmedEmail = email.trim().toLowerCase();
|
||||
|
||||
if (trimmedEmail.length() > 128) {
|
||||
errors.rejectValue("email", "email.too.long",
|
||||
"Email must be less than 128 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trimmedEmail.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) {
|
||||
errors.rejectValue("email", "email.invalid.format",
|
||||
"Invalid email format");
|
||||
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) {
|
||||
@@ -42,4 +191,92 @@ public class RegRequestValidator implements Validator {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePassword(String password, Errors errors) {
|
||||
if (password == null || password.isEmpty()) {
|
||||
errors.rejectValue("password", "password.empty",
|
||||
"Password are not allowed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length() < 8) {
|
||||
errors.rejectValue("password", "password.too.short",
|
||||
"Password must be at least 8 characters");
|
||||
}
|
||||
|
||||
if (password.length() > 50) {
|
||||
errors.rejectValue("password", "password.too.long",
|
||||
"Password must be less than 50 characters");
|
||||
}
|
||||
|
||||
if (password.contains(" ")) {
|
||||
errors.rejectValue("password", "password.contains.spaces",
|
||||
"Password cannot contain spaces");
|
||||
}
|
||||
|
||||
// recomment if need complexity
|
||||
// checkPasswordComplexity(password, errors);
|
||||
|
||||
// reccoment if need check simply standart password
|
||||
// checkCommonPasswords(password, errors);
|
||||
|
||||
// reccoment if need check simply standart password
|
||||
// checkForSequences(password, errors);
|
||||
}
|
||||
|
||||
private void checkPasswordComplexity(String password, Errors errors) {
|
||||
boolean hasUpper = false;
|
||||
boolean hasLower = false;
|
||||
boolean hasDigit = false;
|
||||
boolean hasSpecial = false;
|
||||
|
||||
for (char c : password.toCharArray()) {
|
||||
if (Character.isUpperCase(c)) hasUpper = true;
|
||||
if (Character.isLowerCase(c)) hasLower = true;
|
||||
if (Character.isDigit(c)) hasDigit = true;
|
||||
if ("!@#$%^&*()_+-=[]{}|;:,.<>?".indexOf(c) >= 0) hasSpecial = true;
|
||||
}
|
||||
|
||||
List<String> missing = new ArrayList<>();
|
||||
if (!hasUpper) missing.add("uppercase letter");
|
||||
if (!hasLower) missing.add("lowercase letter");
|
||||
if (!hasDigit) missing.add("digit");
|
||||
if (!hasSpecial) missing.add("special character");
|
||||
|
||||
if (!missing.isEmpty()) {
|
||||
String message = "Password must contain: " + String.join(", ", missing);
|
||||
errors.rejectValue("password", "password.complexity.missing", message);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkCommonPasswords(String password, Errors errors) {
|
||||
Set<String> commonPasswords = Set.of(
|
||||
"password", "12345678", "qwerty123", "admin123",
|
||||
"welcome1", "password1", "123456789", "qwertyuiop"
|
||||
);
|
||||
|
||||
if (commonPasswords.contains(password.toLowerCase())) {
|
||||
errors.rejectValue("password", "password.too.common",
|
||||
"This password is too common, please choose another");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkForSequences(String password, Errors errors) {
|
||||
String lower = password.toLowerCase();
|
||||
|
||||
if (lower.matches(".*123456.*") || lower.matches(".*987654.*")) {
|
||||
errors.rejectValue("password", "password.sequence.numbers",
|
||||
"Password contains predictable number sequence");
|
||||
}
|
||||
|
||||
if (lower.matches(".*abcdef.*") || lower.matches(".*qwerty.*")) {
|
||||
errors.rejectValue("password", "password.sequence.letters",
|
||||
"Password contains predictable letter sequence");
|
||||
}
|
||||
|
||||
if (lower.matches(".*(.)\\1{3,}.*")) {
|
||||
errors.rejectValue("password", "password.repeating.chars",
|
||||
"Password contains too many repeating characters");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user