@@ -1,10 +1,19 @@
|
|||||||
|
# Postgres
|
||||||
POSTGRES_DB=no_copy_
|
POSTGRES_DB=no_copy_
|
||||||
POSTGRES_USER=ncp_db
|
POSTGRES_USER=ncp_db
|
||||||
POSTGRES_PASSWORD=ncpDbApp
|
POSTGRES_PASSWORD=ncpDbApp
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_HOST=postgres
|
POSTGRES_HOST=postgres
|
||||||
|
|
||||||
|
# Redis
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
SERVER_PORT=8080
|
# Backend server
|
||||||
|
SERVER_PORT=8080
|
||||||
|
|
||||||
|
# Postfix
|
||||||
|
MAIL_HOST_PROD=postfix-production
|
||||||
|
MAIL_PORT_PROD=25
|
||||||
|
MAIL_USERNAME_PROD=noreply@nocopy.com
|
||||||
|
SMTP_PASSWORD_PROD=nocopy!nocopy!
|
||||||
+37
-182
@@ -1,15 +1,32 @@
|
|||||||
version: '3.9'
|
version: '3.9'
|
||||||
|
|
||||||
services:
|
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:
|
storage:
|
||||||
image: alpine:latest
|
image: alpine:latest
|
||||||
container_name: file-storage
|
container_name: file-storage
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 128M
|
|
||||||
reservations:
|
|
||||||
memory: 64M
|
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
volumes:
|
volumes:
|
||||||
@@ -19,19 +36,9 @@ services:
|
|||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:17.7
|
image: postgres:17.7
|
||||||
restart: always
|
container_name: postgres
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '2.0'
|
|
||||||
memory: 2G
|
|
||||||
reservations:
|
|
||||||
cpus: '1.0'
|
|
||||||
memory: 1G
|
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: no_copy_
|
POSTGRES_DB: no_copy_
|
||||||
# POSTGRES_USER: postgres
|
|
||||||
# POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_USER: ${POSTGRES_USER}
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
POSTGRES_SHARED_BUFFERS: 512MB
|
POSTGRES_SHARED_BUFFERS: 512MB
|
||||||
@@ -41,42 +48,36 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
- ./init-scripts:/docker-entrypoint-initdb.d:ro
|
- ./init-scripts:/docker-entrypoint-initdb.d:ro
|
||||||
container_name: postgres
|
|
||||||
networks:
|
networks:
|
||||||
app-network:
|
app-network:
|
||||||
aliases:
|
aliases:
|
||||||
- database
|
- database
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
container_name: app-backend
|
container_name: app-backend
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '1.5'
|
|
||||||
memory: 1G
|
|
||||||
reservations:
|
|
||||||
cpus: '0.5'
|
|
||||||
memory: 512M
|
|
||||||
environment:
|
environment:
|
||||||
FILE_STORAGE_PATH: /data/uploads
|
FILE_STORAGE_PATH: /data/uploads
|
||||||
MAX_FILE_SIZE: 10737418240
|
MAX_FILE_SIZE: 10737418240
|
||||||
# FILE_CHUNK_SIZE: 1048576
|
|
||||||
FILE_CHUNK_SIZE: 1000000
|
FILE_CHUNK_SIZE: 1000000
|
||||||
POSTGRES_DB: no_copy_
|
POSTGRES_DB: no_copy_
|
||||||
# POSTGRES_USER: postgres
|
|
||||||
# POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_USER: ${POSTGRES_USER}
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
POSTGRES_PORT: 5432
|
POSTGRES_PORT: 5432
|
||||||
POSTGRES_HOST: db
|
POSTGRES_HOST: db
|
||||||
STORAGE_SERVICE_URL: http://storage:8081
|
STORAGE_SERVICE_URL: http://storage:8081
|
||||||
SPRING_PROFILES_ACTIVE: docker
|
SPRING_PROFILES_ACTIVE: docker
|
||||||
YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q
|
MAIL_HOST: postfix
|
||||||
YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd
|
MAIL_PORT: 25
|
||||||
YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image"
|
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:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
- postfix
|
||||||
ports:
|
ports:
|
||||||
- "80:8080"
|
- "80:8080"
|
||||||
networks:
|
networks:
|
||||||
@@ -88,163 +89,17 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- uploads_data:/data/uploads:rw
|
- uploads_data:/data/uploads:rw
|
||||||
healthcheck:
|
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
|
interval: 10s
|
||||||
retries: 5
|
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:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
artifacts:
|
|
||||||
tempo_data:
|
|
||||||
loki_data:
|
|
||||||
loki_chunks:
|
|
||||||
loki_index:
|
|
||||||
loki_rules:
|
|
||||||
uploads_data:
|
uploads_data:
|
||||||
prometheus_data:
|
postfix-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
app-network:
|
app-network:
|
||||||
external: true
|
external: true
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ public class User {
|
|||||||
@Column(name = "email", nullable = false, length = 1024, unique = true)
|
@Column(name = "email", nullable = false, length = 1024, unique = true)
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
@Column(name = "email_verified", nullable = false)
|
@Column(name = "email_verified")
|
||||||
private Boolean emailVerified = false;
|
private boolean emailVerified = false;
|
||||||
|
|
||||||
@Column(name = "company")
|
@Column(name = "company")
|
||||||
private String company;
|
private String company;
|
||||||
@@ -82,11 +82,6 @@ public class User {
|
|||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
private List<UserContent> userContents = new ArrayList<>();
|
private List<UserContent> userContents = new ArrayList<>();
|
||||||
|
|
||||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
||||||
@JsonIgnore
|
|
||||||
@ToString.Exclude
|
|
||||||
private List<UserAction> userActions = new ArrayList<>();
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package ru.soune.nocopy.entity.user;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import jakarta.mail.MessagingException;
|
|||||||
import jakarta.mail.internet.MimeMessage;
|
import jakarta.mail.internet.MimeMessage;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.mail.javamail.JavaMailSender;
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||||
@@ -35,16 +36,17 @@ public class EmailService {
|
|||||||
|
|
||||||
helper.setFrom("NO COPY <noreply@nocopy.com>");
|
helper.setFrom("NO COPY <noreply@nocopy.com>");
|
||||||
helper.setTo(user.getEmail());
|
helper.setTo(user.getEmail());
|
||||||
helper.setSubject("NO COPY - Подтверждение email адреса");
|
helper.setSubject("NO COPY - Подтверждение email");
|
||||||
|
|
||||||
String htmlContent = loadAndProcessTemplate(user.getFullName(), code);
|
String htmlContent = loadAndProcessTemplate(user.getFullName(), code);
|
||||||
|
|
||||||
helper.setText(htmlContent, true);
|
helper.setText(htmlContent, true);
|
||||||
|
|
||||||
mailSender.send(message);
|
mailSender.send(message);
|
||||||
|
|
||||||
} catch (MessagingException | IOException e) {
|
log.info("Email sent to: {}", user.getEmail());
|
||||||
log.error("Fail send email: " + e.getMessage());
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to send email: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
spring:
|
spring:
|
||||||
mail:
|
mail:
|
||||||
host: smtp.gmail.com
|
host: postfix
|
||||||
port: 587
|
port: 25
|
||||||
username: vlad.popovtsev@gmail.com
|
username: noreply@nocopy.com
|
||||||
password: aylgtjcxdtjvohrk
|
password: nocopy!nocopy!
|
||||||
properties:
|
properties:
|
||||||
mail:
|
mail.smtp.auth: false
|
||||||
smtp:
|
mail.smtp.starttls.enable: false
|
||||||
auth: true
|
mail.debug: true
|
||||||
starttls:
|
|
||||||
enable: true
|
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||||
@@ -36,8 +34,6 @@ spring:
|
|||||||
file:
|
file:
|
||||||
storage:
|
storage:
|
||||||
base-path: ${FILE_STORAGE_PATH:/data/uploads}
|
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
|
chunk-size: ${FILE_CHUNK_SIZE:1000000} # 1MB
|
||||||
max-file-size: ${MAX_FILE_SIZE:10737418240} # 10GB
|
max-file-size: ${MAX_FILE_SIZE:10737418240} # 10GB
|
||||||
max-retry-attempts: ${MAX_RETRY_ATTEMPTS:3}
|
max-retry-attempts: ${MAX_RETRY_ATTEMPTS:3}
|
||||||
@@ -69,12 +65,3 @@ yandex:
|
|||||||
api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q}
|
api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q}
|
||||||
folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd}
|
folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd}
|
||||||
search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image}
|
search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image}
|
||||||
|
|
||||||
app:
|
|
||||||
email:
|
|
||||||
verification:
|
|
||||||
expiration-hours: ${EMAIL_VERIFICATION_EXPIRATION:24}
|
|
||||||
base-url: ${EMAIL_BASE_URL:http://localhost:80}
|
|
||||||
from-email: ${EMAIL_FROM:noreply@yourdomain.com}
|
|
||||||
templates:
|
|
||||||
verification: verification-email.html
|
|
||||||
Reference in New Issue
Block a user