diff --git a/src/main/java/ru/soune/no_copy/controller/ApiController.java b/src/main/java/ru/soune/no_copy/controller/ApiController.java new file mode 100644 index 0000000..0e7a207 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/controller/ApiController.java @@ -0,0 +1,116 @@ +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; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +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 +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class ApiController { + + private final ObjectMapper objectMapper; + + private final UserRepository userRepository; + + private final RegRequestValidator regRequestValidator; + + @Autowired + private AuthService authService; + + @PostMapping("/v{version}/data") + public ResponseEntity handlePostRequest( + @RequestBody BaseRequest request, + @PathVariable("version") int version) { + Integer msgId = request.getMsgId(); + Map response = new HashMap<>(); + + switch (msgId) { + case 20002: + RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); + + if (userRepository.existsByEmail(regRequest.getEmail())) { + throw new UserAlreadyExistsException("User already exists with email: " + regRequest.getEmail(), + msgId, MessageCode.REG_EMAIL_EXISTS.getCode(), MessageCode.REG_EMAIL_EXISTS.getDescription()); + } + + BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); + + regRequestValidator.validate(regRequest, bindingResult); + + if (bindingResult.hasErrors()) { + return createValidationErrorResponse(bindingResult, msgId); + } else { + authService.register(regRequest); + + response = Map.of( + "msg_id", msgId, + "message_code", MessageCode.SUCCESS.getCode(), + "message_desc", MessageCode.SUCCESS.getDescription()); + } + break; + 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.status(MessageCode.SUCCESS.getCode()).body(response); + } + + private ResponseEntity> createValidationErrorResponse( + BindingResult bindingResult, Integer msgId) { + + Map response = new HashMap<>(); + response.put("msg_id", msgId); + response.put("message_code", MessageCode.INVALID_FIELD.getCode()); + response.put("message_desc", MessageCode.INVALID_FIELD.getDescription()); + + List> fieldErrors = bindingResult.getFieldErrors() + .stream() + .map(this::createErrorDetail) + .collect(Collectors.toList()); + + response.put("field_errors", fieldErrors); + + return ResponseEntity.badRequest().body(response); + } + + private Map createErrorDetail(FieldError fieldError) { + Map errorDetail = new HashMap<>(); + errorDetail.put("field", fieldError.getField()); + errorDetail.put("code", fieldError.getCode() != null ? fieldError.getCode() : "VALIDATION_ERROR"); + errorDetail.put("message", fieldError.getDefaultMessage()); + + if (fieldError.getRejectedValue() != null && + !fieldError.getField().toLowerCase().contains("password")) { + errorDetail.put("rejected_value", fieldError.getRejectedValue().toString()); + } + + return errorDetail; + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/no_copy/controller/AuthController.java b/src/main/java/ru/soune/no_copy/controller/AuthController.java index 764b1a8..3edeb6f 100644 --- a/src/main/java/ru/soune/no_copy/controller/AuthController.java +++ b/src/main/java/ru/soune/no_copy/controller/AuthController.java @@ -25,13 +25,13 @@ public class AuthController { private final AuthTokenRepository authTokenRepository; - @PostMapping("/register") - public ResponseEntity register(@Valid @RequestBody RegisterRequest registerRequest) { - AuthToken authToken = authService.register(registerRequest); - - return ResponseEntity.ok(new AuthResponse(true, "success.user.register", authToken.getToken(), - authToken.getExpiresAt())); - } +// @PostMapping("/register") +// public ResponseEntity 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 login(@Valid @RequestBody LoginRequest request) { diff --git a/src/main/java/ru/soune/no_copy/controller/HealtCheckController.java b/src/main/java/ru/soune/no_copy/controller/HealtCheckController.java index 33b6ab9..b455f67 100644 --- a/src/main/java/ru/soune/no_copy/controller/HealtCheckController.java +++ b/src/main/java/ru/soune/no_copy/controller/HealtCheckController.java @@ -5,7 +5,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api") +@RequestMapping("check/api") public class HealtCheckController { @GetMapping("/healt") diff --git a/src/main/java/ru/soune/no_copy/dto/BaseRequest.java b/src/main/java/ru/soune/no_copy/dto/BaseRequest.java new file mode 100644 index 0000000..818890d --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/BaseRequest.java @@ -0,0 +1,16 @@ +package ru.soune.no_copy.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class BaseRequest { + @JsonProperty("version") + Integer version; + + @JsonProperty("msg_id") + Integer msgId; + + @JsonProperty("message_body") + Object messageBody; +} diff --git a/src/main/java/ru/soune/no_copy/dto/BaseResponse.java b/src/main/java/ru/soune/no_copy/dto/BaseResponse.java new file mode 100644 index 0000000..cf16762 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/BaseResponse.java @@ -0,0 +1,21 @@ +package ru.soune.no_copy.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class BaseResponse { + @JsonProperty("msg_id") + private Integer msgId; + + @JsonProperty("message_code") + private Integer messageCode; + + @JsonProperty("message_desc") + private String messageDesc; + + @JsonProperty("message_body") + private Object messageBody; +} diff --git a/src/main/java/ru/soune/no_copy/dto/MessageCode.java b/src/main/java/ru/soune/no_copy/dto/MessageCode.java new file mode 100644 index 0000000..5b4e69b --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/MessageCode.java @@ -0,0 +1,37 @@ +package ru.soune.no_copy.dto; + +public enum MessageCode { + SUCCESS(0, "Operation successful"), + REG_EMAIL_EXISTS(2001, "Email already registered"), + INVALID_FIELD(2002, "Invalid field"), + MSG_ID_NOT_FOUND(2004, "Message id not found"), + + + REGISTER_SUCCESS(10002, "Register success"), + BAD_REQUEST(400, "Bad request"), + UNAUTHORIZED(403, "Unauthorized"), + FORBIDDEN(402, "Forbidden"), + NOT_FOUND(404, "Not found"), + INTERNAL_ERROR(500, "Internal server error"), + + AUTH_INVALID_CREDENTIALS(101, "Invalid credentials"), + AUTH_EMAIL_NOT_CONFIRMED(103, "Email not confirmed"); + + + private final Integer code; + + private final String description; + + MessageCode(Integer code, String description) { + this.code = code; + this.description = description; + } + + public Integer getCode() { + return code; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/ru/soune/no_copy/dto/RegRequest.java b/src/main/java/ru/soune/no_copy/dto/RegRequest.java new file mode 100644 index 0000000..f43cb75 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/RegRequest.java @@ -0,0 +1,25 @@ +package ru.soune.no_copy.dto; + +import jakarta.validation.constraints.*; +import lombok.Data; + + +@Data +public class RegRequest { + @NotEmpty(message = "Full name is required") + private String fullName; + + private String companyName; + + @Size(min = 11, max = 14, message = "Phone must be 11-14 digits") + private String phone; + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + @Size(max = 128, message = "Email too long") + private String email; + + @NotBlank(message = "Password is required") + @Size(min = 8, message = "Password must be at least 8 characters") + private String password; +} diff --git a/src/main/java/ru/soune/no_copy/exception/UserAlreadyExistsException.java b/src/main/java/ru/soune/no_copy/exception/UserAlreadyExistsException.java index 8815885..099770c 100644 --- a/src/main/java/ru/soune/no_copy/exception/UserAlreadyExistsException.java +++ b/src/main/java/ru/soune/no_copy/exception/UserAlreadyExistsException.java @@ -1,7 +1,23 @@ package ru.soune.no_copy.exception; +import lombok.Getter; + +@Getter public class UserAlreadyExistsException extends RuntimeException { - public UserAlreadyExistsException(String message) { + + private final String message; + + private final Integer messageId; + + private final Integer messageCode; + + private final String messageDescription; + + public UserAlreadyExistsException(String message, Integer messageId, Integer messageCode, String messageDescription) { super(message); + this.message = message; + this.messageId = messageId; + this.messageCode = messageCode; + this.messageDescription = messageDescription; } } diff --git a/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java b/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java index c3a02b2..2edd0ff 100644 --- a/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java +++ b/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java @@ -8,8 +8,10 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.soune.no_copy.dto.MessageCode; import ru.soune.no_copy.exception.*; +import java.time.LocalDateTime; import java.util.Map; @RestControllerAdvice @@ -30,14 +32,15 @@ public class GlobalExceptionHandler { } @ExceptionHandler(UserAlreadyExistsException.class) - @ResponseStatus(HttpStatus.CONFLICT) + @ResponseStatus(HttpStatus.OK) public ResponseEntity handleUserContainsException(UserAlreadyExistsException ex) { - return ResponseEntity. - badRequest() + + return ResponseEntity + .ok() .body(Map.of( - "success", false, - "message" ,ex.getMessage() - )); + "msg_id", ex.getMessageId(), + "message_code", ex.getMessageCode(), + "message_desc", ex.getMessageDescription())); } @ExceptionHandler(UserNotFoundException.class) diff --git a/src/main/java/ru/soune/no_copy/handler/RegRequestValidator.java b/src/main/java/ru/soune/no_copy/handler/RegRequestValidator.java new file mode 100644 index 0000000..98cf464 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/handler/RegRequestValidator.java @@ -0,0 +1,45 @@ +package ru.soune.no_copy.handler; + +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import ru.soune.no_copy.dto.RegRequest; + +@Component +public class RegRequestValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return RegRequest.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + RegRequest request = (RegRequest) target; + + validatePhone(request.getPhone(), errors); + } + + private void validatePhone(String phone, Errors errors) { + if (phone == null || phone.trim().isEmpty()) { + errors.rejectValue("phone", "phone.empty", + "Test phone numbers are not allowed"); + return; + } + + String trimmedPhone = phone.trim(); + String digitsOnly = trimmedPhone.replaceAll("[^0-9+]", ""); + + if (digitsOnly.length() < 11 || digitsOnly.length() > 14) { + errors.rejectValue("phone", "phone.invalid.length", + "Phone number must contain 11-14 digits"); + return; + } + + if (!digitsOnly.matches("^(\\+7|8|7)[0-9]{10}$")) { + errors.rejectValue("phone", "phone.invalid.format", + "Invalid phone number format"); + return; + } + } +} diff --git a/src/main/java/ru/soune/no_copy/service/AuthService.java b/src/main/java/ru/soune/no_copy/service/AuthService.java index 1cb1966..3176dd2 100644 --- a/src/main/java/ru/soune/no_copy/service/AuthService.java +++ b/src/main/java/ru/soune/no_copy/service/AuthService.java @@ -6,6 +6,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.soune.no_copy.dto.LoginRequest; +import ru.soune.no_copy.dto.RegRequest; import ru.soune.no_copy.dto.RegisterRequest; import ru.soune.no_copy.entity.AuthToken; import ru.soune.no_copy.entity.User; @@ -35,23 +36,43 @@ public class AuthService { private final SecureRandom secureRandom = new SecureRandom(); +// @Transactional +// public AuthToken register(RegisterRequest registerRequest) { +// User user = new User(); +// user.setFullName(registerRequest.fullName()); +// user.setEmail(registerRequest.email()); +// user.setPassword(passwordEncoder.encode(registerRequest.password())); +// +// if (registerRequest.companyName() != null) { +// user.setCompany(registerRequest.companyName()); +// } +// +// if (registerRequest.phone() != null) { +// user.setPhone(registerRequest.phone()); +// } +// +// User savedUser = userRepository.save(user); +// +// AuthToken authToken = new AuthToken(); +// authToken.setToken(generateAuthToken()); +// authToken.setUser(savedUser); +// +// return authTokenRepository.save(authToken); +// } + @Transactional - public AuthToken register(RegisterRequest registerRequest) { - if (userRepository.existsByEmail(registerRequest.email())) { - throw new UserAlreadyExistsException("User already exists with email: " + registerRequest.email()); - } - + public AuthToken register(RegRequest registerRequest) { User user = new User(); - user.setFullName(registerRequest.fullName()); - user.setEmail(registerRequest.email()); - user.setPassword(passwordEncoder.encode(registerRequest.password())); + user.setFullName(registerRequest.getFullName()); + user.setEmail(registerRequest.getEmail()); + user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); - if (registerRequest.companyName() != null) { - user.setCompany(registerRequest.companyName()); + if (registerRequest.getCompanyName() != null) { + user.setCompany(registerRequest.getCompanyName()); } - if (registerRequest.phone() != null) { - user.setPhone(registerRequest.phone()); + if (registerRequest.getPhone() != null) { + user.setPhone(registerRequest.getPhone()); } User savedUser = userRepository.save(user);