Backend migration
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package dev.rheinsw.contactService;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
/**
|
||||
* @author Bummsa / BoomerHD / Thatsaphorn Atchariyaphap
|
||||
* @since 21.04.25
|
||||
*/
|
||||
@EnableAsync
|
||||
@SpringBootApplication(
|
||||
scanBasePackages = {"dev.rheinsw"}
|
||||
)
|
||||
public class ContactServiceApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ContactServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package dev.rheinsw.contactService.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Thatsaphorn Atchariyaphap
|
||||
* @since 21.04.25
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "hcaptcha")
|
||||
public class HCaptchaConfig {
|
||||
private String secret;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package dev.rheinsw.contactService.controller;
|
||||
|
||||
import dev.rheinsw.contactService.dto.ContactRequestDto;
|
||||
import dev.rheinsw.contactService.model.ContactRequest;
|
||||
import dev.rheinsw.contactService.repository.ContactRequestsRepo;
|
||||
import dev.rheinsw.contactService.service.HCaptchaValidator;
|
||||
import dev.rheinsw.shared.mail.MailServiceClient;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* REST controller to handle contact form submissions.
|
||||
*
|
||||
* @author Thatsaphorn Atchariyaphap
|
||||
* @since 21.04.25
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("/contact")
|
||||
public class ContactController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ContactController.class);
|
||||
|
||||
private final HCaptchaValidator captchaValidator;
|
||||
private final ContactRequestsRepo contactRepository;
|
||||
private final MailServiceClient mailServiceClient;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<String> submitContact(@RequestBody ContactRequestDto request) {
|
||||
log.info("Received contact form from: {}", request.name);
|
||||
log.debug("Captcha token: {}", request.captcha);
|
||||
log.info("Message: {}", request.message);
|
||||
|
||||
if (request.email != null) {
|
||||
log.info("Reply to: {} ({})", request.email, request.name);
|
||||
}
|
||||
|
||||
if (!isValidCaptcha(request.captcha)) {
|
||||
log.warn("Captcha verification failed for {}", request.email);
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Captcha verification failed");
|
||||
}
|
||||
|
||||
ContactRequest message = new ContactRequest()
|
||||
.setName(request.name)
|
||||
.setEmail(request.email)
|
||||
.setMessage(request.message)
|
||||
.setCompany(request.company)
|
||||
.setPhone(request.phone)
|
||||
.setWebsite(request.website)
|
||||
.setCaptchaToken(request.captcha)
|
||||
.setSubmittedAt(LocalDateTime.now());
|
||||
|
||||
contactRepository.save(message);
|
||||
|
||||
notifyContactAndTeam(request);
|
||||
|
||||
return ResponseEntity.ok("Contact form submitted successfully");
|
||||
}
|
||||
|
||||
private boolean isValidCaptcha(String captcha) {
|
||||
return "10000000-aaaa-bbbb-cccc-000000000001".equals(captcha) || captchaValidator.isValid(captcha);
|
||||
}
|
||||
|
||||
private void notifyContactAndTeam(ContactRequestDto request) {
|
||||
// User confirmation
|
||||
String userSubject = "Kontaktanfrage erhalten";
|
||||
String userBody = """
|
||||
Hallo %s,
|
||||
|
||||
wir haben Ihre Nachricht erhalten und melden uns schnellstmöglich bei Ihnen.
|
||||
|
||||
Ihre Nachricht:
|
||||
%s
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
Rhein Software
|
||||
""".formatted(request.name, request.message);
|
||||
|
||||
mailServiceClient.sendMail(request.email, userSubject, userBody);
|
||||
|
||||
// Team notification
|
||||
String teamSubject = "Neue Kontaktanfrage";
|
||||
String teamBody = """
|
||||
Neue Kontaktanfrage von: %s
|
||||
E-Mail: %s
|
||||
Unternehmen: %s
|
||||
Telefonnummer: %s
|
||||
Webseite: %s
|
||||
|
||||
Nachricht:
|
||||
%s
|
||||
""".formatted(
|
||||
request.name,
|
||||
request.email,
|
||||
safe(request.company),
|
||||
safe(request.phone),
|
||||
safe(request.website),
|
||||
request.message
|
||||
);
|
||||
|
||||
mailServiceClient.sendMail("rhein.software@gmail.com", teamSubject, teamBody);
|
||||
}
|
||||
|
||||
private String safe(String value) {
|
||||
return value != null ? value : "-";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dev.rheinsw.contactService.dto;
|
||||
|
||||
import dev.rheinsw.shared.transport.Dto;
|
||||
|
||||
/**
|
||||
* @author Bummsa / BoomerHD / Thatsaphorn Atchariyaphap
|
||||
* @since 21.04.25
|
||||
*/
|
||||
public class ContactRequestDto implements Dto {
|
||||
public String name;
|
||||
public String email;
|
||||
public String message;
|
||||
|
||||
public String company; // optional
|
||||
public String phone; // optional
|
||||
public String website; // optional
|
||||
public String captcha; // required for hCaptcha validation
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package dev.rheinsw.contactService.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* @author Thatsaphorn Atchariyaphap
|
||||
* @since 22.04.25
|
||||
*/
|
||||
@Entity
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Table(name = "contact_requests")
|
||||
public class ContactRequest {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Size(max = 100)
|
||||
private String name;
|
||||
|
||||
@Size(max = 100)
|
||||
@Email
|
||||
private String email;
|
||||
|
||||
@Size(max = 1000)
|
||||
@Column(length = 1000)
|
||||
private String message;
|
||||
|
||||
@Size(max = 100)
|
||||
private String company;
|
||||
|
||||
@Size(max = 20)
|
||||
private String phone;
|
||||
|
||||
@Size(max = 100)
|
||||
private String website;
|
||||
|
||||
@Size(max = 1024)
|
||||
@Column(name = "captcha_token", length = 1024)
|
||||
private String captchaToken;
|
||||
|
||||
private LocalDateTime submittedAt;
|
||||
|
||||
public ContactRequest setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContactRequest setEmail(String email) {
|
||||
this.email = email;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContactRequest setMessage(String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContactRequest setCompany(String company) {
|
||||
this.company = company;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContactRequest setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContactRequest setWebsite(String website) {
|
||||
this.website = website;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContactRequest setCaptchaToken(String captchaToken) {
|
||||
this.captchaToken = captchaToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContactRequest setSubmittedAt(LocalDateTime submittedAt) {
|
||||
this.submittedAt = submittedAt;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dev.rheinsw.contactService.repository;
|
||||
|
||||
import dev.rheinsw.contactService.model.ContactRequest;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/**
|
||||
* @author Thatsaphorn Atchariyaphap
|
||||
* @since 22.04.25
|
||||
*/
|
||||
public interface ContactRequestsRepo extends JpaRepository<ContactRequest, Long> {
|
||||
// empty
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package dev.rheinsw.contactService.service;
|
||||
|
||||
import dev.rheinsw.contactService.config.HCaptchaConfig;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Thatsaphorn Atchariyaphap
|
||||
* @since 21.04.25
|
||||
*/
|
||||
@Service
|
||||
public class HCaptchaValidator {
|
||||
private static final Logger log = LoggerFactory.getLogger(HCaptchaValidator.class);
|
||||
|
||||
private final HCaptchaConfig config;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public HCaptchaValidator(HCaptchaConfig config, RestTemplate restTemplate) {
|
||||
this.config = config;
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
public boolean isValid(String token) {
|
||||
if (token == null || token.isBlank()) {
|
||||
log.warn("Captcha token is missing or blank");
|
||||
return false;
|
||||
}
|
||||
|
||||
String secret = config.getSecret();
|
||||
if (secret == null || secret.isBlank()) {
|
||||
log.error("Captcha secret is missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
var response = restTemplate.postForObject(
|
||||
"https://api.hcaptcha.com/siteverify",
|
||||
new org.springframework.util.LinkedMultiValueMap<String, String>() {{
|
||||
add("secret", secret);
|
||||
add("response", token);
|
||||
}},
|
||||
Map.class
|
||||
);
|
||||
|
||||
return response != null && Boolean.TRUE.equals(response.get("success"));
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to verify hCaptcha", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
server:
|
||||
port: 0 # random port
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: contactService
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5432/rheinsw_dev
|
||||
username: rheinsw
|
||||
password: rheinsw
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
|
||||
eureka:
|
||||
client:
|
||||
service-url:
|
||||
defaultZone: http://localhost:8761/eureka/
|
||||
|
||||
hcaptcha:
|
||||
secret: ES_ff59a664dc764f92870bf2c7b4eab7c5
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.hibernate.SQL: DEBUG
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE contact_requests
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
email VARCHAR(100),
|
||||
message VARCHAR(1000),
|
||||
company VARCHAR(100),
|
||||
phone VARCHAR(20),
|
||||
website VARCHAR(100),
|
||||
captcha_token VARCHAR(1024),
|
||||
submitted_at TIMESTAMP
|
||||
);
|
||||
@@ -0,0 +1,125 @@
|
||||
package dev.rheinsw.contactService.controller;
|
||||
|
||||
import dev.rheinsw.contactService.dto.ContactRequestDto;
|
||||
import dev.rheinsw.contactService.model.ContactRequest;
|
||||
import dev.rheinsw.contactService.repository.ContactRequestsRepo;
|
||||
import dev.rheinsw.contactService.service.HCaptchaValidator;
|
||||
import dev.rheinsw.shared.mail.MailServiceClient;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.*;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ContactControllerTest {
|
||||
|
||||
@Mock
|
||||
private HCaptchaValidator hCaptchaValidator;
|
||||
|
||||
@Mock
|
||||
private ContactRequestsRepo repository;
|
||||
|
||||
@Mock
|
||||
private MailServiceClient mailServiceClient;
|
||||
|
||||
@InjectMocks
|
||||
private ContactController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitContact_ShouldReturnSuccess_WhenDevCaptchaTokenUsed() {
|
||||
// Arrange
|
||||
ContactRequestDto request = new ContactRequestDto();
|
||||
request.name = "Test User";
|
||||
request.email = "test@example.com";
|
||||
request.message = "Hello!";
|
||||
request.captcha = "10000000-aaaa-bbbb-cccc-000000000001";
|
||||
|
||||
// Act
|
||||
ResponseEntity<String> response = controller.submitContact(request);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals("Contact form submitted successfully", response.getBody());
|
||||
|
||||
verify(repository, times(1)).save(any(ContactRequest.class));
|
||||
verify(mailServiceClient, times(2)).sendMail(anyString(), anyString(), anyString());
|
||||
verifyNoInteractions(hCaptchaValidator);
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitContact_ShouldReturnSuccess_WhenCaptchaValid() {
|
||||
// Arrange
|
||||
ContactRequestDto request = new ContactRequestDto();
|
||||
request.name = "Valid User";
|
||||
request.email = "valid@example.com";
|
||||
request.message = "Some message";
|
||||
request.captcha = "real-token";
|
||||
|
||||
when(hCaptchaValidator.isValid("real-token")).thenReturn(true);
|
||||
|
||||
// Act
|
||||
ResponseEntity<String> response = controller.submitContact(request);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals("Contact form submitted successfully", response.getBody());
|
||||
|
||||
verify(hCaptchaValidator, times(1)).isValid("real-token");
|
||||
verify(repository, times(1)).save(any(ContactRequest.class));
|
||||
verify(mailServiceClient, times(2)).sendMail(anyString(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitContact_ShouldReturnForbidden_WhenCaptchaInvalid() {
|
||||
// Arrange
|
||||
ContactRequestDto request = new ContactRequestDto();
|
||||
request.name = "Bot User";
|
||||
request.email = "bot@example.com";
|
||||
request.message = "Spam spam spam";
|
||||
request.captcha = "invalid-token";
|
||||
|
||||
when(hCaptchaValidator.isValid("invalid-token")).thenReturn(false);
|
||||
|
||||
// Act
|
||||
ResponseEntity<String> response = controller.submitContact(request);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode());
|
||||
assertEquals("Captcha verification failed", response.getBody());
|
||||
|
||||
verify(hCaptchaValidator).isValid("invalid-token");
|
||||
verifyNoInteractions(repository);
|
||||
verifyNoInteractions(mailServiceClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitContact_ShouldHandleNullOptionalFields() {
|
||||
// Arrange
|
||||
ContactRequestDto request = new ContactRequestDto();
|
||||
request.name = "No Company";
|
||||
request.email = "user@example.com";
|
||||
request.message = "Just a message";
|
||||
request.captcha = "real-token";
|
||||
request.company = null;
|
||||
request.phone = null;
|
||||
request.website = null;
|
||||
|
||||
when(hCaptchaValidator.isValid("real-token")).thenReturn(true);
|
||||
|
||||
// Act
|
||||
ResponseEntity<String> response = controller.submitContact(request);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
verify(repository).save(any(ContactRequest.class));
|
||||
verify(mailServiceClient, times(2)).sendMail(anyString(), anyString(), anyString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package dev.rheinsw.contactService.service;
|
||||
|
||||
import dev.rheinsw.contactService.config.HCaptchaConfig;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HCaptchaValidatorTest {
|
||||
|
||||
@Mock
|
||||
private HCaptchaConfig hCaptchaConfig;
|
||||
|
||||
@Mock
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
private HCaptchaValidator validator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
validator = new HCaptchaValidator(hCaptchaConfig, restTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValid_ShouldReturnFalse_WhenTokenIsNull() {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
boolean result = validator.isValid(null);
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValid_ShouldReturnFalse_WhenTokenIsBlank() {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
boolean result = validator.isValid(" ");
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValid_ShouldReturnFalse_WhenSecretIsNull() {
|
||||
// Arrange
|
||||
Mockito.when(hCaptchaConfig.getSecret()).thenReturn(null);
|
||||
|
||||
// Act
|
||||
boolean result = validator.isValid("test-token");
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValid_ShouldReturnFalse_WhenSecretIsBlank() {
|
||||
// Arrange
|
||||
Mockito.when(hCaptchaConfig.getSecret()).thenReturn(" ");
|
||||
|
||||
// Act
|
||||
boolean result = validator.isValid("test-token");
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValid_ShouldReturnFalse_WhenApiResponseIsNull() {
|
||||
// Arrange
|
||||
Mockito.when(hCaptchaConfig.getSecret()).thenReturn("test-secret");
|
||||
Mockito.when(restTemplate.postForObject(
|
||||
eq("https://api.hcaptcha.com/siteverify"),
|
||||
any(),
|
||||
eq(Map.class)
|
||||
)).thenReturn(null);
|
||||
|
||||
// Act
|
||||
boolean result = validator.isValid("test-token");
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValid_ShouldReturnFalse_WhenApiResponseSuccessIsFalse() {
|
||||
// Arrange
|
||||
Mockito.when(hCaptchaConfig.getSecret()).thenReturn("test-secret");
|
||||
Mockito.when(restTemplate.postForObject(
|
||||
eq("https://api.hcaptcha.com/siteverify"),
|
||||
any(),
|
||||
eq(Map.class)
|
||||
)).thenReturn(Map.of("success", false));
|
||||
|
||||
// Act
|
||||
boolean result = validator.isValid("test-token");
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValid_ShouldReturnTrue_WhenApiResponseSuccessIsTrue() {
|
||||
// Arrange
|
||||
Mockito.when(hCaptchaConfig.getSecret()).thenReturn("test-secret");
|
||||
Mockito.when(restTemplate.postForObject(
|
||||
eq("https://api.hcaptcha.com/siteverify"),
|
||||
any(),
|
||||
eq(Map.class)
|
||||
)).thenReturn(Map.of("success", true));
|
||||
|
||||
// Act
|
||||
boolean result = validator.isValid("test-token");
|
||||
|
||||
// Assert
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValid_ShouldReturnFalse_WhenExceptionIsThrown() {
|
||||
// Arrange
|
||||
Mockito.when(hCaptchaConfig.getSecret()).thenReturn("test-secret");
|
||||
Mockito.when(restTemplate.postForObject(
|
||||
eq("https://api.hcaptcha.com/siteverify"),
|
||||
any(),
|
||||
eq(Map.class)
|
||||
)).thenThrow(new RuntimeException("Simulated Exception"));
|
||||
|
||||
// Act
|
||||
boolean result = validator.isValid("test-token");
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user