NCBACK-33 #16

Merged
backdev merged 13 commits from NCBACK-33 into dev 2026-01-27 09:11:31 +08:00
44 changed files with 443 additions and 147 deletions
Showing only changes of commit 55fa8b84e2 - Show all commits
+213 -38
View File
@@ -1,32 +1,15 @@
version: '3.9'
services:
postfix:
image: boky/postfix:latest
container_name: postfix-production
hostname: ${DOMAIN:-no-copy.ru}
environment:
- DOMAIN=${DOMAIN:-no-copy.ru}
- ALLOWED_SENDER_DOMAINS=*
- RELAYHOST=
- DISABLE_SMTP_AUTH=
- SMTP_USERNAME=${MAIL_USERNAME:-noreply@no-copy.ru}
- SMTP_PASSWORD=${SMTP_PASSWORD:-nocopy!nocopy!}
- RELAY_NETWORKS=172.16.0.0/12
- POSTFIX_mynetworks=127.0.0.0/8 172.17.0.0/16 172.16.0.0/12
- POSTFIX_smtpd_relay_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
- POSTFIX_smtpd_recipient_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
- DISABLE_SIGNING=true
ports:
- "25:25"
- "587:587"
networks:
- app-network
restart: unless-stopped
storage:
image: alpine:latest
container_name: file-storage
deploy:
resources:
limits:
memory: 128M
reservations:
memory: 64M
networks:
- app-network
volumes:
@@ -34,11 +17,44 @@ services:
command: tail -f /dev/null
restart: unless-stopped
# postfix:
# image: boky/postfix:latest
# container_name: postfix-production
# hostname: ${DOMAIN:-no-copy.ru}
# environment:
# - DOMAIN=${DOMAIN:-no-copy.ru}
# - ALLOWED_SENDER_DOMAINS=*
# - RELAYHOST=
# - DISABLE_SMTP_AUTH=
# - SMTP_USERNAME=${MAIL_USERNAME:-noreply@no-copy.ru}
# - SMTP_PASSWORD=${SMTP_PASSWORD:-nocopy!nocopy!}
# - RELAY_NETWORKS=172.16.0.0/12
# - POSTFIX_mynetworks=127.0.0.0/8 172.17.0.0/16 172.16.0.0/12
# - POSTFIX_smtpd_relay_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
# - POSTFIX_smtpd_recipient_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
# - DISABLE_SIGNING=true
# ports:
# - "25:25"
# - "587:587"
# networks:
# - app-network
# restart: unless-stopped
db:
image: postgres:17.7
container_name: postgres
restart: always
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '1.0'
memory: 1G
environment:
POSTGRES_DB: no_copy_
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_SHARED_BUFFERS: 512MB
@@ -48,36 +64,49 @@ services:
volumes:
- pgdata:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro
container_name: postgres
networks:
app-network:
aliases:
- database
restart: unless-stopped
app:
build: .
container_name: app-backend
deploy:
resources:
limits:
cpus: '1.5'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
environment:
FILE_STORAGE_PATH: /data/uploads
MAX_FILE_SIZE: 10737418240
# FILE_CHUNK_SIZE: 1048576
FILE_CHUNK_SIZE: 1000000
POSTGRES_DB: no_copy_
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PORT: 5432
POSTGRES_HOST: db
STORAGE_SERVICE_URL: http://storage:8081
SPRING_PROFILES_ACTIVE: docker
MAIL_HOST: postfix
MAIL_PORT: 25
MAIL_USERNAME: noreply@no-copy.ru
SMTP_PASSWORD: nocopy!nocopy!
SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "false"
SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "false"
SPRING_MAIL_PROPERTIES_MAIL_DEBUG: "true"
YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q
YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd
YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image"
# MAIL_HOST: postfix
# MAIL_PORT: 25
# MAIL_USERNAME: noreply@no-copy.ru
# SMTP_PASSWORD: nocopy!nocopy!
# SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "false"
# SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "false"
# SPRING_MAIL_PROPERTIES_MAIL_DEBUG: "true"
depends_on:
- db
- postfix
ports:
- "80:8080"
networks:
@@ -89,17 +118,163 @@ services:
volumes:
- uploads_data:/data/uploads:rw
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d no_copy_"]
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d no_copy_" ]
interval: 10s
retries: 5
restart: unless-stopped
grafana:
image: grafana/grafana:10.3.1
container_name: grafana
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
memory: 256M
ports:
- "3000:3000"
depends_on:
prometheus:
condition: service_healthy
loki:
condition: service_healthy
tempo:
condition: service_started
alloy:
condition: service_started
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin
GF_METRICS_ENABLED: "true"
GF_DATABASE_MAX_IDLE_CONN: "2"
GF_DATABASE_MAX_OPEN_CONN: "10"
volumes:
- ./infrastructure/grafana/provisioning:/etc/grafana/provisioning
- ./infrastructure/grafana/dashboards:/var/lib/grafana/dashboards
healthcheck:
test: [ "CMD", "wget", "--spider", "http://localhost:3000/api/health" ]
interval: 10s
timeout: 5s
retries: 10
networks:
- app-network
prometheus:
image: prom/prometheus:latest
container_name: prometheus
deploy:
resources:
limits:
cpus: '0.5'
memory: 1G
reservations:
memory: 512M
ports:
- "9090:9090"
volumes:
- ./infrastructure/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=15d' # Удерживать 15 дней
- '--web.enable-lifecycle'
healthcheck:
test: [ "CMD", "wget", "--spider", "http://localhost:9090/-/healthy" ]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
loki:
image: grafana/loki:2.9.2
container_name: loki
deploy:
resources:
limits:
cpus: '1.0'
memory: 2G
reservations:
memory: 1G
ports:
- "3100:3100"
volumes:
- ./infrastructure/loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro
- loki_data:/loki
command:
- -config.file=/etc/loki/loki-config.yaml
healthcheck:
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3100/ready" ]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
tempo:
image: grafana/tempo:2.4.1
container_name: tempo
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
memory: 256M
command: [ "-config.file=/etc/tempo/tempo.yaml" ]
volumes:
- ./infrastructure/tempo/tempo.yaml:/etc/tempo/tempo.yaml
- tempo_data:/var/tempo
ports:
- "3200:3200"
networks:
- app-network
alloy:
image: grafana/alloy:latest
container_name: alloy
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
memory: 128M
user: root
ports:
- "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
- /var/run/docker.sock:/var/run/docker.sock
- /var/log:/var/log:ro
environment:
GRAFANA_LOKI_URL: http://loki:3100/loki/api/v1/push
command:
- run
- --server.http.listen-addr=0.0.0.0:9080
- --storage.path=/var/lib/alloy/data
- /etc/alloy/config.alloy
networks:
- app-network
volumes:
pgdata:
artifacts:
tempo_data:
loki_data:
loki_chunks:
loki_index:
loki_rules:
uploads_data:
postfix-data:
prometheus_data:
# postfix-data:
networks:
app-network:
external: true
driver: bridge
driver: bridge
@@ -17,7 +17,8 @@ public class HandlerConfig {
FileUploadHandler upload,
FileEntityHandler file,
LogoutRequestHandler logoutHandler,
ImageFoundRequestHandler imageFoundRequestHandler
ImageFoundRequestHandler imageFoundRequestHandler,
VerifyRegisterUserHandler verifyRegisterUser
) {
Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login);
@@ -26,6 +27,7 @@ public class HandlerConfig {
map.put(20005, file);
map.put(20006, logoutHandler);
map.put(20007, imageFoundRequestHandler);
map.put(20009, verifyRegisterUser);
return map;
}
@@ -14,7 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.RegAnswer;
import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.file.ChunkUploadResponse;
import ru.soune.nocopy.dto.file.CompleteUploadResponse;
import ru.soune.nocopy.dto.file.FileEntityResponse;
@@ -3,9 +3,9 @@ package ru.soune.nocopy.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.soune.nocopy.dto.ChangePasswordRequest;
import ru.soune.nocopy.dto.UserDTO;
import ru.soune.nocopy.dto.UserRequest;
import ru.soune.nocopy.dto.register.ChangePasswordRequest;
import ru.soune.nocopy.dto.user.UserDTO;
import ru.soune.nocopy.dto.user.UserRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.InvalidUserEmail;
@@ -13,7 +13,7 @@ import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.mapper.UserMapper;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.UserService;
import ru.soune.nocopy.service.user.UserService;
import java.util.List;
import java.util.Optional;
@@ -1,10 +0,0 @@
package ru.soune.nocopy.dto;
import java.time.LocalDateTime;
public record AuthResponse (
boolean success,
String message,
String token,
LocalDateTime expiresAt
) {}
@@ -1,14 +0,0 @@
package ru.soune.nocopy.dto;
import lombok.Data;
@Data
public class UserContentDTO {
String fileName;
String originalFilename;
String fileType;
String fileExtension;
Long fileSize;
String filePath;
Integer protectionLevel;
}
@@ -1,17 +0,0 @@
package ru.soune.nocopy.dto;
import ru.soune.nocopy.entity.ContentStatus;
import ru.soune.nocopy.entity.file.FileType;
import java.time.LocalDateTime;
public record UserContentResponse(
Long id,
String filename,
String originalFilename,
FileType fileType,
long fileSize,
String filePath,
ContentStatus status,
LocalDateTime uploadDate
) {}
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.Email;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.*;
@@ -0,0 +1,17 @@
package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class VerifyUserRequest {
@JsonProperty("verify_token")
private String verifyToken;
@JsonProperty("user_id")
private Long userId;
}
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.user;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.user;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
@@ -1,11 +1,11 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.user;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import ru.soune.nocopy.entity.user.GenderType;
import ru.soune.nocopy.entity.SubscriptionType;
import ru.soune.nocopy.entity.user.SubscriptionType;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.user;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.Email;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity;
package ru.soune.nocopy.entity.file;
import lombok.Getter;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity;
package ru.soune.nocopy.entity.file;
import jakarta.persistence.*;
import lombok.*;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity;
package ru.soune.nocopy.entity.file;
import jakarta.persistence.*;
import lombok.*;
@@ -24,7 +24,7 @@ public class EmailVerificationToken {
private String token;
@Column(name = "expires_at", nullable = false)
private LocalDateTime expiresAt;
private LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(15);
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity;
package ru.soune.nocopy.entity.user;
import lombok.Getter;
@@ -6,9 +6,8 @@ import jakarta.validation.constraints.Size;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.ImageProtection;
import ru.soune.nocopy.entity.SubscriptionType;
import ru.soune.nocopy.entity.Violation;
import ru.soune.nocopy.entity.file.ImageProtection;
import ru.soune.nocopy.entity.file.Violation;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.ContentStatus;
import ru.soune.nocopy.entity.file.ContentStatus;
import ru.soune.nocopy.entity.file.FileType;
import java.time.LocalDateTime;
@@ -3,14 +3,13 @@ package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.ImageSearchRequest;
import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.service.YandexSearchService;
import ru.soune.nocopy.service.search.YandexSearchService;
@Slf4j
@Component
@@ -4,10 +4,12 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.register.LoginAnswer;
import ru.soune.nocopy.dto.register.LoginRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.AuthService;
import ru.soune.nocopy.service.register.AuthService;
import java.util.Arrays;
import java.util.Map;
@@ -4,12 +4,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.register.LogOutAnswer;
import ru.soune.nocopy.dto.register.LogoutRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.AuthService;
import ru.soune.nocopy.service.register.AuthService;
import java.util.Objects;
import java.util.Optional;
@@ -7,20 +7,19 @@ import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.register.RegRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.exception.ValidationException;
import ru.soune.nocopy.handler.validator.RegRequestValidator;
import ru.soune.nocopy.repository.EmailVerificationTokenRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.AuthService;
import ru.soune.nocopy.service.EmailService;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.mail.EmailService;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
@Slf4j
@Component
@@ -36,8 +35,6 @@ public class RegRequestHandler implements RequestHandler {
private final EmailService emailService;
private final EmailVerificationTokenRepository emailVerificationTokenRepository;
@Override
public BaseResponse handle(BaseRequest request) throws ValidationException {
RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class);
@@ -0,0 +1,68 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.register.VerifyUserRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.exception.TokenNotFoundException;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.EmailVerificationTokenRepository;
import ru.soune.nocopy.service.user.UserService;
import java.util.Map;
import java.util.Optional;
@Component
@Slf4j
@RequiredArgsConstructor
public class VerifyRegisterUserHandler implements RequestHandler{
private final ObjectMapper objectMapper;
private final EmailVerificationTokenRepository emailVerificationTokenRepository;
private final UserService userService;
private final AuthTokenRepository authTokenRepository;
@Override
public BaseResponse handle(BaseRequest request) {
VerifyUserRequest verifyUserRequest = objectMapper.convertValue(request.getMessageBody(),
VerifyUserRequest.class);
String verifyToken = verifyUserRequest.getVerifyToken();
Long userId = verifyUserRequest.getUserId();
EmailVerificationToken emailVerificationToken =
emailVerificationTokenRepository.findByUserIdAndToken(userId, verifyToken);
if (emailVerificationToken == null) {
throw new NotValidFieldException("Token was incorrected:" + verifyToken,
new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(),
MessageCode.INVALID_TOKEN.getDescription(), Map.of(
"userId", userId,
"token", verifyToken)));
}
userService.activateAndVerifyUser(userId);
emailVerificationTokenRepository.delete(emailVerificationToken);
Optional<AuthToken> byUserIdAndIsActive = authTokenRepository.findByUserIdAndIsActive(userId, true);
AuthToken authToken = byUserIdAndIsActive.orElseThrow(() ->
new TokenNotFoundException("Token not found with user id: " + userId));
RegAnswer regAnswer = new RegAnswer();
regAnswer.setToken(authToken.getToken());
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), regAnswer);
}
}
@@ -1,14 +1,11 @@
package ru.soune.nocopy.handler.validator;
import org.apache.commons.validator.routines.DomainValidator;
import org.apache.commons.validator.routines.EmailValidator;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import ru.soune.nocopy.dto.RegRequest;
import ru.soune.nocopy.dto.register.RegRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
@Component
@@ -4,7 +4,7 @@ import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
import ru.soune.nocopy.dto.UserContentUpdateRequest;
import ru.soune.nocopy.dto.user.UserContentUpdateRequest;
import ru.soune.nocopy.entity.user.UserContent;
@Mapper(componentModel = "spring",
@@ -3,7 +3,7 @@ package ru.soune.nocopy.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
import ru.soune.nocopy.dto.UserDTO;
import ru.soune.nocopy.dto.user.UserDTO;
import ru.soune.nocopy.entity.user.User;
@Mapper(componentModel = "spring",
@@ -11,4 +11,5 @@ import java.util.Optional;
public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
Optional<AuthToken> findByTokenAndExpiresAtAfter(String token, LocalDate expiresAtAfter);
Optional<AuthToken> findByToken(String token);
Optional<AuthToken> findByUserIdAndIsActive(Long userId, boolean isActive);
}
@@ -3,8 +3,12 @@ package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
public interface EmailVerificationTokenRepository extends JpaRepository<EmailVerificationToken, Long> {
Optional<EmailVerificationToken> findByToken(String token);
EmailVerificationToken findByUserIdAndToken(Long userId, String token);
List<EmailVerificationToken> findByExpiresAtBefore(LocalDateTime expiresAtBefore);
}
@@ -1,6 +1,5 @@
package ru.soune.nocopy.service;
package ru.soune.nocopy.service.mail;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -29,14 +28,23 @@ public class EmailService {
private final EmailVerificationTokenRepository emailVerificationTokenRepository;
@Value("${app.email.verification.standart-subject}")
private String standartSubject;
@Value("${app.email.verification.standart-address-from}")
private String standartAdressFrom;
@Value("${app.email.verification.user}")
private String user;
public void sendVerificationEmail(User user, String code) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom("NO COPY <noreply@nocopy.com>");
helper.setFrom(standartAdressFrom);
helper.setTo(user.getEmail());
helper.setSubject("NO COPY - Подтверждение email");
helper.setSubject(standartSubject);
String htmlContent = loadAndProcessTemplate(user.getFullName(), code);
helper.setText(htmlContent, true);
@@ -55,7 +63,6 @@ public class EmailService {
EmailVerificationToken emailToken = new EmailVerificationToken();
emailToken.setUser(authToken.getUser());
emailToken.setToken(generateVerificationCode());
emailToken.setExpiresAt(LocalDateTime.now().plusMinutes(15));
emailToken.setCreatedAt(LocalDateTime.now());
return emailVerificationTokenRepository.save(emailToken);
@@ -71,7 +78,7 @@ public class EmailService {
private String loadAndProcessTemplate(String username, String code) throws IOException {
ClassPathResource resource = new ClassPathResource("templates/email-verification.html");
String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
String safeUsername = escapeHtml(username != null ? username : "Пользователь");
String safeUsername = escapeHtml(username != null ? username : user);
String safeCode = escapeHtml(code);
return template
@@ -1,4 +1,4 @@
package ru.soune.nocopy.service;
package ru.soune.nocopy.service.register;
import lombok.RequiredArgsConstructor;
import org.springframework.context.MessageSource;
@@ -6,6 +6,9 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.register.LoginAnswer;
import ru.soune.nocopy.dto.register.LoginRequest;
import ru.soune.nocopy.dto.register.RegRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.NotValidFieldException;
@@ -0,0 +1,31 @@
package ru.soune.nocopy.service.register;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import ru.soune.nocopy.repository.EmailVerificationTokenRepository;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CleanUpVerifyTokens {
private final EmailVerificationTokenRepository emailVerificationTokenRepository;
@Transactional
@Scheduled(fixedDelay = 30000)
public void cleanupExpiredTokens() {
LocalDateTime now = LocalDateTime.now();
List<EmailVerificationToken> expiredTokens = emailVerificationTokenRepository.findByExpiresAtBefore(now);
if (!expiredTokens.isEmpty()) {
emailVerificationTokenRepository.deleteAll(expiredTokens);
}
}
}
@@ -1,4 +1,4 @@
package ru.soune.nocopy.service;
package ru.soune.nocopy.service.search;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -12,7 +12,6 @@ import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.FileEntityRepository;
@@ -1,10 +1,10 @@
package ru.soune.nocopy.service;
package ru.soune.nocopy.service.user;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.UserContentRequest;
import ru.soune.nocopy.dto.UserContentUpdateRequest;
import ru.soune.nocopy.dto.user.UserContentRequest;
import ru.soune.nocopy.dto.user.UserContentUpdateRequest;
import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.entity.user.UserContent;
@@ -1,18 +1,20 @@
package ru.soune.nocopy.service;
package ru.soune.nocopy.service.user;
import lombok.AllArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.ChangePasswordRequest;
import ru.soune.nocopy.dto.UserDTO;
import ru.soune.nocopy.dto.UserRequest;
import ru.soune.nocopy.dto.register.ChangePasswordRequest;
import ru.soune.nocopy.dto.user.UserDTO;
import ru.soune.nocopy.dto.user.UserRequest;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.InvalidUserEmail;
import ru.soune.nocopy.exception.NotValidationPasswordException;
import ru.soune.nocopy.exception.UserNotFoundException;
import ru.soune.nocopy.repository.UserRepository;
import java.util.Optional;
@Service
@AllArgsConstructor
public class UserService {
@@ -44,6 +46,16 @@ public class UserService {
return mapToDTO(updatedUser);
}
@Transactional
public void activateAndVerifyUser(long userId) {
Optional<User> users = userRepository.findById(userId);
User user = users.orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId));
user.setIsActive(true);
user.setEmailVerified(true);
userRepository.save(user);
}
private void updateUserFromRequest(User user, UserRequest request) {
if (request.getFullName() != null) {
user.setFullName(request.getFullName());
+28 -7
View File
@@ -1,13 +1,25 @@
spring:
mail:
host: postfix
port: 25
username: noreply@nocopy.com
password: nocopy!nocopy!
host: smtp.gmail.com
port: 587
username: vlad.popovtsev@gmail.com
password: gocjwvrqrzlaatsr
properties:
mail.smtp.auth: false
mail.smtp.starttls.enable: false
mail.debug: true
mail:
smtp:
auth: true
starttls:
enable: true
# mail:
# host: postfix
# port: 25
# username: noreply@nocopy.com
# password: nocopy!nocopy!
# properties:
# mail.smtp.auth: false
# mail.smtp.starttls.enable: false
# mail.debug: true
datasource:
url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
@@ -34,6 +46,8 @@ spring:
file:
storage:
base-path: ${FILE_STORAGE_PATH:/data/uploads}
# chunk-size: ${FILE_CHUNK_SIZE:5242880} # 5MB
# chunk-size: ${FILE_CHUNK_SIZE:1048576} # 1MB
chunk-size: ${FILE_CHUNK_SIZE:1000000} # 1MB
max-file-size: ${MAX_FILE_SIZE:10737418240} # 10GB
max-retry-attempts: ${MAX_RETRY_ATTEMPTS:3}
@@ -65,3 +79,10 @@ yandex:
api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q}
folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd}
search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image}
app:
email:
verification:
standart-subject: NO COPY - Подтверждение email адреса
standart-address-from: NO COPY <noreply@nocopy.com>
user: Пользователь
@@ -8,13 +8,14 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.context.MessageSource;
import org.springframework.security.crypto.password.PasswordEncoder;
import ru.soune.nocopy.dto.LoginRequest;
import ru.soune.nocopy.dto.register.LoginRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.NotValidationPasswordException;
import ru.soune.nocopy.exception.UserNotFoundException;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.register.AuthService;
import java.util.Optional;