Compare commits
4 Commits
3573f862eb
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
994b682389 | ||
|
|
e9682f095b | ||
|
|
cad49a86ac | ||
|
|
66fb19fa0b |
@@ -83,7 +83,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.miarma</groupId>
|
<groupId>net.miarma</groupId>
|
||||||
<artifactId>backlib</artifactId>
|
<artifactId>backlib</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.1.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,14 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mariadb.jdbc</groupId>
|
<groupId>org.mariadb.jdbc</groupId>
|
||||||
<artifactId>mariadb-java-client</artifactId>
|
<artifactId>mariadb-java-client</artifactId>
|
||||||
@@ -60,7 +68,31 @@
|
|||||||
<version>1.1.1</version>
|
<version>1.1.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<version>${spring.boot.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>repackage</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@@ -3,6 +3,7 @@ package net.miarma.backend.mpaste.config;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -10,8 +11,10 @@ public class NoSecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
.csrf(csrf -> csrf.disable())
|
.cors(AbstractHttpConfigurer::disable)
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
|
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package net.miarma.backend.mpaste.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class RedisConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||||
|
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||||
|
template.setConnectionFactory(connectionFactory);
|
||||||
|
|
||||||
|
RedisSerializer<Object> jsonSerializer = RedisSerializer.json();
|
||||||
|
|
||||||
|
template.setKeySerializer(RedisSerializer.string());
|
||||||
|
template.setValueSerializer(jsonSerializer);
|
||||||
|
template.setHashKeySerializer(RedisSerializer.string());
|
||||||
|
template.setHashValueSerializer(jsonSerializer);
|
||||||
|
|
||||||
|
template.afterPropertiesSet();
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package net.miarma.backend.mpaste.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
|
||||||
|
|
||||||
@EnableScheduling
|
|
||||||
@Configuration
|
|
||||||
public class SchedulingConfig {
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package net.miarma.backend.mpaste.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||||
|
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||||
|
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSocketMessageBroker
|
||||||
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
@Override
|
||||||
|
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||||
|
config.enableSimpleBroker("/topic");
|
||||||
|
config.setApplicationDestinationPrefixes("/app");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
|
registry.addEndpoint("/ws")
|
||||||
|
.setAllowedOrigins("https://paste.miarma.net", "http://localhost:3000")
|
||||||
|
.withSockJS();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package net.miarma.backend.mpaste.controller;
|
package net.miarma.backend.mpaste.controller;
|
||||||
|
|
||||||
import net.miarma.backend.mpaste.dto.PasteDto;
|
import net.miarma.backend.mpaste.dto.PasteDto;
|
||||||
import net.miarma.backend.mpaste.dto.PastePassword;
|
|
||||||
import net.miarma.backend.mpaste.mapper.PasteMapper;
|
import net.miarma.backend.mpaste.mapper.PasteMapper;
|
||||||
import net.miarma.backend.mpaste.service.PasteService;
|
import net.miarma.backend.mpaste.service.PasteService;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -35,10 +34,10 @@ public class PasteController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{paste_key}")
|
@GetMapping("/s/{paste_key}")
|
||||||
public ResponseEntity<PasteDto.Response> getByKey(
|
public ResponseEntity<PasteDto.Response> getByKey(
|
||||||
@PathVariable("paste_key") String pasteKey,
|
@PathVariable("paste_key") String pasteKey,
|
||||||
@RequestParam(value = "password", required = false) String password
|
@RequestHeader(value = "X-Paste-Password", required = false) String password
|
||||||
) {
|
) {
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
PasteMapper.toResponse(pasteService.getByKey(pasteKey, password))
|
PasteMapper.toResponse(pasteService.getByKey(pasteKey, password))
|
||||||
@@ -63,10 +62,6 @@ public class PasteController {
|
|||||||
|
|
||||||
@DeleteMapping("/{paste_id}")
|
@DeleteMapping("/{paste_id}")
|
||||||
public ResponseEntity<PasteDto.Response> delete(@PathVariable("paste_id") UUID pasteId) {
|
public ResponseEntity<PasteDto.Response> delete(@PathVariable("paste_id") UUID pasteId) {
|
||||||
return ResponseEntity.ok(
|
throw new UnsupportedOperationException("Pastes cannot be deleted manually");
|
||||||
PasteMapper.toResponse(
|
|
||||||
pasteService.delete(pasteId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package net.miarma.backend.mpaste.controller;
|
||||||
|
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.messaging.handler.annotation.DestinationVariable;
|
||||||
|
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||||
|
import org.springframework.messaging.handler.annotation.SendTo;
|
||||||
|
import org.springframework.messaging.simp.annotation.SendToUser;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class RTPasteController {
|
||||||
|
private final RedisTemplate<String, Object> template;
|
||||||
|
private static final String REDIS_PREFIX = "rt_paste:";
|
||||||
|
|
||||||
|
public RTPasteController(RedisTemplate<String, Object> template) {
|
||||||
|
this.template = template;
|
||||||
|
}
|
||||||
|
|
||||||
|
@MessageMapping("/edit/{key}")
|
||||||
|
@SendTo("/topic/session/{key}")
|
||||||
|
public Map<String, Object> handleUpdate(@DestinationVariable("key") String key, Map<String, Object> payload) {
|
||||||
|
template.opsForValue().set(REDIS_PREFIX + key, payload, 24, TimeUnit.HOURS);
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
@MessageMapping("/join/{key}")
|
||||||
|
@SendTo("/topic/session/{key}")
|
||||||
|
public Map<String, Object> handleJoin(@DestinationVariable("key") String key) {
|
||||||
|
Object data = template.opsForValue().get(REDIS_PREFIX + key);
|
||||||
|
if (data instanceof Map) {
|
||||||
|
return (Map<String, Object>) data;
|
||||||
|
}
|
||||||
|
return Map.of("content", "", "syntax", "plaintext", "title", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,14 +9,24 @@ public class PasteDto {
|
|||||||
|
|
||||||
public static class Request {
|
public static class Request {
|
||||||
|
|
||||||
|
private String pasteKey;
|
||||||
private String title;
|
private String title;
|
||||||
private String content;
|
private String content;
|
||||||
private String syntax;
|
private String syntax;
|
||||||
|
|
||||||
private Boolean burnAfter;
|
private Boolean burnAfter;
|
||||||
private Boolean isPrivate;
|
private Boolean isPrivate;
|
||||||
|
private Boolean isRt;
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
|
public String getPasteKey() {
|
||||||
|
return pasteKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPasteKey(String pasteKey) {
|
||||||
|
this.pasteKey = pasteKey;
|
||||||
|
}
|
||||||
|
|
||||||
public String getTitle() {
|
public String getTitle() {
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
@@ -41,7 +51,7 @@ public class PasteDto {
|
|||||||
this.syntax = syntax;
|
this.syntax = syntax;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getBurnAfter() {
|
public Boolean getIsBurnAfter() {
|
||||||
return burnAfter;
|
return burnAfter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +67,14 @@ public class PasteDto {
|
|||||||
this.isPrivate = isPrivate;
|
this.isPrivate = isPrivate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getIsRt() {
|
||||||
|
return isRt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsRt(Boolean rt) {
|
||||||
|
isRt = rt;
|
||||||
|
}
|
||||||
|
|
||||||
public String getPassword() {
|
public String getPassword() {
|
||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
@@ -79,6 +97,7 @@ public class PasteDto {
|
|||||||
|
|
||||||
private Boolean burnAfter;
|
private Boolean burnAfter;
|
||||||
private Boolean isPrivate;
|
private Boolean isPrivate;
|
||||||
|
private Boolean isRt;
|
||||||
|
|
||||||
private Instant createdAt;
|
private Instant createdAt;
|
||||||
|
|
||||||
@@ -130,11 +149,11 @@ public class PasteDto {
|
|||||||
this.views = views;
|
this.views = views;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getBurnAfter() {
|
public Boolean getIsBurnAfter() {
|
||||||
return burnAfter;
|
return burnAfter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBurnAfter(Boolean burnAfter) {
|
public void setIsBurnAfter(Boolean burnAfter) {
|
||||||
this.burnAfter = burnAfter;
|
this.burnAfter = burnAfter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +165,14 @@ public class PasteDto {
|
|||||||
this.isPrivate = isPrivate;
|
this.isPrivate = isPrivate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getIsRt() {
|
||||||
|
return isRt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsRt(Boolean rt) {
|
||||||
|
isRt = rt;
|
||||||
|
}
|
||||||
|
|
||||||
public Instant getCreatedAt() {
|
public Instant getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
package net.miarma.backend.mpaste.dto;
|
|
||||||
|
|
||||||
public record PastePassword(String password) {}
|
|
||||||
@@ -16,9 +16,11 @@ public final class PasteMapper {
|
|||||||
paste.setTitle(request.getTitle());
|
paste.setTitle(request.getTitle());
|
||||||
paste.setContent(request.getContent());
|
paste.setContent(request.getContent());
|
||||||
paste.setSyntax(request.getSyntax());
|
paste.setSyntax(request.getSyntax());
|
||||||
|
paste.setPasteKey(request.getPasteKey());
|
||||||
|
|
||||||
paste.setBurnAfter(Boolean.TRUE.equals(request.getBurnAfter()));
|
paste.setBurnAfter(Boolean.TRUE.equals(request.getIsBurnAfter()));
|
||||||
paste.setPrivate(Boolean.TRUE.equals(request.getIsPrivate()));
|
paste.setPrivate(Boolean.TRUE.equals(request.getIsPrivate()));
|
||||||
|
paste.setRt(Boolean.TRUE.equals(request.getIsRt()));
|
||||||
|
|
||||||
paste.setPassword(request.getPassword());
|
paste.setPassword(request.getPassword());
|
||||||
|
|
||||||
@@ -41,8 +43,9 @@ public final class PasteMapper {
|
|||||||
|
|
||||||
response.setViews(paste.getViews());
|
response.setViews(paste.getViews());
|
||||||
|
|
||||||
response.setBurnAfter(paste.isBurnAfter());
|
response.setIsBurnAfter(paste.isBurnAfter());
|
||||||
response.setIsPrivate(paste.isPrivate());
|
response.setIsPrivate(paste.isPrivate());
|
||||||
|
response.setIsRt(paste.isRt());
|
||||||
|
|
||||||
response.setCreatedAt(paste.getCreatedAt());
|
response.setCreatedAt(paste.getCreatedAt());
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ public class Paste {
|
|||||||
@Column(name = "is_private")
|
@Column(name = "is_private")
|
||||||
private Boolean isPrivate;
|
private Boolean isPrivate;
|
||||||
|
|
||||||
|
@Column(name = "is_rt")
|
||||||
|
private Boolean isRt;
|
||||||
|
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@Column(name = "created_at")
|
@Column(name = "created_at")
|
||||||
@@ -136,10 +139,14 @@ public class Paste {
|
|||||||
return isPrivate;
|
return isPrivate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPrivate(Boolean aPrivate) {
|
public void setPrivate(Boolean isPrivate) {
|
||||||
isPrivate = aPrivate;
|
this.isPrivate = isPrivate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean isRt() { return isRt; }
|
||||||
|
|
||||||
|
public void setRt(Boolean rt) { isRt = rt; }
|
||||||
|
|
||||||
public String getPassword() {
|
public String getPassword() {
|
||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public class PasteService {
|
|||||||
public List<Paste> getAll() {
|
public List<Paste> getAll() {
|
||||||
return pasteRepository.findAll().stream()
|
return pasteRepository.findAll().stream()
|
||||||
.filter(p -> !Boolean.TRUE.equals(p.isPrivate()))
|
.filter(p -> !Boolean.TRUE.equals(p.isPrivate()))
|
||||||
|
.filter(p -> !Boolean.TRUE.equals(p.isRt()))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ public class PasteService {
|
|||||||
.orElseThrow(() -> new NotFoundException("Paste not found"));
|
.orElseThrow(() -> new NotFoundException("Paste not found"));
|
||||||
|
|
||||||
if(Boolean.TRUE.equals(paste.isPrivate())) {
|
if(Boolean.TRUE.equals(paste.isPrivate())) {
|
||||||
if(password == null || passwordEncoder.matches(password, paste.getPassword())) {
|
if(password == null || !passwordEncoder.matches(password, paste.getPassword())) {
|
||||||
throw new ForbiddenException("Incorrect password");
|
throw new ForbiddenException("Incorrect password");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,9 +64,27 @@ public class PasteService {
|
|||||||
|
|
||||||
public Paste create(Paste paste) {
|
public Paste create(Paste paste) {
|
||||||
PasteValidator.validate(paste);
|
PasteValidator.validate(paste);
|
||||||
|
|
||||||
|
return pasteRepository.findByPasteKey(paste.getPasteKey())
|
||||||
|
.map(existing -> {
|
||||||
|
existing.setContent(paste.getContent());
|
||||||
|
existing.setSyntax(paste.getSyntax());
|
||||||
|
existing.setTitle(paste.getTitle());
|
||||||
|
return pasteRepository.save(existing);
|
||||||
|
})
|
||||||
|
.orElseGet(() -> {
|
||||||
|
if (Boolean.TRUE.equals(paste.isPrivate()) && paste.getPassword() != null) {
|
||||||
|
paste.setPassword(passwordEncoder.encode(paste.getPassword()));
|
||||||
|
}
|
||||||
|
|
||||||
paste.setPasteId(UUID.randomUUID());
|
paste.setPasteId(UUID.randomUUID());
|
||||||
|
|
||||||
|
if (paste.getPasteKey() == null || paste.getPasteKey().isEmpty()) {
|
||||||
paste.setPasteKey(PasteKeyGenerator.generate(6));
|
paste.setPasteKey(PasteKeyGenerator.generate(6));
|
||||||
|
}
|
||||||
|
|
||||||
return pasteRepository.save(paste);
|
return pasteRepository.save(paste);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Paste update(UUID pasteId, Paste changes) {
|
public Paste update(UUID pasteId, Paste changes) {
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ import net.miarma.backlib.exception.ValidationException;
|
|||||||
|
|
||||||
public class PasteValidator {
|
public class PasteValidator {
|
||||||
public static void validate(Paste paste) {
|
public static void validate(Paste paste) {
|
||||||
|
if (Boolean.TRUE.equals(paste.isRt())) {
|
||||||
|
if (paste.getTitle() == null || paste.getTitle().trim().isEmpty()) {
|
||||||
|
String uuidStr = paste.getPasteId().toString();
|
||||||
|
String shortId = uuidStr.substring(0, 8);
|
||||||
|
paste.setTitle("Sesión: " + shortId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (paste.getTitle() == null || paste.getTitle().trim().isEmpty()) {
|
if (paste.getTitle() == null || paste.getTitle().trim().isEmpty()) {
|
||||||
throw new ValidationException("title", "The title cannot be empty");
|
throw new ValidationException("title", "The title cannot be empty");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ spring:
|
|||||||
default-property-inclusion: non_null
|
default-property-inclusion: non_null
|
||||||
time-zone: Europe/Madrid
|
time-zone: Europe/Madrid
|
||||||
|
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: 6379
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
expiration-ms: 3600000
|
expiration-ms: 3600000
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user