4 Commits

Author SHA1 Message Date
Jose
994b682389 fix: cors (again), isRt flag and build in POM 2026-03-17 03:12:09 +01:00
Jose
e9682f095b add: experimental real time websocket-based pastes 2026-03-17 02:27:33 +01:00
Jose
cad49a86ac fix: build plugin not in mpaste POM 2026-03-16 01:36:24 +01:00
Jose
66fb19fa0b fix: password checking condition on private pastes
change: password is now sent via http headers
2026-03-16 01:22:39 +01:00
15 changed files with 214 additions and 34 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
} }
} }

View File

@@ -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;
}
}

View File

@@ -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 {
}

View File

@@ -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();
}
}

View File

@@ -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)
)
);
} }
} }

View File

@@ -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", "");
}
}

View File

@@ -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;
} }

View File

@@ -1,3 +0,0 @@
package net.miarma.backend.mpaste.dto;
public record PastePassword(String password) {}

View File

@@ -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());

View File

@@ -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;
} }

View File

@@ -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) {

View File

@@ -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");
} }

View File

@@ -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