From 149bd961e2f168b6aa2f96dd1f32ac7ce1a78bcb Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 25 Nov 2025 23:09:30 +0700 Subject: [PATCH 01/28] NCP-3 add UserContent entity --- .../no_copy/dto/UserContentResponse.java | 17 +++++ .../soune/no_copy/entity/ContentStatus.java | 5 ++ .../ru/soune/no_copy/entity/FileType.java | 5 ++ .../ru/soune/no_copy/entity/UserContent.java | 73 +++++++++++++++++++ .../repository/UserContentRepository.java | 20 +++++ 5 files changed, 120 insertions(+) create mode 100644 src/main/java/ru/soune/no_copy/dto/UserContentResponse.java create mode 100644 src/main/java/ru/soune/no_copy/entity/ContentStatus.java create mode 100644 src/main/java/ru/soune/no_copy/entity/FileType.java create mode 100644 src/main/java/ru/soune/no_copy/entity/UserContent.java create mode 100644 src/main/java/ru/soune/no_copy/repository/UserContentRepository.java 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/entity/ContentStatus.java b/src/main/java/ru/soune/no_copy/entity/ContentStatus.java new file mode 100644 index 0000000..4009ee9 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/ContentStatus.java @@ -0,0 +1,5 @@ +package ru.soune.no_copy.entity; + +public enum ContentStatus { + PROCESSING, COMPLETED, ERROR +} 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..b07bc9b --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/FileType.java @@ -0,0 +1,5 @@ +package ru.soune.no_copy.entity; + +public enum FileType { + PHOTO, DOCUMENT, AUDIO, VIDEO +} 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..eda224a --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/UserContent.java @@ -0,0 +1,73 @@ +package ru.soune.no_copy.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Getter @Setter +@Table(name = "user_content", +indexes = { + @Index(name = "idx_user_id", columnList = "user_id"), + @Index(name = "idx_file_type", columnList = "file_type"), + @Index(name = "idx_upload_date", columnList = "upload_date") +}) +public class UserContent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false, + foreignKey = @ForeignKey(name = "fk_usercontent_user")) + private User user; + + @Column(nullable = false) + private String filename; + + @Column(name = "original_filename", nullable = false) + private String originalFilename; + + @Enumerated(EnumType.STRING) + @Column(name = "file_type", nullable = false, length = 16) + private FileType fileType; + + @Column(name = "file_extension", length = 16) + private String fileExtension; + + @Column(name = "file_size", nullable = false) + private Long fileSize; + + @Column(name = "file_path", columnDefinition = "TEXT", nullable = false) + private String filePath; + + @Column(name = "protection_level") + private Integer protectionLevel; + + @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(length = 16) + private ContentStatus status = ContentStatus.PROCESSING; + + @CreationTimestamp + @Column(name = "upload_date", nullable = false, updatable = false) + private LocalDateTime uploadDate; +} + 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..aad8c79 --- /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 findByUserIdAndFileTypeOrderByUploadDateDesc( + Long userId, FileType type, Pageable pageable + ); + + long countByUserIdAndFileType(Long userId, FileType type); + + long countByUserId(Long userId); +} From d9faa311a9e953a28a4d110fd72d8912a0dc866e Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 26 Nov 2025 13:03:20 +0700 Subject: [PATCH 02/28] NCP-3 add usercontent crud's,dto,exception and start restcontroller --- build.gradle | 2 + .../controller/UserContentController.java | 11 +++ .../ru/soune/no_copy/dto/UserContentDTO.java | 15 ++++ .../soune/no_copy/dto/UserContentRequest.java | 9 +++ .../no_copy/dto/UserContentUpdateRequest.java | 10 +++ .../exception/ContentNotFoundException.java | 7 ++ .../handler/GlobalExceptionHandler.java | 12 ++++ .../no_copy/mapper/UserContentMapper.java | 18 +++++ .../repository/UserContentRepository.java | 6 +- .../no_copy/service/UserContentService.java | 72 +++++++++++++++++++ 10 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 src/main/java/ru/soune/no_copy/controller/UserContentController.java create mode 100644 src/main/java/ru/soune/no_copy/dto/UserContentDTO.java create mode 100644 src/main/java/ru/soune/no_copy/dto/UserContentRequest.java create mode 100644 src/main/java/ru/soune/no_copy/dto/UserContentUpdateRequest.java create mode 100644 src/main/java/ru/soune/no_copy/exception/ContentNotFoundException.java create mode 100644 src/main/java/ru/soune/no_copy/mapper/UserContentMapper.java create mode 100644 src/main/java/ru/soune/no_copy/service/UserContentService.java 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/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..49eac67 --- /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("api/content") +@RequestMapping("api/content") +@AllArgsConstructor +public class UserContentController { +} 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..de64e7f --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/UserContentRequest.java @@ -0,0 +1,9 @@ +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/UserContentUpdateRequest.java b/src/main/java/ru/soune/no_copy/dto/UserContentUpdateRequest.java new file mode 100644 index 0000000..ba8ab5f --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/UserContentUpdateRequest.java @@ -0,0 +1,10 @@ +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/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/handler/GlobalExceptionHandler.java b/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java index b25623e..3e4efbe 100644 --- a/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java +++ b/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java @@ -8,6 +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.ContentNotFoundException; import ru.soune.no_copy.exception.NotValidationPasswordException; import ru.soune.no_copy.exception.UserAlreadyExistsException; import ru.soune.no_copy.exception.UserNotFoundException; @@ -53,6 +54,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) { 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/repository/UserContentRepository.java b/src/main/java/ru/soune/no_copy/repository/UserContentRepository.java index aad8c79..95f524b 100644 --- a/src/main/java/ru/soune/no_copy/repository/UserContentRepository.java +++ b/src/main/java/ru/soune/no_copy/repository/UserContentRepository.java @@ -10,9 +10,9 @@ import java.util.List; public interface UserContentRepository extends JpaRepository { List findByUserIdOrderByUploadDateDesc(Long userId); - List findByUserIdAndFileTypeOrderByUploadDateDesc( - Long userId, FileType type, Pageable pageable - ); + List findByUserId(Long userId); + + List findByUserIdAndFileTypeOrderByUploadDateDesc(Long userId, FileType type, Pageable pageable); long countByUserIdAndFileType(Long userId, FileType type); 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); + } +} From c2d44b3428fe100eae258089abfd4a659c883a0d Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 26 Nov 2025 15:49:51 +0700 Subject: [PATCH 03/28] NCP-3 Update entity like helper.php --- .../configuration/SubscriptionConfig.java | 35 ++++++++++++ .../ru/soune/no_copy/entity/AuthToken.java | 31 ++++++---- .../ru/soune/no_copy/entity/BaseEntity.java | 29 ++++++++++ .../soune/no_copy/entity/ContentStatus.java | 17 +++++- .../ru/soune/no_copy/entity/FileType.java | 18 +++++- .../soune/no_copy/entity/ImageProtection.java | 49 ++++++++++++++++ .../no_copy/entity/SubscriptionType.java | 21 +++++++ .../java/ru/soune/no_copy/entity/User.java | 57 +++++++++++++++---- .../ru/soune/no_copy/entity/UserAction.java | 44 ++++++++++++++ .../ru/soune/no_copy/entity/UserContent.java | 45 ++++++++------- .../ru/soune/no_copy/entity/Violation.java | 56 ++++++++++++++++++ 11 files changed, 360 insertions(+), 42 deletions(-) create mode 100644 src/main/java/ru/soune/no_copy/configuration/SubscriptionConfig.java create mode 100644 src/main/java/ru/soune/no_copy/entity/BaseEntity.java create mode 100644 src/main/java/ru/soune/no_copy/entity/ImageProtection.java create mode 100644 src/main/java/ru/soune/no_copy/entity/SubscriptionType.java create mode 100644 src/main/java/ru/soune/no_copy/entity/UserAction.java create mode 100644 src/main/java/ru/soune/no_copy/entity/Violation.java 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/entity/AuthToken.java b/src/main/java/ru/soune/no_copy/entity/AuthToken.java index ee78fe9..938d31c 100644 --- a/src/main/java/ru/soune/no_copy/entity/AuthToken.java +++ b/src/main/java/ru/soune/no_copy/entity/AuthToken.java @@ -1,37 +1,48 @@ 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; @Column(name = "expires_at", nullable = false) - private LocalDateTime expiresAt = LocalDateTime.now().plusDays(30); + private LocalDateTime expiresAt; @CreatedDate @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 index 4009ee9..d97f66c 100644 --- a/src/main/java/ru/soune/no_copy/entity/ContentStatus.java +++ b/src/main/java/ru/soune/no_copy/entity/ContentStatus.java @@ -1,5 +1,20 @@ package ru.soune.no_copy.entity; +import lombok.Getter; + +@Getter public enum ContentStatus { - PROCESSING, COMPLETED, ERROR + 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 index b07bc9b..4e0406d 100644 --- a/src/main/java/ru/soune/no_copy/entity/FileType.java +++ b/src/main/java/ru/soune/no_copy/entity/FileType.java @@ -1,5 +1,21 @@ package ru.soune.no_copy.entity; +import lombok.Getter; + +@Getter public enum FileType { - PHOTO, DOCUMENT, AUDIO, VIDEO + 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/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..510e8e9 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/SubscriptionType.java @@ -0,0 +1,21 @@ +package ru.soune.no_copy.entity; + +import lombok.Getter; + +@Getter +public enum SubscriptionType { + START("СТАРТ"), + BASIC("БАЗОВЫЙ"), + PRO("ПРО"), + ENTERPRISE("ЭНТЕРПРАЙЗ"); + + private final String displayName; + + SubscriptionType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} 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..d4595f5 100644 --- a/src/main/java/ru/soune/no_copy/entity/User.java +++ b/src/main/java/ru/soune/no_copy/entity/User.java @@ -12,13 +12,13 @@ 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 @@ -26,15 +26,15 @@ public class User { private Long userId; @Size(max = 64) - @Column(name = "firstName", nullable = false, length = 64) + @Column(name = "first_name", nullable = false, length = 64) private String firstName; @Size(max = 64) - @Column(name = "lastName", nullable = false, length = 64) + @Column(name = "last_name", nullable = false, length = 64) private String lastName; @Size(max = 64) - @Column(name = "secondName", length = 64) + @Column(name = "second_name", length = 64) private String secondName; @Size(max = 1024) @@ -45,14 +45,49 @@ public class User { @JsonIgnore private String password; - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - private List tokens = new ArrayList<>(); + @Enumerated(EnumType.STRING) + @Column(name = "subscription_type", nullable = false, length = 20) + private SubscriptionType subscriptionType = SubscriptionType.START; @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<>(); + + public String getFullName() { + if (secondName != null && !secondName.isBlank()) { + return String.format("%s %s %s", lastName, firstName, secondName); + } + return String.format("%s %s", lastName, firstName); + } } 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 index eda224a..22ee0b0 100644 --- a/src/main/java/ru/soune/no_copy/entity/UserContent.java +++ b/src/main/java/ru/soune/no_copy/entity/UserContent.java @@ -3,52 +3,51 @@ 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 -@Builder -@Getter @Setter -@Table(name = "user_content", -indexes = { - @Index(name = "idx_user_id", columnList = "user_id"), - @Index(name = "idx_file_type", columnList = "file_type"), - @Index(name = "idx_upload_date", columnList = "upload_date") -}) +@ToString +@EntityListeners(AuditingEntityListener.class) public class UserContent { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private Long contentId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false, - foreignKey = @ForeignKey(name = "fk_usercontent_user")) + @JoinColumn(name = "user_id", nullable = false) + @ToString.Exclude private User user; - @Column(nullable = false) + @Column(name = "filename", nullable = false, length = 255) private String filename; - @Column(name = "original_filename", nullable = false) + @Column(name = "original_filename", nullable = false, length = 255) private String originalFilename; @Enumerated(EnumType.STRING) - @Column(name = "file_type", nullable = false, length = 16) + @Column(name = "file_type", nullable = false, length = 20) private FileType fileType; - @Column(name = "file_extension", length = 16) + @Column(name = "file_extension", nullable = false, length = 10) private String fileExtension; @Column(name = "file_size", nullable = false) private Long fileSize; - @Column(name = "file_path", columnDefinition = "TEXT", nullable = false) + @Column(name = "file_path", nullable = false, columnDefinition = "TEXT") private String filePath; @Column(name = "protection_level") - private Integer protectionLevel; + private Integer protectionLevel = 0; @Column(name = "geometric_resistant") private Boolean geometricResistant = false; @@ -63,11 +62,19 @@ public class UserContent { private String watermarkId; @Enumerated(EnumType.STRING) - @Column(length = 16) + @Column(name = "status", nullable = false, length = 20) private ContentStatus status = ContentStatus.PROCESSING; - @CreationTimestamp + @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; +} From ffb1d811a82c28b29ed6434e8a9609066a7d7a8c Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 27 Nov 2025 03:05:07 +0700 Subject: [PATCH 04/28] NCP-3 add exception,add change password logic, fix checkauthtoken --- README.md | 0 docker-compose.yaml | 4 +- .../no_copy/controller/UserController.java | 44 +++++++++++++++++-- .../no_copy/dto/ChangePasswordRequest.java | 14 ++++++ .../ru/soune/no_copy/dto/RegisterRequest.java | 36 ++++++++++++--- .../java/ru/soune/no_copy/dto/UserDTO.java | 13 +++++- .../ru/soune/no_copy/entity/AuthToken.java | 2 +- .../ru/soune/no_copy/entity/GenderType.java | 5 +++ .../no_copy/entity/SubscriptionType.java | 15 +------ .../java/ru/soune/no_copy/entity/User.java | 39 ++++++++-------- .../exception/InvalidPasswordException.java | 7 +++ .../no_copy/exception/InvalidUserEmail.java | 7 +++ .../no_copy/exception/NotFoundAuthToken.java | 7 +++ .../ru/soune/no_copy/mapper/UserMapper.java | 16 +++++++ .../no_copy/repository/UserRepository.java | 1 - .../ru/soune/no_copy/service/AuthService.java | 19 ++++++-- .../ru/soune/no_copy/service/UserService.java | 28 ++++++++++++ .../no_copy/service/AuthServiceTest.java | 14 +++--- 18 files changed, 212 insertions(+), 59 deletions(-) create mode 100644 README.md create mode 100644 src/main/java/ru/soune/no_copy/dto/ChangePasswordRequest.java create mode 100644 src/main/java/ru/soune/no_copy/entity/GenderType.java create mode 100644 src/main/java/ru/soune/no_copy/exception/InvalidPasswordException.java create mode 100644 src/main/java/ru/soune/no_copy/exception/InvalidUserEmail.java create mode 100644 src/main/java/ru/soune/no_copy/exception/NotFoundAuthToken.java create mode 100644 src/main/java/ru/soune/no_copy/mapper/UserMapper.java create mode 100644 src/main/java/ru/soune/no_copy/service/UserService.java diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yaml b/docker-compose.yaml index 0ed4d65..91cfd20 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,8 +15,8 @@ services: container_name: postgres app: - image: popovtsev/ncp -# build: . +# image: popovtsev/ncp + build: . container_name: no_copy_app environment: POSTGRES_DB: no_copy_ 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..167502b 100644 --- a/src/main/java/ru/soune/no_copy/controller/UserController.java +++ b/src/main/java/ru/soune/no_copy/controller/UserController.java @@ -2,13 +2,20 @@ 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.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") @@ -17,12 +24,41 @@ 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())) .toList(); return ResponseEntity.ok(allUsers); } + + //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)); + } } 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/RegisterRequest.java b/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java index bdf67e9..997d27e 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,39 @@ 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.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") + @Size(min = 2) + 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") + 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/UserDTO.java b/src/main/java/ru/soune/no_copy/dto/UserDTO.java index ebe4f63..a5fa251 100644 --- a/src/main/java/ru/soune/no_copy/dto/UserDTO.java +++ b/src/main/java/ru/soune/no_copy/dto/UserDTO.java @@ -2,11 +2,22 @@ package ru.soune.no_copy.dto; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; +import ru.soune.no_copy.entity.GenderType; + +import java.time.LocalDate; +import java.time.LocalDateTime; @Data @AllArgsConstructor +@NoArgsConstructor 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; } 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 938d31c..6418073 100644 --- a/src/main/java/ru/soune/no_copy/entity/AuthToken.java +++ b/src/main/java/ru/soune/no_copy/entity/AuthToken.java @@ -30,7 +30,7 @@ public class AuthToken { private String token; @Column(name = "expires_at", nullable = false) - private LocalDateTime expiresAt; + private LocalDateTime expiresAt = LocalDateTime.now().plusDays(30); @CreatedDate @Column(name = "created_at", updatable = false, nullable = false) 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/SubscriptionType.java b/src/main/java/ru/soune/no_copy/entity/SubscriptionType.java index 510e8e9..4c78049 100644 --- a/src/main/java/ru/soune/no_copy/entity/SubscriptionType.java +++ b/src/main/java/ru/soune/no_copy/entity/SubscriptionType.java @@ -4,18 +4,5 @@ import lombok.Getter; @Getter public enum SubscriptionType { - START("СТАРТ"), - BASIC("БАЗОВЫЙ"), - PRO("ПРО"), - ENTERPRISE("ЭНТЕРПРАЙЗ"); - - private final String displayName; - - SubscriptionType(String displayName) { - this.displayName = displayName; - } - - public String getDisplayName() { - return displayName; - } + 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 d4595f5..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,6 +7,7 @@ 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; @@ -23,31 +24,36 @@ public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long userId; + private Long Id; - @Size(max = 64) - @Column(name = "first_name", nullable = false, length = 64) - private String firstName; - - @Size(max = 64) - @Column(name = "last_name", nullable = false, length = 64) - private String lastName; - - @Size(max = 64) - @Column(name = "second_name", 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; + @Size(min = 11, max = 14) + private String phone; + @Enumerated(EnumType.STRING) @Column(name = "subscription_type", nullable = false, length = 20) - private SubscriptionType subscriptionType = SubscriptionType.START; + 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) @@ -83,11 +89,4 @@ public class User { @JsonIgnore @ToString.Exclude private List imageProtections = new ArrayList<>(); - - public String getFullName() { - if (secondName != null && !secondName.isBlank()) { - return String.format("%s %s %s", lastName, firstName, secondName); - } - return String.format("%s %s", lastName, firstName); - } } 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/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/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..52eccfc 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; @@ -42,12 +43,18 @@ public class AuthService { } 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 +78,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/UserService.java b/src/main/java/ru/soune/no_copy/service/UserService.java new file mode 100644 index 0000000..6197cfc --- /dev/null +++ b/src/main/java/ru/soune/no_copy/service/UserService.java @@ -0,0 +1,28 @@ +package ru.soune.no_copy.service; + +import lombok.AllArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import ru.soune.no_copy.dto.ChangePasswordRequest; +import ru.soune.no_copy.entity.User; +import ru.soune.no_copy.exception.NotValidationPasswordException; +import ru.soune.no_copy.repository.UserRepository; + +@Service +@AllArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + private final PasswordEncoder passwordEncoder; + + 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); + } +} 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()); } From 5c8fdce32364f994939819269835ecdfef02f829 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 27 Nov 2025 12:48:08 +0700 Subject: [PATCH 05/28] NCP-3 Add logic update user fields,change password --- .../no_copy/configuration/JacksonConfig.java | 41 +++++++++++++ .../no_copy/controller/UserController.java | 17 +++++- .../ru/soune/no_copy/dto/AuthResponse.java | 7 ++- .../ru/soune/no_copy/dto/RegisterRequest.java | 2 + .../soune/no_copy/dto/UserContentRequest.java | 14 +++-- .../no_copy/dto/UserContentUpdateRequest.java | 16 +++-- .../java/ru/soune/no_copy/dto/UserDTO.java | 4 ++ .../ru/soune/no_copy/dto/UserRequest.java | 25 ++++++++ .../no_copy/handler/JwtTokenProvider.java | 7 +++ .../ru/soune/no_copy/service/UserService.java | 58 +++++++++++++++++++ 10 files changed, 180 insertions(+), 11 deletions(-) create mode 100644 src/main/java/ru/soune/no_copy/configuration/JacksonConfig.java create mode 100644 src/main/java/ru/soune/no_copy/dto/UserRequest.java create mode 100644 src/main/java/ru/soune/no_copy/handler/JwtTokenProvider.java 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/controller/UserController.java b/src/main/java/ru/soune/no_copy/controller/UserController.java index 167502b..f4db208 100644 --- a/src/main/java/ru/soune/no_copy/controller/UserController.java +++ b/src/main/java/ru/soune/no_copy/controller/UserController.java @@ -5,6 +5,7 @@ import org.springframework.http.ResponseEntity; 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; @@ -35,7 +36,7 @@ public class UserController { List allUsers = userRepository.findAll().stream() .map(u -> new UserDTO(u.getFullName(), u.getCompany(), u.getEmail(), u.getIsActive(), u.getPhone(), u.getGenderType(), - u.getBirthday(), u.getCreatedAt())) + u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType())) .toList(); return ResponseEntity.ok(allUsers); @@ -61,4 +62,18 @@ public class UserController { 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/RegisterRequest.java b/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java index 997d27e..20cef35 100644 --- a/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java +++ b/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java @@ -3,6 +3,7 @@ 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; @@ -27,6 +28,7 @@ public record RegisterRequest( @DateTimeFormat(pattern = "dd-MM-yyyy") @JsonFormat(pattern = "dd-MM-yyyy") + @Past LocalDate birthday, @NotBlank(message = "error.not.blank") diff --git a/src/main/java/ru/soune/no_copy/dto/UserContentRequest.java b/src/main/java/ru/soune/no_copy/dto/UserContentRequest.java index de64e7f..06ef01e 100644 --- a/src/main/java/ru/soune/no_copy/dto/UserContentRequest.java +++ b/src/main/java/ru/soune/no_copy/dto/UserContentRequest.java @@ -3,7 +3,13 @@ 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) {} +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/UserContentUpdateRequest.java b/src/main/java/ru/soune/no_copy/dto/UserContentUpdateRequest.java index ba8ab5f..8955969 100644 --- a/src/main/java/ru/soune/no_copy/dto/UserContentUpdateRequest.java +++ b/src/main/java/ru/soune/no_copy/dto/UserContentUpdateRequest.java @@ -3,8 +3,14 @@ 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) { -} +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 a5fa251..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,9 +1,11 @@ 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; @@ -11,6 +13,7 @@ import java.time.LocalDateTime; @Data @AllArgsConstructor @NoArgsConstructor +@Builder public class UserDTO { private String fullName; private String company; @@ -20,4 +23,5 @@ public class UserDTO { 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/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/service/UserService.java b/src/main/java/ru/soune/no_copy/service/UserService.java index 6197cfc..4cb0a51 100644 --- a/src/main/java/ru/soune/no_copy/service/UserService.java +++ b/src/main/java/ru/soune/no_copy/service/UserService.java @@ -3,9 +3,14 @@ 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 @@ -16,6 +21,7 @@ public class UserService { 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"); @@ -25,4 +31,56 @@ public class UserService { 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(); + } } From 1fd1570669925a39fa1956df5c2d1bda7de3deef Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 27 Nov 2025 17:47:57 +0700 Subject: [PATCH 06/28] NCP-3 --- docker-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 91cfd20..0ed4d65 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,8 +15,8 @@ services: container_name: postgres app: -# image: popovtsev/ncp - build: . + image: popovtsev/ncp +# build: . container_name: no_copy_app environment: POSTGRES_DB: no_copy_ From e6d7fa7aae6412c9e58294e187f1091609a6f389 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 27 Nov 2025 18:01:02 +0700 Subject: [PATCH 07/28] NCP-3 --- .../soune/no_copy/controller/AuthController.java | 11 +++++++++++ .../exception/TokenNotFoundException.java | 7 +++++++ .../no_copy/handler/GlobalExceptionHandler.java | 16 ++++++++++++---- 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/main/java/ru/soune/no_copy/exception/TokenNotFoundException.java 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..1c1350a 100644 --- a/src/main/java/ru/soune/no_copy/controller/AuthController.java +++ b/src/main/java/ru/soune/no_copy/controller/AuthController.java @@ -9,9 +9,12 @@ 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") @@ -20,6 +23,8 @@ 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/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 3e4efbe..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,10 +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.ContentNotFoundException; -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; @@ -75,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() + )); + } } From 3a9980fd861cbd7d04d6211215db76f22502c7cf Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 28 Nov 2025 14:30:25 +0700 Subject: [PATCH 08/28] NCP-3 --- src/main/java/ru/soune/no_copy/dto/LoginRequest.java | 2 ++ src/main/java/ru/soune/no_copy/dto/RegisterRequest.java | 1 - src/main/java/ru/soune/no_copy/service/AuthService.java | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) 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 20cef35..e08f39b 100644 --- a/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java +++ b/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java @@ -13,7 +13,6 @@ import java.time.LocalDate; public record RegisterRequest( @NotBlank(message = "error.name.length") - @Size(min = 2) String fullName, String companyName, 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 52eccfc..1cb1966 100644 --- a/src/main/java/ru/soune/no_copy/service/AuthService.java +++ b/src/main/java/ru/soune/no_copy/service/AuthService.java @@ -38,8 +38,7 @@ 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(); From 54857de238c647423c130f33e598a2b192ba0045 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 1 Dec 2025 16:46:44 +0700 Subject: [PATCH 09/28] set build from dockerfile --- docker-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 0ed4d65..91cfd20 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,8 +15,8 @@ services: container_name: postgres app: - image: popovtsev/ncp -# build: . +# image: popovtsev/ncp + build: . container_name: no_copy_app environment: POSTGRES_DB: no_copy_ From 2041f964a03fc0ff605ed12555e7364587dcc45d Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 1 Dec 2025 16:49:41 +0700 Subject: [PATCH 10/28] work build and deploy --- infra/deploy.groovy | 126 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 infra/deploy.groovy diff --git a/infra/deploy.groovy b/infra/deploy.groovy new file mode 100644 index 0000000..d772217 --- /dev/null +++ b/infra/deploy.groovy @@ -0,0 +1,126 @@ +pipeline { + agent any + + parameters { + string( + name: 'BRANCH', + defaultValue: 'dev', + description: 'Имя ветки' + ) + string( + name: 'DEPLOY_PATH', + defaultValue: '/opt/test-app', + description: 'Путь на сервере для деплоя' + ) + } + + stages { + stage('1. Checkout из Git') { + steps { + git( + url: 'https://code.3err0.ru/backdev/no-copy.git', + branch: params.BRANCH, + credentialsId: 'nx-jen' + ) + + echo "✓ Код успешно загружен из ветки: ${params.BRANCH}" + } + } + + stage('2. Проверка файлов') { + steps { + script { + sh """ + echo "=== ФАЙЛЫ В РЕПОЗИТОРИИ ===" + ls -la + + echo "" + echo "=== DOCKER-COMPOSE.YAML ===" + if [ -f "docker-compose.yaml" ]; then + echo "✓ docker-compose.yaml найден" + cat docker-compose.yaml + else + echo "✗ docker-compose.yaml не найден" + echo "Создаем тестовый docker-compose.yaml..." + cat > docker-compose.yaml << EOF + version: '3.8' + services: + web: + image: nginx:alpine + container_name: web-${params.BRANCH} + ports: + - "8080:80" + restart: unless-stopped + EOF + echo "Тестовый docker-compose.yaml создан" + fi + """ + } + } + } + + stage('3. Деплой на сервер (с паролем)') { + steps { + script { + echo "=== ДЕПЛОЙ НА СЕРВЕР ===" + echo "Сервер: 92.242.61.23" + echo "Путь: ${params.DEPLOY_PATH}" + echo "Ветка: ${params.BRANCH}" + } + + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + script { + sh """ + echo "Начинаем деплой..." + + # Устанавливаем sshpass если нет + if ! command -v sshpass &> /dev/null; then + echo "Устанавливаем sshpass..." + apt-get update && apt-get install -y sshpass + fi + + # 1. Создаем папку на сервере + sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${SSH_USER}@92.242.61.23 " + echo 'Создаю папку ${params.DEPLOY_PATH}...' + mkdir -p ${params.DEPLOY_PATH} + echo 'Папка создана' + " + + # 2. Копируем docker-compose.yml + echo 'Копирую docker-compose.yml...' + sshpass -p '${SSH_PASS}' scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 docker-compose.yml ${SSH_USER}@92.242.61.23:${params.DEPLOY_PATH}/ + + # 3. Запускаем docker-compose на сервере + echo 'Запускаю docker-compose на сервере...' + sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${SSH_USER}@92.242.61.23 " + cd ${params.DEPLOY_PATH} + echo 'Текущая директория: \$(pwd)' + ls -la + + echo 'Останавливаю старые контейнеры...' + docker-compose down 2>/dev/null || echo '✓ Не было запущенных контейнеров' + + echo 'Запускаю новые контейнеры...' + docker-compose up -d + + echo 'Проверяю статус...' + docker-compose ps + + echo 'Логи последние 5 строк:' + docker-compose logs --tail=5 + " + + echo "Деплой завершен" + """ + } + } + } + } + } +} \ No newline at end of file From 6ff7cdeb53836cc2d5af86a96019ee1fed2927b7 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 1 Dec 2025 17:35:47 +0700 Subject: [PATCH 11/28] change server port --- infra/deploy.groovy | 216 ++++++++++++++++++++++++++++---------------- 1 file changed, 136 insertions(+), 80 deletions(-) diff --git a/infra/deploy.groovy b/infra/deploy.groovy index d772217..cc9b146 100644 --- a/infra/deploy.groovy +++ b/infra/deploy.groovy @@ -9,13 +9,18 @@ pipeline { ) string( name: 'DEPLOY_PATH', - defaultValue: '/opt/test-app', - description: 'Путь на сервере для деплоя' + defaultValue: '/opt/myapp', + description: 'Путь на сервере' + ) + string( + name: 'APP_PORT', + defaultValue: '3000', + description: 'Порт приложения' ) } stages { - stage('1. Checkout из Git') { + stage('1. Получение кода') { steps { git( url: 'https://code.3err0.ru/backdev/no-copy.git', @@ -23,100 +28,151 @@ pipeline { credentialsId: 'nx-jen' ) - echo "✓ Код успешно загружен из ветки: ${params.BRANCH}" - } - } - - stage('2. Проверка файлов') { - steps { - script { - sh """ - echo "=== ФАЙЛЫ В РЕПОЗИТОРИИ ===" - ls -la - - echo "" - echo "=== DOCKER-COMPOSE.YAML ===" - if [ -f "docker-compose.yaml" ]; then - echo "✓ docker-compose.yaml найден" - cat docker-compose.yaml - else - echo "✗ docker-compose.yaml не найден" - echo "Создаем тестовый docker-compose.yaml..." - cat > docker-compose.yaml << EOF - version: '3.8' - services: - web: - image: nginx:alpine - container_name: web-${params.BRANCH} - ports: - - "8080:80" - restart: unless-stopped - EOF - echo "Тестовый docker-compose.yaml создан" - fi - """ - } - } - } - - stage('3. Деплой на сервер (с паролем)') { - steps { - script { - echo "=== ДЕПЛОЙ НА СЕРВЕР ===" - echo "Сервер: 92.242.61.23" - echo "Путь: ${params.DEPLOY_PATH}" + sh """ echo "Ветка: ${params.BRANCH}" - } + echo "Файлы:" + ls -la + """ + } + } - withCredentials([ - usernamePassword( - credentialsId: 'server-root-password', - usernameVariable: 'SSH_USER', - passwordVariable: 'SSH_PASS' - ) - ]) { - script { + stage('2. Проверка проекта') { + steps { + script { + echo "=== ПРОВЕРКА ПРОЕКТА ===" + + // Проверяем необходимые файлы + if (!fileExists('docker-compose.yml')) { + error "❌ docker-compose.yml не найден!" + } + + // Проверяем нужно ли собирать образ + def composeContent = readFile('docker-compose.yml') + def needsBuild = composeContent.contains('build:') || fileExists('Dockerfile') + + if (needsBuild) { + echo "✓ Нужно собирать Docker образ" + if (fileExists('Dockerfile')) { + echo "✓ Dockerfile найден" + } + } else { + echo "✓ Используются готовые образы" + } + } + } + } + + // ШАГ 3: Подготовка сервера + stage('3. Подготовка сервера') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { sh """ - echo "Начинаем деплой..." + echo "Подготавливаю сервер..." - # Устанавливаем sshpass если нет - if ! command -v sshpass &> /dev/null; then - echo "Устанавливаем sshpass..." - apt-get update && apt-get install -y sshpass - fi + # Проверяем Docker Compose + sshpass -p '${SSH_PASS}' ssh ${SSH_USER}@92.242.61.23 " + echo 'Проверяю Docker...' + docker --version || echo 'Docker не установлен' + + # Устанавливаем Docker Compose если нужно + if ! command -v docker-compose &> /dev/null; then + echo 'Устанавливаю Docker Compose...' + apt-get update && apt-get install -y docker-compose + fi + docker-compose --version + " + """ + } + } + } + } + + // ШАГ 4: Копирование файлов + stage('4. Копирование файлов на сервер') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + echo "Копирую файлы на сервер..." - # 1. Создаем папку на сервере - sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${SSH_USER}@92.242.61.23 " - echo 'Создаю папку ${params.DEPLOY_PATH}...' + # 1. Очищаем старую папку + sshpass -p '${SSH_PASS}' ssh ${SSH_USER}@92.242.61.23 " + rm -rf ${params.DEPLOY_PATH} 2>/dev/null || true mkdir -p ${params.DEPLOY_PATH} - echo 'Папка создана' " - # 2. Копируем docker-compose.yml - echo 'Копирую docker-compose.yml...' - sshpass -p '${SSH_PASS}' scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 docker-compose.yml ${SSH_USER}@92.242.61.23:${params.DEPLOY_PATH}/ + # 2. Копируем ВЕСЬ проект + echo "Копирую проект..." + tar czf - . | sshpass -p '${SSH_PASS}' ssh ${SSH_USER}@92.242.61.23 "tar xzf - -C ${params.DEPLOY_PATH}" - # 3. Запускаем docker-compose на сервере - echo 'Запускаю docker-compose на сервере...' - sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${SSH_USER}@92.242.61.23 " + # 3. Проверяем что скопировалось + sshpass -p '${SSH_PASS}' ssh ${SSH_USER}@92.242.61.23 " + echo 'На сервере в ${params.DEPLOY_PATH}:' + cd ${params.DEPLOY_PATH} && ls -la + " + """ + } + } + } + } + + // ШАГ 5: Запуск приложения + stage('5. Запуск приложения') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + echo "Запускаю приложение на сервере..." + + sshpass -p '${SSH_PASS}' ssh ${SSH_USER}@92.242.61.23 " cd ${params.DEPLOY_PATH} - echo 'Текущая директория: \$(pwd)' - ls -la + echo '=== НАЧАЛО ЗАПУСКА ===' + echo 'Директория: \$(pwd)' + + # Останавливаем старые контейнеры echo 'Останавливаю старые контейнеры...' - docker-compose down 2>/dev/null || echo '✓ Не было запущенных контейнеров' + docker-compose down 2>/dev/null || echo 'Не было запущенных контейнеров' - echo 'Запускаю новые контейнеры...' - docker-compose up -d + # Запускаем новые + echo 'Запускаю приложение...' + docker-compose up -d --build - echo 'Проверяю статус...' + # Ждем запуска + sleep 5 + + # Проверяем + echo '=== ПРОВЕРКА ===' + echo '1. Контейнеры:' docker-compose ps - echo 'Логи последние 5 строк:' - docker-compose logs --tail=5 + echo '' + echo '2. Логи:' + docker-compose logs --tail=10 + + echo '' + echo '3. Доступность:' + curl -f http://localhost:${params.APP_PORT} && echo '✓ Приложение работает!' || echo '✗ Ошибка доступа' " - - echo "Деплой завершен" """ } } From 641dea1f6d35e13047b9a8ab0624eb44f5fc8e6c Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 1 Dec 2025 17:36:28 +0700 Subject: [PATCH 12/28] change server port --- docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 91cfd20..8d2017b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -27,7 +27,7 @@ services: depends_on: - db ports: - - "8080:8080" + - "80:8080" grafana: image: grafana/grafana:10.3.1 From d54bff1a91f93878d579d8f64d3e8982771a77c1 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 2 Dec 2025 13:57:07 +0700 Subject: [PATCH 13/28] add user by mail endpoint --- .../no_copy/controller/UserController.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) 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 f4db208..6e6c3bb 100644 --- a/src/main/java/ru/soune/no_copy/controller/UserController.java +++ b/src/main/java/ru/soune/no_copy/controller/UserController.java @@ -42,6 +42,38 @@ public class UserController { 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, From 2997d6161659f83a033c271ebf095c8db997e5e3 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 2 Dec 2025 15:17:11 +0700 Subject: [PATCH 14/28] fix grafana environmebnt type --- docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 8d2017b..036eb8d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -44,7 +44,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" From cf6034e74a2ff5ac4bfeceace287dd8d8a4babd5 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 2 Dec 2025 16:40:52 +0700 Subject: [PATCH 15/28] Completed deploy job with works backend apps --- infra/deploy.groovy | 182 ----------------------- infrastructure/jenkins/deploy.groovy | 212 +++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 182 deletions(-) delete mode 100644 infra/deploy.groovy create mode 100644 infrastructure/jenkins/deploy.groovy diff --git a/infra/deploy.groovy b/infra/deploy.groovy deleted file mode 100644 index cc9b146..0000000 --- a/infra/deploy.groovy +++ /dev/null @@ -1,182 +0,0 @@ -pipeline { - agent any - - parameters { - string( - name: 'BRANCH', - defaultValue: 'dev', - description: 'Имя ветки' - ) - string( - name: 'DEPLOY_PATH', - defaultValue: '/opt/myapp', - description: 'Путь на сервере' - ) - string( - name: 'APP_PORT', - defaultValue: '3000', - description: 'Порт приложения' - ) - } - - stages { - stage('1. Получение кода') { - steps { - git( - url: 'https://code.3err0.ru/backdev/no-copy.git', - branch: params.BRANCH, - credentialsId: 'nx-jen' - ) - - sh """ - echo "Ветка: ${params.BRANCH}" - echo "Файлы:" - ls -la - """ - } - } - - stage('2. Проверка проекта') { - steps { - script { - echo "=== ПРОВЕРКА ПРОЕКТА ===" - - // Проверяем необходимые файлы - if (!fileExists('docker-compose.yml')) { - error "❌ docker-compose.yml не найден!" - } - - // Проверяем нужно ли собирать образ - def composeContent = readFile('docker-compose.yml') - def needsBuild = composeContent.contains('build:') || fileExists('Dockerfile') - - if (needsBuild) { - echo "✓ Нужно собирать Docker образ" - if (fileExists('Dockerfile')) { - echo "✓ Dockerfile найден" - } - } else { - echo "✓ Используются готовые образы" - } - } - } - } - - // ШАГ 3: Подготовка сервера - stage('3. Подготовка сервера') { - steps { - script { - withCredentials([ - usernamePassword( - credentialsId: 'server-root-password', - usernameVariable: 'SSH_USER', - passwordVariable: 'SSH_PASS' - ) - ]) { - sh """ - echo "Подготавливаю сервер..." - - # Проверяем Docker Compose - sshpass -p '${SSH_PASS}' ssh ${SSH_USER}@92.242.61.23 " - echo 'Проверяю Docker...' - docker --version || echo 'Docker не установлен' - - # Устанавливаем Docker Compose если нужно - if ! command -v docker-compose &> /dev/null; then - echo 'Устанавливаю Docker Compose...' - apt-get update && apt-get install -y docker-compose - fi - docker-compose --version - " - """ - } - } - } - } - - // ШАГ 4: Копирование файлов - stage('4. Копирование файлов на сервер') { - steps { - script { - withCredentials([ - usernamePassword( - credentialsId: 'server-root-password', - usernameVariable: 'SSH_USER', - passwordVariable: 'SSH_PASS' - ) - ]) { - sh """ - echo "Копирую файлы на сервер..." - - # 1. Очищаем старую папку - sshpass -p '${SSH_PASS}' ssh ${SSH_USER}@92.242.61.23 " - rm -rf ${params.DEPLOY_PATH} 2>/dev/null || true - mkdir -p ${params.DEPLOY_PATH} - " - - # 2. Копируем ВЕСЬ проект - echo "Копирую проект..." - tar czf - . | sshpass -p '${SSH_PASS}' ssh ${SSH_USER}@92.242.61.23 "tar xzf - -C ${params.DEPLOY_PATH}" - - # 3. Проверяем что скопировалось - sshpass -p '${SSH_PASS}' ssh ${SSH_USER}@92.242.61.23 " - echo 'На сервере в ${params.DEPLOY_PATH}:' - cd ${params.DEPLOY_PATH} && ls -la - " - """ - } - } - } - } - - // ШАГ 5: Запуск приложения - stage('5. Запуск приложения') { - steps { - script { - withCredentials([ - usernamePassword( - credentialsId: 'server-root-password', - usernameVariable: 'SSH_USER', - passwordVariable: 'SSH_PASS' - ) - ]) { - sh """ - echo "Запускаю приложение на сервере..." - - sshpass -p '${SSH_PASS}' ssh ${SSH_USER}@92.242.61.23 " - cd ${params.DEPLOY_PATH} - - echo '=== НАЧАЛО ЗАПУСКА ===' - echo 'Директория: \$(pwd)' - - # Останавливаем старые контейнеры - echo 'Останавливаю старые контейнеры...' - docker-compose down 2>/dev/null || echo 'Не было запущенных контейнеров' - - # Запускаем новые - echo 'Запускаю приложение...' - docker-compose up -d --build - - # Ждем запуска - sleep 5 - - # Проверяем - echo '=== ПРОВЕРКА ===' - echo '1. Контейнеры:' - docker-compose ps - - echo '' - echo '2. Логи:' - docker-compose logs --tail=10 - - echo '' - echo '3. Доступность:' - curl -f http://localhost:${params.APP_PORT} && echo '✓ Приложение работает!' || echo '✗ Ошибка доступа' - " - """ - } - } - } - } - } -} \ No newline at end of file diff --git a/infrastructure/jenkins/deploy.groovy b/infrastructure/jenkins/deploy.groovy new file mode 100644 index 0000000..6e7b69b --- /dev/null +++ b/infrastructure/jenkins/deploy.groovy @@ -0,0 +1,212 @@ +pipeline { + agent any + + parameters { + string( + name: 'BRANCH', + defaultValue: 'dev', + 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@92.242.61.23 " + 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@92.242.61.23 " + rm -rf /opt/deployments/${params.BRANCH}/* + mkdir -p /opt/deployments/${params.BRANCH} + " + + # Copy all files + sshpass -p '$SSH_PASS' scp -r ./* $SSH_USER@92.242.61.23:/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@92.242.61.23 " + 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@92.242.61.23 " + 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://92.242.61.23:80' + " + """ + } + } + } + } + } + + post { + success { + echo "Deploy branch ${params.BRANCH} completed" + echo "App : http://92.242.61.23:80" + } + failure { + echo "Deploy failed" + } + } +} \ No newline at end of file From f73f465aa328cd3df1cad20dd7862f2ad874d998 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 2 Dec 2025 17:06:04 +0700 Subject: [PATCH 16/28] add field with server --- infrastructure/jenkins/deploy.groovy | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/infrastructure/jenkins/deploy.groovy b/infrastructure/jenkins/deploy.groovy index 6e7b69b..c14fb90 100644 --- a/infrastructure/jenkins/deploy.groovy +++ b/infrastructure/jenkins/deploy.groovy @@ -7,6 +7,12 @@ pipeline { defaultValue: 'dev', description: 'Ветка для деплоя' ) + + string( + name: 'SERVER', + defaultValue: '92.242.61.23', + description: 'Сервер для деплоя' + ) } stages { @@ -38,7 +44,7 @@ pipeline { sh """ echo "Stop old container by branch ${params.BRANCH}..." - sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " + 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 @@ -76,13 +82,13 @@ pipeline { echo "Copy files on server..." # Clean mkdir - sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " + 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@92.242.61.23:/opt/deployments/${params.BRANCH}/ 2>/dev/null || true + sshpass -p '$SSH_PASS' scp -r ./* $SSH_USER@$SERVER:/opt/deployments/${params.BRANCH}/ 2>/dev/null || true echo "Files copied" """ @@ -104,7 +110,7 @@ pipeline { sh """ echo "Build on server..." - sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " + sshpass -p '$SSH_PASS' ssh $SSH_USER@$SERVER " cd /opt/deployments/${params.BRANCH} echo "Build Docker for branch ${params.BRANCH}..." @@ -148,7 +154,7 @@ pipeline { sh """ echo "Start container..." - sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " + sshpass -p '$SSH_PASS' ssh $SSH_USER@$SERVER " cd /opt/deployments/${params.BRANCH} # If have docker-compose - use them @@ -191,7 +197,7 @@ pipeline { echo '' echo 'Apps start on:' - echo 'http://92.242.61.23:80' + echo 'http://$SERVER:80' " """ } @@ -203,7 +209,7 @@ pipeline { post { success { echo "Deploy branch ${params.BRANCH} completed" - echo "App : http://92.242.61.23:80" + echo "App : http://$SERVER:80" } failure { echo "Deploy failed" From eccd5d94fc39ef2db6806726047496b76273f640 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 3 Dec 2025 12:41:56 +0700 Subject: [PATCH 17/28] add fron deploy job --- infrastructure/jenkins/deploy-front.groovy | 183 +++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 infrastructure/jenkins/deploy-front.groovy 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 From 575342a128a2493d71c68175d72e95732858122d Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 3 Dec 2025 13:20:09 +0700 Subject: [PATCH 18/28] add network --- docker-compose.yaml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 036eb8d..97475b7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,6 +13,8 @@ services: volumes: - pgdata:/var/lib/postgresql/data container_name: postgres + networks: + - app-network app: # image: popovtsev/ncp @@ -28,6 +30,8 @@ services: - db ports: - "80:8080" + networks: + - app-network grafana: image: grafana/grafana:10.3.1 @@ -56,6 +60,8 @@ services: interval: 10s timeout: 5s retries: 10 + networks: + - app-network prometheus: image: prom/prometheus:latest @@ -69,6 +75,8 @@ services: interval: 10s timeout: 5s retries: 5 + networks: + - app-network loki: image: grafana/loki:2.9.2 @@ -82,6 +90,8 @@ services: interval: 10s timeout: 5s retries: 5 + networks: + - app-network tempo: image: grafana/tempo:2.4.1 @@ -92,6 +102,8 @@ services: - tempo_data:/var/tempo ports: - "3200:3200" + networks: + - app-network alloy: image: grafana/alloy:latest @@ -113,6 +125,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 +135,7 @@ volumes: loki_data: loki_chunks: loki_index: - loki_rules: \ No newline at end of file + loki_rules: +networks: + app-network: + driver: bridge \ No newline at end of file From 96257c21bfffe26f613d6aeaad2ba175950bb154 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 3 Dec 2025 14:51:14 +0700 Subject: [PATCH 19/28] add network --- docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 97475b7..1c18eac 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -29,7 +29,7 @@ services: depends_on: - db ports: - - "80:8080" + - "81:8080" networks: - app-network From 36694afd455bae7b4f43a019c27b8e2168e79a5b Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 3 Dec 2025 15:44:30 +0700 Subject: [PATCH 20/28] delete network --- docker-compose.yaml | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 1c18eac..657a6d8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,8 +13,7 @@ services: volumes: - pgdata:/var/lib/postgresql/data container_name: postgres - networks: - - app-network + app: # image: popovtsev/ncp @@ -30,8 +29,7 @@ services: - db ports: - "81:8080" - networks: - - app-network + grafana: image: grafana/grafana:10.3.1 @@ -60,8 +58,7 @@ services: interval: 10s timeout: 5s retries: 10 - networks: - - app-network + prometheus: image: prom/prometheus:latest @@ -75,8 +72,7 @@ services: interval: 10s timeout: 5s retries: 5 - networks: - - app-network + loki: image: grafana/loki:2.9.2 @@ -90,8 +86,7 @@ services: interval: 10s timeout: 5s retries: 5 - networks: - - app-network + tempo: image: grafana/tempo:2.4.1 @@ -102,8 +97,7 @@ services: - tempo_data:/var/tempo ports: - "3200:3200" - networks: - - app-network + alloy: image: grafana/alloy:latest @@ -125,8 +119,7 @@ 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: @@ -135,7 +128,4 @@ volumes: loki_data: loki_chunks: loki_index: - loki_rules: -networks: - app-network: - driver: bridge \ No newline at end of file + loki_rules: \ No newline at end of file From 069666879001c11384630a1dbb98e7c3e26a7996 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 3 Dec 2025 16:56:49 +0700 Subject: [PATCH 21/28] retutn network --- docker-compose.yaml | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 657a6d8..1532b2d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,11 +13,11 @@ services: volumes: - pgdata:/var/lib/postgresql/data container_name: postgres - + networks: + - app-network app: -# image: popovtsev/ncp - build: . + build: . container_name: no_copy_app environment: POSTGRES_DB: no_copy_ @@ -29,7 +29,8 @@ services: - db ports: - "81:8080" - + networks: + - app-network grafana: image: grafana/grafana:10.3.1 @@ -58,7 +59,8 @@ services: interval: 10s timeout: 5s retries: 10 - + networks: + - app-network prometheus: image: prom/prometheus:latest @@ -72,7 +74,8 @@ services: interval: 10s timeout: 5s retries: 5 - + networks: + - app-network loki: image: grafana/loki:2.9.2 @@ -86,7 +89,8 @@ services: interval: 10s timeout: 5s retries: 5 - + networks: + - app-network tempo: image: grafana/tempo:2.4.1 @@ -97,16 +101,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 @@ -119,7 +124,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: @@ -128,4 +134,8 @@ volumes: loki_data: loki_chunks: loki_index: - loki_rules: \ No newline at end of file + loki_rules: + +networks: + app-network: + driver: bridge \ No newline at end of file From e8b17031ec42f78b420bf42cc686ba0a4e198c33 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 4 Dec 2025 10:26:00 +0700 Subject: [PATCH 22/28] retutn network --- docker-compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 1532b2d..cff0c4c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -138,4 +138,5 @@ volumes: networks: app-network: + external: true driver: bridge \ No newline at end of file From 6f90835c27f5cb7215303037deac45cec361b9c0 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 4 Dec 2025 10:50:26 +0700 Subject: [PATCH 23/28] retutn network --- docker-compose.yaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index cff0c4c..9bab4a7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,11 +14,13 @@ services: - pgdata:/var/lib/postgresql/data container_name: postgres networks: - - app-network + app-network: + aliases: + - database app: build: . - container_name: no_copy_app + container_name: app-backend environment: POSTGRES_DB: no_copy_ POSTGRES_USER: postgres @@ -30,7 +32,11 @@ services: ports: - "81:8080" networks: - - app-network + app-network: + aliases: + - app + - backend + - api grafana: image: grafana/grafana:10.3.1 From 243c32be724dd4deebf0a28f8f31eb85eb9d5a57 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 4 Dec 2025 14:15:50 +0700 Subject: [PATCH 24/28] retutn port 80 --- docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 9bab4a7..13e1b65 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -30,7 +30,7 @@ services: depends_on: - db ports: - - "81:8080" + - "80:8080" networks: app-network: aliases: From 5425681ac2572607e651f64d2237f9a1bc9a4205 Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 4 Dec 2025 15:09:47 +0700 Subject: [PATCH 25/28] add cookie --- docker-compose.yaml | 4 ++ .../no_copy/configuration/CorsConfig.java | 34 +++++++++++++++ src/main/resources/application.yaml | 41 +++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 src/main/java/ru/soune/no_copy/configuration/CorsConfig.java diff --git a/docker-compose.yaml b/docker-compose.yaml index 13e1b65..76905c2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -27,6 +27,10 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_PORT: 5432 POSTGRES_HOST: db + SERVER_PORT: 8080 + CORS_ALLOWED_ORIGINS: http://92.242.61.23:2998,http://localhost:3000,http://localhost:2998 + SPRING_SESSION_COOKIE_SAMESITE: none + SPRING_SESSION_COOKIE_SECURE: false depends_on: - db ports: diff --git a/src/main/java/ru/soune/no_copy/configuration/CorsConfig.java b/src/main/java/ru/soune/no_copy/configuration/CorsConfig.java new file mode 100644 index 0000000..0bb8259 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/configuration/CorsConfig.java @@ -0,0 +1,34 @@ +package ru.soune.no_copy.configuration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Arrays; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + + config.setAllowedOrigins(Arrays.asList( + "http://92.242.61.23:2998", + "http://localhost:3000", + "http://localhost:2998" + )); + + config.setAllowedMethods(Arrays.asList("*")); + config.setAllowedHeaders(Arrays.asList("*")); + config.setExposedHeaders(Arrays.asList("Set-Cookie", "Authorization")); + config.setAllowCredentials(true); // ВАЖНО! + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return new CorsFilter(source); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 2b84ef5..a33cc4c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,4 +1,27 @@ spring: + session: + timeout: 7d + cookie: + name: SESSION_ID + http-only: true + secure: false + same-site: none + max-age: 604800 + + cors: + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://92.242.61.23:2998,http://localhost:3000} + allowed-methods: GET,POST,PUT,DELETE,OPTIONS,PATCH,HEAD + allowed-headers: "*" + exposed-headers: Set-Cookie,Authorization,Content-Type + allow-credentials: true # ВАЖНО для кук! + max-age: 3600 + + logging: + level: + org.springframework.web.cors: DEBUG + org.springframework.security: DEBUG + org.springframework.web: DEBUG + datasource: url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} username: ${POSTGRES_USER} @@ -9,10 +32,28 @@ spring: hibernate: ddl-auto: update show-sql: true + properties: + hibernate: + format_sql: true server: port: ${SERVER_PORT:8080} + servlet: + session: + cookie: + same-site: none + encoding: + charset: UTF-8 + enabled: true logging: level: root: INFO + org.springframework.web.cors: TRACE # Более детальные логи + org.springframework.session: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + file: + name: ./logs/app.log + max-size: 10MB + max-history: 30 \ No newline at end of file From 82fb7394d215ce629d23e58567aec35a125335ef Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 4 Dec 2025 15:21:14 +0700 Subject: [PATCH 26/28] add cookie --- docker-compose.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 76905c2..3884313 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -27,10 +27,10 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_PORT: 5432 POSTGRES_HOST: db - SERVER_PORT: 8080 - CORS_ALLOWED_ORIGINS: http://92.242.61.23:2998,http://localhost:3000,http://localhost:2998 + SERVER_PORT: "8080" + CORS_ALLOWED_ORIGINS: "http://92.242.61.23:2998,http://localhost:3000,http://localhost:2998" SPRING_SESSION_COOKIE_SAMESITE: none - SPRING_SESSION_COOKIE_SECURE: false + SPRING_SESSION_COOKIE_SECURE: "false" depends_on: - db ports: From aabce4d64627048ca1e515d0511852cd975d45ba Mon Sep 17 00:00:00 2001 From: vladp Date: Thu, 4 Dec 2025 15:28:52 +0700 Subject: [PATCH 27/28] add cookie --- docker-compose.yaml | 4 -- .../no_copy/configuration/CorsConfig.java | 34 --------------- src/main/resources/application.yaml | 43 +------------------ 3 files changed, 1 insertion(+), 80 deletions(-) delete mode 100644 src/main/java/ru/soune/no_copy/configuration/CorsConfig.java diff --git a/docker-compose.yaml b/docker-compose.yaml index 3884313..13e1b65 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -27,10 +27,6 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_PORT: 5432 POSTGRES_HOST: db - SERVER_PORT: "8080" - CORS_ALLOWED_ORIGINS: "http://92.242.61.23:2998,http://localhost:3000,http://localhost:2998" - SPRING_SESSION_COOKIE_SAMESITE: none - SPRING_SESSION_COOKIE_SECURE: "false" depends_on: - db ports: diff --git a/src/main/java/ru/soune/no_copy/configuration/CorsConfig.java b/src/main/java/ru/soune/no_copy/configuration/CorsConfig.java deleted file mode 100644 index 0bb8259..0000000 --- a/src/main/java/ru/soune/no_copy/configuration/CorsConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package ru.soune.no_copy.configuration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; - -import java.util.Arrays; - -@Configuration -public class CorsConfig { - - @Bean - public CorsFilter corsFilter() { - CorsConfiguration config = new CorsConfiguration(); - - config.setAllowedOrigins(Arrays.asList( - "http://92.242.61.23:2998", - "http://localhost:3000", - "http://localhost:2998" - )); - - config.setAllowedMethods(Arrays.asList("*")); - config.setAllowedHeaders(Arrays.asList("*")); - config.setExposedHeaders(Arrays.asList("Set-Cookie", "Authorization")); - config.setAllowCredentials(true); // ВАЖНО! - config.setMaxAge(3600L); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - - return new CorsFilter(source); - } -} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index a33cc4c..e1e88fd 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,27 +1,4 @@ spring: - session: - timeout: 7d - cookie: - name: SESSION_ID - http-only: true - secure: false - same-site: none - max-age: 604800 - - cors: - allowed-origins: ${CORS_ALLOWED_ORIGINS:http://92.242.61.23:2998,http://localhost:3000} - allowed-methods: GET,POST,PUT,DELETE,OPTIONS,PATCH,HEAD - allowed-headers: "*" - exposed-headers: Set-Cookie,Authorization,Content-Type - allow-credentials: true # ВАЖНО для кук! - max-age: 3600 - - logging: - level: - org.springframework.web.cors: DEBUG - org.springframework.security: DEBUG - org.springframework.web: DEBUG - datasource: url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} username: ${POSTGRES_USER} @@ -32,28 +9,10 @@ spring: hibernate: ddl-auto: update show-sql: true - properties: - hibernate: - format_sql: true server: port: ${SERVER_PORT:8080} - servlet: - session: - cookie: - same-site: none - encoding: - charset: UTF-8 - enabled: true logging: level: - root: INFO - org.springframework.web.cors: TRACE # Более детальные логи - org.springframework.session: DEBUG - pattern: - console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" - file: - name: ./logs/app.log - max-size: 10MB - max-history: 30 \ No newline at end of file + root: INFO \ No newline at end of file From 182de806a8b1dc1fc98827ab5c7f33ba22529f0c Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 5 Dec 2025 11:45:51 +0700 Subject: [PATCH 28/28] add controller versions --- src/main/java/ru/soune/no_copy/controller/AuthController.java | 2 +- .../ru/soune/no_copy/controller/UserContentController.java | 4 ++-- src/main/java/ru/soune/no_copy/controller/UserController.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 1c1350a..764b1a8 100644 --- a/src/main/java/ru/soune/no_copy/controller/AuthController.java +++ b/src/main/java/ru/soune/no_copy/controller/AuthController.java @@ -17,7 +17,7 @@ import java.util.Map; import java.util.Optional; @RestController -@RequestMapping("/api/auth") +@RequestMapping("v1/api/auth") @RequiredArgsConstructor public class AuthController { diff --git a/src/main/java/ru/soune/no_copy/controller/UserContentController.java b/src/main/java/ru/soune/no_copy/controller/UserContentController.java index 49eac67..ea25e8f 100644 --- a/src/main/java/ru/soune/no_copy/controller/UserContentController.java +++ b/src/main/java/ru/soune/no_copy/controller/UserContentController.java @@ -4,8 +4,8 @@ import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@RestController("api/content") -@RequestMapping("api/content") +@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 6e6c3bb..9f23605 100644 --- a/src/main/java/ru/soune/no_copy/controller/UserController.java +++ b/src/main/java/ru/soune/no_copy/controller/UserController.java @@ -19,7 +19,7 @@ import java.util.List; import java.util.Optional; @RestController -@RequestMapping("/api/user") +@RequestMapping("v1/api/user") @RequiredArgsConstructor public class UserController {