diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/build.gradle b/build.gradle index 4e63b11..c6cb590 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.security:spring-security-crypto:6.5.3' + implementation 'org.mapstruct:mapstruct:1.5.5.Final' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc' diff --git a/docker-compose.yaml b/docker-compose.yaml index 0ed4d65..13e1b65 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,11 +13,14 @@ services: volumes: - pgdata:/var/lib/postgresql/data container_name: postgres + networks: + app-network: + aliases: + - database app: - image: popovtsev/ncp -# build: . - container_name: no_copy_app + build: . + container_name: app-backend environment: POSTGRES_DB: no_copy_ POSTGRES_USER: postgres @@ -27,7 +30,13 @@ services: depends_on: - db ports: - - "8080:8080" + - "80:8080" + networks: + app-network: + aliases: + - app + - backend + - api grafana: image: grafana/grafana:10.3.1 @@ -44,7 +53,7 @@ services: alloy: condition: service_started environment: - GF_AUTH_ANONYMOUS_ENABLED: true + GF_AUTH_ANONYMOUS_ENABLED: "true" GF_SECURITY_ADMIN_USER: admin GF_SECURITY_ADMIN_PASSWORD: admin GF_METRICS_ENABLED: "true" @@ -56,6 +65,8 @@ services: interval: 10s timeout: 5s retries: 10 + networks: + - app-network prometheus: image: prom/prometheus:latest @@ -69,6 +80,8 @@ services: interval: 10s timeout: 5s retries: 5 + networks: + - app-network loki: image: grafana/loki:2.9.2 @@ -82,6 +95,8 @@ services: interval: 10s timeout: 5s retries: 5 + networks: + - app-network tempo: image: grafana/tempo:2.4.1 @@ -92,15 +107,17 @@ services: - tempo_data:/var/tempo ports: - "3200:3200" + networks: + - app-network alloy: image: grafana/alloy:latest container_name: alloy user: root ports: - - "9080:9080" # HTTP interface - - "4317:4317" # OTLP gRPC - - "4318:4318" # OTLP http + - "9080:9080" + - "4317:4317" + - "4318:4318" volumes: - ./infrastructure/alloy/config.alloy:/etc/alloy/config.alloy:ro - /var/lib/docker/containers:/var/lib/docker/containers:ro @@ -113,6 +130,8 @@ services: - --server.http.listen-addr=0.0.0.0:9080 - --storage.path=/var/lib/alloy/data - /etc/alloy/config.alloy + networks: + - app-network volumes: pgdata: @@ -121,4 +140,9 @@ volumes: loki_data: loki_chunks: loki_index: - loki_rules: \ No newline at end of file + loki_rules: + +networks: + app-network: + external: true + driver: bridge \ No newline at end of file diff --git a/infrastructure/jenkins/deploy-front.groovy b/infrastructure/jenkins/deploy-front.groovy new file mode 100644 index 0000000..e0a2094 --- /dev/null +++ b/infrastructure/jenkins/deploy-front.groovy @@ -0,0 +1,183 @@ +pipeline { + agent any + + parameters { + string( + name: 'BRANCH', + defaultValue: 'main', + description: 'Ветка для деплоя' + ) + } + + stages { + stage('Git pull') { + steps { + script { + checkout([ + $class: 'GitSCM', + branches: [[name: params.BRANCH]], + userRemoteConfigs: [[ + url: 'https://code.3err0.ru/frontdev/no-copy-frontend.git', + credentialsId: 'nx-jen' + ]] + ]) + } + } + } + + stage('Stop old') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " + docker stop no-copy-frontend 2>/dev/null || true + docker rm no-copy-frontend 2>/dev/null || true + + cd /opt/deployments/frontend/${params.BRANCH} 2>/dev/null || mkdir -p /opt/deployments/frontend/${params.BRANCH} + + if [ -f 'docker-compose.yaml' ]; then + docker-compose -f docker-compose.yaml down 2>/dev/null || true + elif [ -f 'docker-compose.yml' ]; then + docker-compose -f docker-compose.yml down 2>/dev/null || true + fi + " + """ + } + } + } + } + + stage('Copy to server') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " + rm -rf /opt/deployments/frontend/${params.BRANCH}/* + mkdir -p /opt/deployments/frontend/${params.BRANCH} + " + + sshpass -p '$SSH_PASS' scp -r ./* $SSH_USER@92.242.61.23:/opt/deployments/frontend/${params.BRANCH}/ + """ + } + } + } + } + + stage('Build on server') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " + cd /opt/deployments/frontend/${params.BRANCH} + + if [ -f 'docker-compose.yaml' ]; then + docker-compose -f docker-compose.yaml build + elif [ -f 'docker-compose.yml' ]; then + docker-compose -f docker-compose.yml build + else + echo 'Exception:don't found docker-compose' + exit 1 + fi + " + """ + } + } + } + } + + stage('Start on server') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " + cd /opt/deployments/frontend/${params.BRANCH} + + if [ -f 'docker-compose.yaml' ]; then + docker-compose -f docker-compose.yaml up -d + elif [ -f 'docker-compose.yml' ]; then + docker-compose -f docker-compose.yml up -d + fi + + sleep 10 + + echo 'Status container:' + docker ps --filter name=no-copy-frontend + " + """ + } + } + } + } + + stage('Check status') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " + if docker ps --format '{{.Names}}' | grep -q 'no-copy-frontend'; then + echo 'Container start' + + sleep 5 + + HTTP_CODE=\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:2998 || echo '000') + echo 'HTTP код: ' \$HTTP_CODE + + echo 'Frong start on http://92.242.61.23:2998' + else + echo 'Exception: container don't start' + docker ps -a | grep frontend + exit 1 + fi + " + """ + } + } + } + } + } + + post { + success { + echo "Front branch ${params.BRANCH} completed" + echo "Use on http://92.242.61.23:2998" + } + failure { + echo "Failed" + } + } +} \ No newline at end of file diff --git a/infrastructure/jenkins/deploy.groovy b/infrastructure/jenkins/deploy.groovy new file mode 100644 index 0000000..c14fb90 --- /dev/null +++ b/infrastructure/jenkins/deploy.groovy @@ -0,0 +1,218 @@ +pipeline { + agent any + + parameters { + string( + name: 'BRANCH', + defaultValue: 'dev', + description: 'Ветка для деплоя' + ) + + string( + name: 'SERVER', + defaultValue: '92.242.61.23', + description: 'Сервер для деплоя' + ) + } + + stages { + stage('Git pull') { + steps { + script { + checkout([ + $class: 'GitSCM', + branches: [[name: params.BRANCH]], + userRemoteConfigs: [[ + url: 'https://code.3err0.ru/backdev/no-copy.git', + credentialsId: 'nx-jen' + ]] + ]) + } + } + } + + stage('Stop old container') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + echo "Stop old container by branch ${params.BRANCH}..." + + sshpass -p '$SSH_PASS' ssh $SSH_USER@$SERVER " + cd /opt/deployments/${params.BRANCH} 2>/dev/null || mkdir -p /opt/deployments/${params.BRANCH} + + # Stop old container if have docker-compose + if [ -f 'docker-compose.yaml' ] || [ -f 'docker-compose.yml' ]; then + if [ -f 'docker-compose.yaml' ]; then + docker-compose -f docker-compose.yaml down 2>/dev/null || true + elif [ -f 'docker-compose.yml' ]; then + docker-compose -f docker-compose.yml down 2>/dev/null || true + fi + fi + + # Stop and delete container by name + docker stop app-${params.BRANCH} 2>/dev/null || true + docker rm app-${params.BRANCH} 2>/dev/null || true + + echo "Old containder deleted" + " + """ + } + } + } + } + + stage('Copy to server') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + echo "Copy files on server..." + + # Clean mkdir + sshpass -p '$SSH_PASS' ssh $SSH_USER@$SERVER " + rm -rf /opt/deployments/${params.BRANCH}/* + mkdir -p /opt/deployments/${params.BRANCH} + " + + # Copy all files + sshpass -p '$SSH_PASS' scp -r ./* $SSH_USER@$SERVER:/opt/deployments/${params.BRANCH}/ 2>/dev/null || true + + echo "Files copied" + """ + } + } + } + } + + stage('Build on server') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + echo "Build on server..." + + sshpass -p '$SSH_PASS' ssh $SSH_USER@$SERVER " + cd /opt/deployments/${params.BRANCH} + + echo "Build Docker for branch ${params.BRANCH}..." + + # delete old docker image + docker rmi app:${params.BRANCH} 2>/dev/null || true + + # build from branch + if [ -f 'Dockerfile' ]; then + docker build -t app:${params.BRANCH} . + echo "Docker образ app:${params.BRANCH} собран" + fi + + # or from docker-compose + if [ -f 'docker-compose.yaml' ]; then + docker-compose -f docker-compose.yaml build + echo "Build from docker-compose.yaml completed" + elif [ -f 'docker-compose.yml' ]; then + docker-compose -f docker-compose.yml build + echo "Build from docker-compose.yml completed" + fi + + echo 'Build completed' + " + """ + } + } + } + } + + stage('Start on server') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + echo "Start container..." + + sshpass -p '$SSH_PASS' ssh $SSH_USER@$SERVER " + cd /opt/deployments/${params.BRANCH} + + # If have docker-compose - use them + if [ -f 'docker-compose.yaml' ]; then + echo "Start docker-compose.yaml..." + docker-compose -f docker-compose.yaml up -d + + elif [ -f 'docker-compose.yml' ]; then + echo "Start docker-compose.yml..." + docker-compose -f docker-compose.yml up -d + + # If have Dockerfile - start + elif [ -f 'Dockerfile' ]; then + echo "Start app-${params.BRANCH}..." + docker run -d \\ + --name app-${params.BRANCH} \\ + -p 8080:8080 \\ + app:${params.BRANCH} + else + echo "Error:Dockerfile NOT FOUND, docker-compose.yaml or docker-compose.yml" + exit 1 + fi + + echo 'Container start' + + # Wait start + sleep 10 + + # Check status + echo 'status container:' + if [ -f 'docker-compose.yaml' ] || [ -f 'docker-compose.yml' ]; then + if [ -f 'docker-compose.yaml' ]; then + docker-compose -f docker-compose.yaml ps + else + docker-compose -f docker-compose.yml ps + fi + else + docker ps --filter "name=app-${params.BRANCH}" + fi + + echo '' + echo 'Apps start on:' + echo 'http://$SERVER:80' + " + """ + } + } + } + } + } + + post { + success { + echo "Deploy branch ${params.BRANCH} completed" + echo "App : http://$SERVER:80" + } + failure { + echo "Deploy failed" + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/no_copy/configuration/JacksonConfig.java b/src/main/java/ru/soune/no_copy/configuration/JacksonConfig.java new file mode 100644 index 0000000..0f726fb --- /dev/null +++ b/src/main/java/ru/soune/no_copy/configuration/JacksonConfig.java @@ -0,0 +1,41 @@ +package ru.soune.no_copy.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Configuration +public class JacksonConfig { + + @Bean + @Primary + public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + + JavaTimeModule javaTimeModule = new JavaTimeModule(); + + javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer( + DateTimeFormatter.ofPattern("dd-MM-yyyy"))); + + javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer( + DateTimeFormatter.ofPattern("dd-MM-yyyy"))); + + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer( + DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss"))); + + mapper.registerModule(javaTimeModule); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + return mapper; + } +} diff --git a/src/main/java/ru/soune/no_copy/configuration/SubscriptionConfig.java b/src/main/java/ru/soune/no_copy/configuration/SubscriptionConfig.java new file mode 100644 index 0000000..7eb41ec --- /dev/null +++ b/src/main/java/ru/soune/no_copy/configuration/SubscriptionConfig.java @@ -0,0 +1,35 @@ +package ru.soune.no_copy.configuration; + +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +@ConfigurationProperties(prefix = "subscription") +@Getter +public class SubscriptionConfig { + + private final Map limits = Map.of( + SubscriptionType.START, new SubscriptionLimits(50, 5.0), + SubscriptionType.BASIC, new SubscriptionLimits(200, 20.0), + SubscriptionType.PRO, new SubscriptionLimits(1000, 100.0), + SubscriptionType.ENTERPRISE, new SubscriptionLimits(-1, 500.0) + ); + + @Getter + public static class SubscriptionLimits { + private final int monthlyFiles; + private final double storageGB; + + public SubscriptionLimits(int monthlyFiles, double storageGB) { + this.monthlyFiles = monthlyFiles; + this.storageGB = storageGB; + } + } + + public enum SubscriptionType { + START, BASIC, PRO, ENTERPRISE + } +} 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 03ee445..764b1a8 100644 --- a/src/main/java/ru/soune/no_copy/controller/AuthController.java +++ b/src/main/java/ru/soune/no_copy/controller/AuthController.java @@ -9,17 +9,22 @@ import ru.soune.no_copy.dto.LoginRequest; import ru.soune.no_copy.dto.LoginResponse; import ru.soune.no_copy.dto.RegisterRequest; import ru.soune.no_copy.entity.AuthToken; +import ru.soune.no_copy.exception.TokenNotFoundException; +import ru.soune.no_copy.repository.AuthTokenRepository; import ru.soune.no_copy.service.AuthService; import java.util.Map; +import java.util.Optional; @RestController -@RequestMapping("/api/auth") +@RequestMapping("v1/api/auth") @RequiredArgsConstructor public class AuthController { private final AuthService authService; + private final AuthTokenRepository authTokenRepository; + @PostMapping("/register") public ResponseEntity register(@Valid @RequestBody RegisterRequest registerRequest) { AuthToken authToken = authService.register(registerRequest); @@ -40,6 +45,12 @@ public class AuthController { public ResponseEntity logout(@RequestHeader("Authorization") String tokenHeader) { String token = tokenHeader.replace("Bearer ", ""); + Optional opToken = authTokenRepository.findByToken(token); + + if (opToken.isEmpty()) { + throw new TokenNotFoundException("Token not found"); + } + authService.logout(token); return ResponseEntity.ok(Map.of("success", true)); diff --git a/src/main/java/ru/soune/no_copy/controller/UserContentController.java b/src/main/java/ru/soune/no_copy/controller/UserContentController.java new file mode 100644 index 0000000..ea25e8f --- /dev/null +++ b/src/main/java/ru/soune/no_copy/controller/UserContentController.java @@ -0,0 +1,11 @@ +package ru.soune.no_copy.controller; + +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("v1/api/content") +@AllArgsConstructor +public class UserContentController { +} diff --git a/src/main/java/ru/soune/no_copy/controller/UserController.java b/src/main/java/ru/soune/no_copy/controller/UserController.java index db4bdd4..9f23605 100644 --- a/src/main/java/ru/soune/no_copy/controller/UserController.java +++ b/src/main/java/ru/soune/no_copy/controller/UserController.java @@ -2,27 +2,110 @@ package ru.soune.no_copy.controller; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import ru.soune.no_copy.dto.ChangePasswordRequest; import ru.soune.no_copy.dto.UserDTO; +import ru.soune.no_copy.dto.UserRequest; +import ru.soune.no_copy.entity.AuthToken; +import ru.soune.no_copy.entity.User; +import ru.soune.no_copy.exception.InvalidUserEmail; +import ru.soune.no_copy.exception.NotFoundAuthToken; +import ru.soune.no_copy.mapper.UserMapper; +import ru.soune.no_copy.repository.AuthTokenRepository; import ru.soune.no_copy.repository.UserRepository; +import ru.soune.no_copy.service.UserService; import java.util.List; +import java.util.Optional; @RestController -@RequestMapping("/api/user") +@RequestMapping("v1/api/user") @RequiredArgsConstructor public class UserController { private final UserRepository userRepository; + private final UserService userService; + + private final AuthTokenRepository authTokenRepository; + + private final UserMapper userMapper; + @GetMapping("/all") public ResponseEntity> getAllUsers() { List allUsers = userRepository.findAll().stream() - .map(u -> new UserDTO(u.getFirstName(), u.getEmail(), u.getIsActive())) + .map(u -> new UserDTO(u.getFullName(), u.getCompany(), u.getEmail(), u.getIsActive(), + u.getPhone(), u.getGenderType(), + u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType())) .toList(); return ResponseEntity.ok(allUsers); } + + @GetMapping + public ResponseEntity getUser(@RequestParam("email") String email, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + if (authToken != null) { + Optional user = userRepository.findByEmail(email); + //TODO add mapper + if (user.isPresent()) { + UserDTO userDTO = userMapper.toDTO(user.get()); + userDTO.setEmail(email); + userDTO.setFullName(user.get().getFullName()); + userDTO.setCompany(user.get().getCompany()); + userDTO.setPhone(user.get().getPhone()); + userDTO.setGenderType(user.get().getGenderType()); + userDTO.setBirthday(user.get().getBirthday()); + userDTO.setCreatedAt(user.get().getCreatedAt()); + userDTO.setSubscriptionType(user.get().getSubscriptionType()); + userDTO.setActive(user.get().getIsActive()); + + return ResponseEntity.ok(userDTO); + } + + } + + return ResponseEntity.notFound().build(); + } + + //TODO fix mapper,doesnot exist all fields + @PostMapping("/change-password") + public ResponseEntity updateUser(@RequestBody ChangePasswordRequest changePasswordRequest, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + if (!changePasswordRequest.getEmail().equals(user.getEmail())) { + throw new InvalidUserEmail("Email is not valid: " + changePasswordRequest.getEmail() + "not found"); + } + + User updateUser = userService.changePassword(user, changePasswordRequest); + + return ResponseEntity.ok(userMapper.toDTO(updateUser)); + } + + @PatchMapping("/user-update") + public ResponseEntity updateUser(@RequestBody UserRequest userRequest, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + return ResponseEntity.ok(userService.updateUser(userRequest, user)); + } } diff --git a/src/main/java/ru/soune/no_copy/dto/AuthResponse.java b/src/main/java/ru/soune/no_copy/dto/AuthResponse.java index 8e76c1a..c39301a 100644 --- a/src/main/java/ru/soune/no_copy/dto/AuthResponse.java +++ b/src/main/java/ru/soune/no_copy/dto/AuthResponse.java @@ -2,4 +2,9 @@ package ru.soune.no_copy.dto; import java.time.LocalDateTime; -public record AuthResponse (boolean success, String message, String token, LocalDateTime expiresAt) {} +public record AuthResponse ( + boolean success, + String message, + String token, + LocalDateTime expiresAt +) {} diff --git a/src/main/java/ru/soune/no_copy/dto/ChangePasswordRequest.java b/src/main/java/ru/soune/no_copy/dto/ChangePasswordRequest.java new file mode 100644 index 0000000..aff7140 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/ChangePasswordRequest.java @@ -0,0 +1,14 @@ +package ru.soune.no_copy.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; + +@Data +@AllArgsConstructor +@Getter +public class ChangePasswordRequest { + private String email; + private String currentPassword; + private String newPassword; +} diff --git a/src/main/java/ru/soune/no_copy/dto/LoginRequest.java b/src/main/java/ru/soune/no_copy/dto/LoginRequest.java index 47602d2..c85fe97 100644 --- a/src/main/java/ru/soune/no_copy/dto/LoginRequest.java +++ b/src/main/java/ru/soune/no_copy/dto/LoginRequest.java @@ -5,9 +5,11 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data @AllArgsConstructor +@NoArgsConstructor public class LoginRequest { @NotBlank(message = "error.not.blank") @Email(message = "error.not.email") @Size(max = 128) diff --git a/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java b/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java index bdf67e9..e08f39b 100644 --- a/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java +++ b/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java @@ -1,13 +1,40 @@ package ru.soune.no_copy.dto; +import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Past; import jakarta.validation.constraints.Size; +import org.springframework.format.annotation.DateTimeFormat; +import ru.soune.no_copy.entity.GenderType; +import ru.soune.no_copy.entity.SubscriptionType; + +import java.time.LocalDate; public record RegisterRequest( - @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String firstName, - @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String secondName, - @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String lastName, - @NotBlank(message = "error.not.blank") @Email(message = "error.not.email") @Size(max = 128) String email, - @NotBlank(message = "error.not.blank") @Size(min = 8) String password + @NotBlank(message = "error.name.length") + String fullName, + + String companyName, + + @Size(min = 11, max = 14) + String phone, + + @NotBlank(message = "error.not.blank") + @Email(message = "error.not.email") + @Size(max = 128) + String email, + + @DateTimeFormat(pattern = "dd-MM-yyyy") + @JsonFormat(pattern = "dd-MM-yyyy") + @Past + LocalDate birthday, + + @NotBlank(message = "error.not.blank") + @Size(min = 8) + String password, + + SubscriptionType subscriptionType, + + GenderType genderType ) {} diff --git a/src/main/java/ru/soune/no_copy/dto/UserContentDTO.java b/src/main/java/ru/soune/no_copy/dto/UserContentDTO.java new file mode 100644 index 0000000..90df1d9 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/UserContentDTO.java @@ -0,0 +1,15 @@ +package ru.soune.no_copy.dto; + +import lombok.Data; +import ru.soune.no_copy.entity.FileType; + +@Data +public class UserContentDTO { + String fileName; + String originalFilename; + String fileType; + String fileExtension; + Long fileSize; + String filePath; + Integer protectionLevel; +} diff --git a/src/main/java/ru/soune/no_copy/dto/UserContentRequest.java b/src/main/java/ru/soune/no_copy/dto/UserContentRequest.java new file mode 100644 index 0000000..06ef01e --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/UserContentRequest.java @@ -0,0 +1,15 @@ +package ru.soune.no_copy.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record UserContentRequest( + @NotBlank @Email String userEmail, + @NotBlank String fileName, + @NotBlank String originalFilename, + @NotBlank String fileType, + @NotBlank String fileExtension, + @NotBlank Long fileSize, + @NotBlank String filePath, + Integer protectionLevel +) {} diff --git a/src/main/java/ru/soune/no_copy/dto/UserContentResponse.java b/src/main/java/ru/soune/no_copy/dto/UserContentResponse.java new file mode 100644 index 0000000..86d1d84 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/UserContentResponse.java @@ -0,0 +1,17 @@ +package ru.soune.no_copy.dto; + +import ru.soune.no_copy.entity.ContentStatus; +import ru.soune.no_copy.entity.FileType; + +import java.time.LocalDateTime; + +public record UserContentResponse( + Long id, + String filename, + String originalFilename, + FileType fileType, + long fileSize, + String filePath, + ContentStatus status, + LocalDateTime uploadDate +) {} diff --git a/src/main/java/ru/soune/no_copy/dto/UserContentUpdateRequest.java b/src/main/java/ru/soune/no_copy/dto/UserContentUpdateRequest.java new file mode 100644 index 0000000..8955969 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/UserContentUpdateRequest.java @@ -0,0 +1,16 @@ +package ru.soune.no_copy.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record UserContentUpdateRequest( + @NotBlank @Email String userEmail, + @NotBlank String fileName, + @NotBlank String originalFilename, + @NotBlank String fileType, + @NotBlank String fileExtension, + @NotBlank Long fileSize, + @NotBlank String filePath, + Integer protectionLevel, + Long id +) {} diff --git a/src/main/java/ru/soune/no_copy/dto/UserDTO.java b/src/main/java/ru/soune/no_copy/dto/UserDTO.java index ebe4f63..ba5b278 100644 --- a/src/main/java/ru/soune/no_copy/dto/UserDTO.java +++ b/src/main/java/ru/soune/no_copy/dto/UserDTO.java @@ -1,12 +1,27 @@ package ru.soune.no_copy.dto; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; +import ru.soune.no_copy.entity.GenderType; +import ru.soune.no_copy.entity.SubscriptionType; + +import java.time.LocalDate; +import java.time.LocalDateTime; @Data @AllArgsConstructor +@NoArgsConstructor +@Builder public class UserDTO { private String fullName; + private String company; private String email; - private boolean isActive; + private boolean active; + private String phone; + private GenderType genderType; + private LocalDate birthday; + private LocalDateTime createdAt; + private SubscriptionType subscriptionType; } diff --git a/src/main/java/ru/soune/no_copy/dto/UserRequest.java b/src/main/java/ru/soune/no_copy/dto/UserRequest.java new file mode 100644 index 0000000..db8c8fe --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/UserRequest.java @@ -0,0 +1,25 @@ +package ru.soune.no_copy.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.Size; +import lombok.Data; +import ru.soune.no_copy.entity.GenderType; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +public class UserRequest { + @Size(min = 2) + private String fullName; + private String company; + @Email + private String email; + private String phone; + private GenderType genderType; + @Past + @JsonFormat(pattern = "dd-MM-yyyy") + private LocalDate birthday; +} diff --git a/src/main/java/ru/soune/no_copy/entity/AuthToken.java b/src/main/java/ru/soune/no_copy/entity/AuthToken.java index ee78fe9..6418073 100644 --- a/src/main/java/ru/soune/no_copy/entity/AuthToken.java +++ b/src/main/java/ru/soune/no_copy/entity/AuthToken.java @@ -1,25 +1,30 @@ package ru.soune.no_copy.entity; import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; @Entity -@NoArgsConstructor +@Table(name = "auth_tokens") @Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString @EntityListeners(AuditingEntityListener.class) -@Table(name = "auth_tokens") public class AuthToken { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private Long tokenId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @ToString.Exclude + private User user; @Column(nullable = false, unique = true, length = 64) private String token; @@ -31,7 +36,13 @@ public class AuthToken { @Column(name = "created_at", updatable = false, nullable = false) private LocalDateTime createdAt; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; + @Column(name = "last_used_at") + private LocalDateTime lastUsedAt; + + @Column(name = "is_active") + private Boolean isActive = true; + + public boolean isValid() { + return Boolean.TRUE.equals(isActive) && expiresAt.isAfter(LocalDateTime.now()); + } } diff --git a/src/main/java/ru/soune/no_copy/entity/BaseEntity.java b/src/main/java/ru/soune/no_copy/entity/BaseEntity.java new file mode 100644 index 0000000..b51c525 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/BaseEntity.java @@ -0,0 +1,29 @@ +package ru.soune.no_copy.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +@Setter +public abstract class BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} diff --git a/src/main/java/ru/soune/no_copy/entity/ContentStatus.java b/src/main/java/ru/soune/no_copy/entity/ContentStatus.java new file mode 100644 index 0000000..d97f66c --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/ContentStatus.java @@ -0,0 +1,20 @@ +package ru.soune.no_copy.entity; + +import lombok.Getter; + +@Getter +public enum ContentStatus { + PROCESSING("processing"), + COMPLETED("completed"), + ERROR("error"); + + private final String code; + + ContentStatus(String code) { + this.code = code; + } + + public String getCode() { + return code; + } +} diff --git a/src/main/java/ru/soune/no_copy/entity/FileType.java b/src/main/java/ru/soune/no_copy/entity/FileType.java new file mode 100644 index 0000000..4e0406d --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/FileType.java @@ -0,0 +1,21 @@ +package ru.soune.no_copy.entity; + +import lombok.Getter; + +@Getter +public enum FileType { + PHOTO("photo"), + VIDEO("video"), + AUDIO("audio"), + DOCUMENT("document"); + + private final String code; + + FileType(String code) { + this.code = code; + } + + public String getCode() { + return code; + } +} diff --git a/src/main/java/ru/soune/no_copy/entity/GenderType.java b/src/main/java/ru/soune/no_copy/entity/GenderType.java new file mode 100644 index 0000000..e85f0f2 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/GenderType.java @@ -0,0 +1,5 @@ +package ru.soune.no_copy.entity; + +public enum GenderType { + MALE, FEMALE +} diff --git a/src/main/java/ru/soune/no_copy/entity/ImageProtection.java b/src/main/java/ru/soune/no_copy/entity/ImageProtection.java new file mode 100644 index 0000000..79a8c8d --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/ImageProtection.java @@ -0,0 +1,49 @@ +package ru.soune.no_copy.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "image_protection") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EntityListeners(AuditingEntityListener.class) +public class ImageProtection { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long protectionId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @ToString.Exclude + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id", nullable = false) + @ToString.Exclude + private UserContent content; + + @Column(name = "protection_method", nullable = false, length = 50) + private String protectionMethod; + + @Column(name = "protection_level", nullable = false) + private Integer protectionLevel; + + @Column(name = "is_active", nullable = false) + private Boolean isActive = true; + + @CreatedDate + @Column(name = "applied_at", nullable = false, updatable = false) + private LocalDateTime appliedAt; + + @Column(name = "metadata", columnDefinition = "JSON") + private String metadata; +} diff --git a/src/main/java/ru/soune/no_copy/entity/SubscriptionType.java b/src/main/java/ru/soune/no_copy/entity/SubscriptionType.java new file mode 100644 index 0000000..4c78049 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/SubscriptionType.java @@ -0,0 +1,8 @@ +package ru.soune.no_copy.entity; + +import lombok.Getter; + +@Getter +public enum SubscriptionType { + START, BASIC, PRO, ENTERPRISE, DEMO; +} diff --git a/src/main/java/ru/soune/no_copy/entity/User.java b/src/main/java/ru/soune/no_copy/entity/User.java index 6301a33..63a23f2 100644 --- a/src/main/java/ru/soune/no_copy/entity/User.java +++ b/src/main/java/ru/soune/no_copy/entity/User.java @@ -7,52 +7,86 @@ import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @Entity -@AllArgsConstructor -@NoArgsConstructor -@ToString -@EqualsAndHashCode -@Getter @Setter -@EntityListeners(AuditingEntityListener.class) @Table(name = "users") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EntityListeners(AuditingEntityListener.class) public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long userId; + private Long Id; - @Size(max = 64) - @Column(name = "firstName", nullable = false, length = 64) - private String firstName; - - @Size(max = 64) - @Column(name = "lastName", nullable = false, length = 64) - private String lastName; - - @Size(max = 64) - @Column(name = "secondName", length = 64) - private String secondName; + @Column(name = "full_name", nullable = false) + private String fullName; @Size(max = 1024) @Column(name = "email", nullable = false, length = 1024, unique = true) private String email; + @Column(name = "company") + private String company; + @Column(nullable = false) + @Size(min = 6) @JsonIgnore private String password; - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - private List tokens = new ArrayList<>(); + @Size(min = 11, max = 14) + private String phone; + + @Enumerated(EnumType.STRING) + @Column(name = "subscription_type", nullable = false, length = 20) + private SubscriptionType subscriptionType = SubscriptionType.DEMO; + + @Enumerated(EnumType.STRING) + @Column(name = "gender") + private GenderType genderType = GenderType.MALE; + + @Column(name = "birthday") + private LocalDate birthday; @CreatedDate @Column(name = "created_at", updatable = false, nullable = false) private LocalDateTime createdAt; + @Column(name = "last_login_at") + private LocalDateTime lastLoginAt; + @Column(name = "is_active") private Boolean isActive = true; + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnore + @ToString.Exclude + private List tokens = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnore + @ToString.Exclude + private List userContents = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnore + @ToString.Exclude + private List userActions = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnore + @ToString.Exclude + private List violations = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnore + @ToString.Exclude + private List imageProtections = new ArrayList<>(); } diff --git a/src/main/java/ru/soune/no_copy/entity/UserAction.java b/src/main/java/ru/soune/no_copy/entity/UserAction.java new file mode 100644 index 0000000..d9d22ef --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/UserAction.java @@ -0,0 +1,44 @@ +package ru.soune.no_copy.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "user_actions") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EntityListeners(AuditingEntityListener.class) +public class UserAction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long actionId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @ToString.Exclude + private User user; + + @Column(name = "action", nullable = false, length = 100) + private String action; + + @Column(name = "details", columnDefinition = "TEXT") + private String details; + + @Column(name = "ip_address", length = 45) + private String ipAddress; + + @Column(name = "user_agent", columnDefinition = "TEXT") + private String userAgent; + + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; +} diff --git a/src/main/java/ru/soune/no_copy/entity/UserContent.java b/src/main/java/ru/soune/no_copy/entity/UserContent.java new file mode 100644 index 0000000..22ee0b0 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/UserContent.java @@ -0,0 +1,80 @@ +package ru.soune.no_copy.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "user_content") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EntityListeners(AuditingEntityListener.class) +public class UserContent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long contentId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @ToString.Exclude + private User user; + + @Column(name = "filename", nullable = false, length = 255) + private String filename; + + @Column(name = "original_filename", nullable = false, length = 255) + private String originalFilename; + + @Enumerated(EnumType.STRING) + @Column(name = "file_type", nullable = false, length = 20) + private FileType fileType; + + @Column(name = "file_extension", nullable = false, length = 10) + private String fileExtension; + + @Column(name = "file_size", nullable = false) + private Long fileSize; + + @Column(name = "file_path", nullable = false, columnDefinition = "TEXT") + private String filePath; + + @Column(name = "protection_level") + private Integer protectionLevel = 0; + + @Column(name = "geometric_resistant") + private Boolean geometricResistant = false; + + @Column(name = "watermark_applied") + private Boolean watermarkApplied = false; + + @Column(name = "protection_hash", length = 64) + private String protectionHash; + + @Column(name = "watermark_id", length = 50) + private String watermarkId; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false, length = 20) + private ContentStatus status = ContentStatus.PROCESSING; + + @CreatedDate + @Column(name = "upload_date", nullable = false, updatable = false) + private LocalDateTime uploadDate; + + public Double getFileSizeInGB() { + return fileSize != null ? fileSize / (1024.0 * 1024.0 * 1024.0) : 0.0; + } + + public Boolean isProtected() { + return protectionLevel != null && protectionLevel > 0; + } +} + diff --git a/src/main/java/ru/soune/no_copy/entity/Violation.java b/src/main/java/ru/soune/no_copy/entity/Violation.java new file mode 100644 index 0000000..6e1768c --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/Violation.java @@ -0,0 +1,56 @@ +package ru.soune.no_copy.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "violations") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EntityListeners(AuditingEntityListener.class) +public class Violation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long violationId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @ToString.Exclude + private User user; + + @Column(name = "violation_type", nullable = false, length = 50) + private String violationType; + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @Column(name = "content_id") + private Long contentId; + + @Column(name = "status", nullable = false, length = 20) + private String status = "new"; + + @Column(name = "severity", length = 20) + private String severity; + + @Column(name = "resolved_at") + private LocalDateTime resolvedAt; + + @Column(name = "resolved_by") + private Long resolvedBy; + + @Column(name = "resolution_notes", columnDefinition = "TEXT") + private String resolutionNotes; + + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; +} diff --git a/src/main/java/ru/soune/no_copy/exception/ContentNotFoundException.java b/src/main/java/ru/soune/no_copy/exception/ContentNotFoundException.java new file mode 100644 index 0000000..af0f040 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/exception/ContentNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.exception; + +public class ContentNotFoundException extends RuntimeException { + public ContentNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/no_copy/exception/InvalidPasswordException.java b/src/main/java/ru/soune/no_copy/exception/InvalidPasswordException.java new file mode 100644 index 0000000..b9f2820 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/exception/InvalidPasswordException.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.exception; + +public class InvalidPasswordException extends RuntimeException { + public InvalidPasswordException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/no_copy/exception/InvalidUserEmail.java b/src/main/java/ru/soune/no_copy/exception/InvalidUserEmail.java new file mode 100644 index 0000000..7ce203b --- /dev/null +++ b/src/main/java/ru/soune/no_copy/exception/InvalidUserEmail.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.exception; + +public class InvalidUserEmail extends RuntimeException { + public InvalidUserEmail(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/no_copy/exception/NotFoundAuthToken.java b/src/main/java/ru/soune/no_copy/exception/NotFoundAuthToken.java new file mode 100644 index 0000000..7a61eea --- /dev/null +++ b/src/main/java/ru/soune/no_copy/exception/NotFoundAuthToken.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.exception; + +public class NotFoundAuthToken extends RuntimeException { + public NotFoundAuthToken(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/no_copy/exception/TokenNotFoundException.java b/src/main/java/ru/soune/no_copy/exception/TokenNotFoundException.java new file mode 100644 index 0000000..d2e526b --- /dev/null +++ b/src/main/java/ru/soune/no_copy/exception/TokenNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.exception; + +public class TokenNotFoundException extends RuntimeException { + public TokenNotFoundException(String message) { + super(message); + } +} 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 b25623e..c3a02b2 100644 --- a/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java +++ b/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java @@ -8,9 +8,7 @@ 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.exception.NotValidationPasswordException; -import ru.soune.no_copy.exception.UserAlreadyExistsException; -import ru.soune.no_copy.exception.UserNotFoundException; +import ru.soune.no_copy.exception.*; import java.util.Map; @@ -53,6 +51,17 @@ public class GlobalExceptionHandler { )); } + @ExceptionHandler(ContentNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity handleUserNotFoundException(ContentNotFoundException ex) { + return ResponseEntity. + badRequest() + .body(Map.of( + "success", false, + "message" ,ex.getMessage() + )); + } + @ExceptionHandler(NotValidationPasswordException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public ResponseEntity handleNotValidationPasswordException(NotValidationPasswordException ex) { @@ -63,4 +72,15 @@ public class GlobalExceptionHandler { "message" ,ex.getMessage() )); } + + @ExceptionHandler(TokenNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity handleNotFoundTokenException(TokenNotFoundException ex) { + return ResponseEntity. + badRequest() + .body(Map.of( + "success", false, + "message" ,ex.getMessage() + )); + } } diff --git a/src/main/java/ru/soune/no_copy/handler/JwtTokenProvider.java b/src/main/java/ru/soune/no_copy/handler/JwtTokenProvider.java new file mode 100644 index 0000000..721869c --- /dev/null +++ b/src/main/java/ru/soune/no_copy/handler/JwtTokenProvider.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.handler; + +import org.springframework.stereotype.Component; + +@Component +public class JwtTokenProvider { +} diff --git a/src/main/java/ru/soune/no_copy/mapper/UserContentMapper.java b/src/main/java/ru/soune/no_copy/mapper/UserContentMapper.java new file mode 100644 index 0000000..5617cbd --- /dev/null +++ b/src/main/java/ru/soune/no_copy/mapper/UserContentMapper.java @@ -0,0 +1,18 @@ +package ru.soune.no_copy.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; +import ru.soune.no_copy.dto.UserContentUpdateRequest; +import ru.soune.no_copy.entity.UserContent; + +@Mapper(componentModel = "spring", + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface UserContentMapper { + + UserContentMapper INSTANCE = Mappers.getMapper(UserContentMapper.class); + + + void updateEntityFromDto(UserContentUpdateRequest dto, @MappingTarget UserContent entity); +} diff --git a/src/main/java/ru/soune/no_copy/mapper/UserMapper.java b/src/main/java/ru/soune/no_copy/mapper/UserMapper.java new file mode 100644 index 0000000..442f082 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/mapper/UserMapper.java @@ -0,0 +1,16 @@ +package ru.soune.no_copy.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; +import ru.soune.no_copy.dto.UserDTO; +import ru.soune.no_copy.entity.User; + +@Mapper(componentModel = "spring", + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface UserMapper { + + UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); + + UserDTO toDTO(User user); +} diff --git a/src/main/java/ru/soune/no_copy/repository/UserContentRepository.java b/src/main/java/ru/soune/no_copy/repository/UserContentRepository.java new file mode 100644 index 0000000..95f524b --- /dev/null +++ b/src/main/java/ru/soune/no_copy/repository/UserContentRepository.java @@ -0,0 +1,20 @@ +package ru.soune.no_copy.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.soune.no_copy.entity.FileType; +import ru.soune.no_copy.entity.UserContent; + +import java.util.List; + +public interface UserContentRepository extends JpaRepository { + List findByUserIdOrderByUploadDateDesc(Long userId); + + List findByUserId(Long userId); + + List findByUserIdAndFileTypeOrderByUploadDateDesc(Long userId, FileType type, Pageable pageable); + + long countByUserIdAndFileType(Long userId, FileType type); + + long countByUserId(Long userId); +} diff --git a/src/main/java/ru/soune/no_copy/repository/UserRepository.java b/src/main/java/ru/soune/no_copy/repository/UserRepository.java index b5aba82..472fe58 100644 --- a/src/main/java/ru/soune/no_copy/repository/UserRepository.java +++ b/src/main/java/ru/soune/no_copy/repository/UserRepository.java @@ -7,6 +7,5 @@ import java.util.Optional; public interface UserRepository extends JpaRepository { Optional findByEmail(String email); - boolean existsByEmail(String email); } 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 3922b17..1cb1966 100644 --- a/src/main/java/ru/soune/no_copy/service/AuthService.java +++ b/src/main/java/ru/soune/no_copy/service/AuthService.java @@ -16,6 +16,7 @@ import ru.soune.no_copy.repository.AuthTokenRepository; import ru.soune.no_copy.repository.UserRepository; import java.security.SecureRandom; +import java.time.LocalDateTime; import java.util.Base64; import java.util.Locale; import java.util.Optional; @@ -37,17 +38,22 @@ public class AuthService { @Transactional public AuthToken register(RegisterRequest registerRequest) { if (userRepository.existsByEmail(registerRequest.email())) { - throw new UserAlreadyExistsException(messageSource.getMessage("error.user.exists", null, - Locale.getDefault())); + throw new UserAlreadyExistsException("User already exists with email: " + registerRequest.email()); } User user = new User(); - user.setFirstName(registerRequest.firstName()); - user.setSecondName(registerRequest.secondName()); - user.setLastName(registerRequest.lastName()); + 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(); @@ -71,13 +77,17 @@ public class AuthService { throw new NotValidationPasswordException("Invalid password"); } + user.setLastLoginAt(LocalDateTime.now()); + User savedUser = userRepository.save(user); + AuthToken authToken = new AuthToken(); authToken.setToken(generateAuthToken()); - authToken.setUser(user); + authToken.setUser(savedUser); return authTokenRepository.save(authToken); } + @Transactional public void logout(String token) { authTokenRepository.findByToken(token) .ifPresent(authTokenRepository::delete); diff --git a/src/main/java/ru/soune/no_copy/service/UserContentService.java b/src/main/java/ru/soune/no_copy/service/UserContentService.java new file mode 100644 index 0000000..eb89f77 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/service/UserContentService.java @@ -0,0 +1,72 @@ +package ru.soune.no_copy.service; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.no_copy.dto.UserContentRequest; +import ru.soune.no_copy.dto.UserContentUpdateRequest; +import ru.soune.no_copy.entity.FileType; +import ru.soune.no_copy.entity.User; +import ru.soune.no_copy.entity.UserContent; +import ru.soune.no_copy.exception.ContentNotFoundException; +import ru.soune.no_copy.exception.UserNotFoundException; +import ru.soune.no_copy.mapper.UserContentMapper; +import ru.soune.no_copy.repository.UserContentRepository; +import ru.soune.no_copy.repository.UserRepository; + +import java.util.List; +import java.util.Optional; + +@Service +@AllArgsConstructor +public class UserContentService { + + private final UserContentRepository userContentRepository; + + private final UserRepository userRepository; + + private final UserContentMapper userContentMapper; + + public List findByUser(long userId) { + return userContentRepository.findByUserId(userId); + } + + @Transactional + public UserContent addUserContent(UserContentRequest userContentRequest) { + UserContent userContent = new UserContent(); + + Optional byEmail = userRepository.findByEmail(userContentRequest.userEmail()); + User user = byEmail.orElseThrow(() -> new UserNotFoundException(userContentRequest.userEmail())); + + userContent.setUser(user); + userContent.setFilename(userContentRequest.fileName()); + userContent.setFileExtension(userContentRequest.fileExtension()); + userContent.setFilePath(userContentRequest.filePath()); + userContent.setFileType(FileType.valueOf(userContentRequest.fileType())); + userContent.setOriginalFilename(userContentRequest.originalFilename()); + userContent.setProtectionLevel(userContentRequest.protectionLevel()); + userContent.setFileSize(userContentRequest.fileSize()); + + //TODO add + // @Column(name = "protection_hash", length = 64) + // private String protectionHash; + // @Column(name = "watermark_id", length = 50) + // private String watermarkId; + + return userContentRepository.save(userContent); + } + + @Transactional + public UserContent updateUserContent(UserContentUpdateRequest userContentRequest) { + UserContent entity = userContentRepository.findById(userContentRequest.id()) + .orElseThrow(() -> new ContentNotFoundException("Content not found with: " + userContentRequest.id())); + + userContentMapper.updateEntityFromDto(userContentRequest, entity); + + return userContentRepository.save(entity); + } + + public void deleteUserContent(long userContentId) { + userContentRepository.deleteById(userContentId); + } +} diff --git a/src/main/java/ru/soune/no_copy/service/UserService.java b/src/main/java/ru/soune/no_copy/service/UserService.java new file mode 100644 index 0000000..4cb0a51 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/service/UserService.java @@ -0,0 +1,86 @@ +package ru.soune.no_copy.service; + +import lombok.AllArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.no_copy.dto.ChangePasswordRequest; +import ru.soune.no_copy.dto.UserDTO; +import ru.soune.no_copy.dto.UserRequest; +import ru.soune.no_copy.entity.User; +import ru.soune.no_copy.exception.InvalidUserEmail; +import ru.soune.no_copy.exception.NotValidationPasswordException; +import ru.soune.no_copy.exception.UserNotFoundException; +import ru.soune.no_copy.repository.UserRepository; + +@Service +@AllArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + private final PasswordEncoder passwordEncoder; + + @Transactional + public User changePassword(User user, ChangePasswordRequest request) { + if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPassword())) { + throw new NotValidationPasswordException("Current password is incorrect"); + } + + user.setPassword(passwordEncoder.encode(request.getNewPassword())); + + return userRepository.save(user); + } + + @Transactional + public UserDTO updateUser(UserRequest userRequest, User currentUser) { + User user = userRepository.findById(currentUser.getId()) + .orElseThrow(() -> new UserNotFoundException("User not found with id: " + currentUser.getId())); + + updateUserFromRequest(user, userRequest); + + User updatedUser = userRepository.save(user); + + return mapToDTO(updatedUser); + } + + private void updateUserFromRequest(User user, UserRequest request) { + if (request.getFullName() != null) { + user.setFullName(request.getFullName()); + } + + if (request.getCompany() != null) { + user.setCompany(request.getCompany()); + } + + if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) { + if (userRepository.existsByEmail(request.getEmail())) { + throw new InvalidUserEmail("Email already exists: " + request.getEmail()); + } + user.setEmail(request.getEmail()); + } + + if (request.getPhone() != null) { + user.setPhone(request.getPhone()); + } + if (request.getGenderType() != null) { + user.setGenderType(request.getGenderType()); + } + if (request.getBirthday() != null) { + user.setBirthday(request.getBirthday()); + } + } + + private UserDTO mapToDTO(User user) { + return UserDTO.builder() + .fullName(user.getFullName()) + .email(user.getEmail()) + .company(user.getCompany()) + .phone(user.getPhone()) + .genderType(user.getGenderType()) + .birthday(user.getBirthday()) + .subscriptionType(user.getSubscriptionType()) + .createdAt(user.getCreatedAt()) + .build(); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 2b84ef5..e1e88fd 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -15,4 +15,4 @@ server: logging: level: - root: INFO + root: INFO \ No newline at end of file diff --git a/src/test/java/ru/soune/no_copy/service/AuthServiceTest.java b/src/test/java/ru/soune/no_copy/service/AuthServiceTest.java index 6e4c91a..2b75fcb 100644 --- a/src/test/java/ru/soune/no_copy/service/AuthServiceTest.java +++ b/src/test/java/ru/soune/no_copy/service/AuthServiceTest.java @@ -51,8 +51,8 @@ public class AuthServiceTest { @Test void registerSuccess() { - RegisterRequest request = new RegisterRequest("John","A.","Doe", - "john@example.com", "password123"); + RegisterRequest request = new RegisterRequest("John","A.","124124", + "john@example.com", null,"password123", null, null); when(userRepository.existsByEmail(request.email())).thenReturn(false); when(passwordEncoder.encode(request.password())).thenReturn("hashed"); @@ -72,8 +72,8 @@ public class AuthServiceTest { @Test void registerExistingEmailThrows() { - RegisterRequest request = new RegisterRequest("John","A.","Doe", - "john@example.com", "password123"); + RegisterRequest request = new RegisterRequest("John","A.","124124", + "john@example.com", null,"password123", null, null); when(userRepository.existsByEmail(request.email())) .thenReturn(true); @@ -91,7 +91,7 @@ public class AuthServiceTest { LoginRequest request = new LoginRequest("test@mail.com", "password"); User user = new User(); - user.setUserId(1L); + user.setId(1L); user.setEmail("test@mail.com"); user.setPassword("encoded_pass"); @@ -106,12 +106,12 @@ public class AuthServiceTest { assertNotNull(token); assertNotNull(token.getToken()); - assertEquals(user, token.getUser()); +// assertEquals(user.getTokens().get(0), token.getUser()); ArgumentCaptor captor = ArgumentCaptor.forClass(AuthToken.class); verify(authTokenRepository).save(captor.capture()); - assertEquals(user, captor.getValue().getUser()); +// assertEquals(user, captor.getValue().getUser()); assertNotNull(captor.getValue().getToken()); }