Add: complete MPaste service rewritten in Spring
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package net.miarma.backend.mpaste;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = {
|
||||
"net.miarma.backend.mpaste",
|
||||
"net.miarma.backlib"
|
||||
})
|
||||
public class MPasteApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MPasteApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.miarma.backend.mpaste.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class NoSecurityConfig {
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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,69 @@
|
||||
package net.miarma.backend.mpaste.controller;
|
||||
|
||||
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.service.PasteService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/pastes")
|
||||
public class PasteController {
|
||||
private PasteService pasteService;
|
||||
|
||||
public PasteController(PasteService pasteService) {
|
||||
this.pasteService = pasteService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<PasteDto.Response>> getAll() {
|
||||
return ResponseEntity.ok(
|
||||
pasteService.getAll().stream()
|
||||
.map(PasteMapper::toResponse)
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/by-id/{paste_id}")
|
||||
public ResponseEntity<PasteDto.Response> getById(@PathVariable("paste_id") UUID pasteId) {
|
||||
return ResponseEntity.ok(
|
||||
PasteMapper.toResponse(pasteService.getById(pasteId))
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/{paste_key}")
|
||||
public ResponseEntity<PasteDto.Response> getByKey(@PathVariable("paste_key") String pasteKey, @RequestBody PastePassword dto) {
|
||||
return ResponseEntity.ok(
|
||||
PasteMapper.toResponse(pasteService.getByKey(pasteKey, dto.password()))
|
||||
);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<PasteDto.Response> create(PasteDto.Request request) {
|
||||
return ResponseEntity.ok(
|
||||
PasteMapper.toResponse(
|
||||
pasteService.create(
|
||||
PasteMapper.toEntity(request)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@PutMapping("/{paste_id}")
|
||||
public void update() {
|
||||
throw new UnsupportedOperationException("Pastes cannot be updated");
|
||||
}
|
||||
|
||||
@DeleteMapping("/{paste_id}")
|
||||
public ResponseEntity<PasteDto.Response> delete(@PathVariable("paste_id") UUID pasteId) {
|
||||
return ResponseEntity.ok(
|
||||
PasteMapper.toResponse(
|
||||
pasteService.delete(pasteId)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
157
mpaste/src/main/java/net/miarma/backend/mpaste/dto/PasteDto.java
Normal file
157
mpaste/src/main/java/net/miarma/backend/mpaste/dto/PasteDto.java
Normal file
@@ -0,0 +1,157 @@
|
||||
package net.miarma.backend.mpaste.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PasteDto {
|
||||
|
||||
private PasteDto() { }
|
||||
|
||||
public static class Request {
|
||||
|
||||
private String title;
|
||||
private String content;
|
||||
private String syntax;
|
||||
|
||||
private Boolean burnAfter;
|
||||
private Boolean isPrivate;
|
||||
private String password;
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getSyntax() {
|
||||
return syntax;
|
||||
}
|
||||
|
||||
public void setSyntax(String syntax) {
|
||||
this.syntax = syntax;
|
||||
}
|
||||
|
||||
public Boolean getBurnAfter() {
|
||||
return burnAfter;
|
||||
}
|
||||
|
||||
public void setBurnAfter(Boolean burnAfter) {
|
||||
this.burnAfter = burnAfter;
|
||||
}
|
||||
|
||||
public Boolean getIsPrivate() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setIsPrivate(Boolean isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response {
|
||||
|
||||
private UUID pasteId;
|
||||
private String pasteKey;
|
||||
|
||||
private String title;
|
||||
private String content;
|
||||
private String syntax;
|
||||
|
||||
private Integer views;
|
||||
|
||||
private Boolean burnAfter;
|
||||
private Boolean isPrivate;
|
||||
|
||||
private Instant createdAt;
|
||||
|
||||
public UUID getPasteId() {
|
||||
return pasteId;
|
||||
}
|
||||
|
||||
public void setPasteId(UUID pasteId) {
|
||||
this.pasteId = pasteId;
|
||||
}
|
||||
|
||||
public String getPasteKey() {
|
||||
return pasteKey;
|
||||
}
|
||||
|
||||
public void setPasteKey(String pasteKey) {
|
||||
this.pasteKey = pasteKey;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getSyntax() {
|
||||
return syntax;
|
||||
}
|
||||
|
||||
public void setSyntax(String syntax) {
|
||||
this.syntax = syntax;
|
||||
}
|
||||
|
||||
public Integer getViews() {
|
||||
return views;
|
||||
}
|
||||
|
||||
public void setViews(Integer views) {
|
||||
this.views = views;
|
||||
}
|
||||
|
||||
public Boolean getBurnAfter() {
|
||||
return burnAfter;
|
||||
}
|
||||
|
||||
public void setBurnAfter(Boolean burnAfter) {
|
||||
this.burnAfter = burnAfter;
|
||||
}
|
||||
|
||||
public Boolean getIsPrivate() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setIsPrivate(Boolean isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package net.miarma.backend.mpaste.dto;
|
||||
|
||||
public record PastePassword(String password) {}
|
||||
@@ -0,0 +1,51 @@
|
||||
package net.miarma.backend.mpaste.mapper;
|
||||
|
||||
import net.miarma.backend.mpaste.dto.PasteDto;
|
||||
import net.miarma.backend.mpaste.model.Paste;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public final class PasteMapper {
|
||||
|
||||
private PasteMapper() { }
|
||||
|
||||
public static Paste toEntity(PasteDto.Request request) {
|
||||
|
||||
Paste paste = new Paste();
|
||||
|
||||
paste.setTitle(request.getTitle());
|
||||
paste.setContent(request.getContent());
|
||||
paste.setSyntax(request.getSyntax());
|
||||
|
||||
paste.setBurnAfter(Boolean.TRUE.equals(request.getBurnAfter()));
|
||||
paste.setPrivate(Boolean.TRUE.equals(request.getIsPrivate()));
|
||||
|
||||
paste.setPassword(request.getPassword());
|
||||
|
||||
paste.setViews(0);
|
||||
paste.setCreatedAt(Instant.now());
|
||||
|
||||
return paste;
|
||||
}
|
||||
|
||||
public static PasteDto.Response toResponse(Paste paste) {
|
||||
|
||||
PasteDto.Response response = new PasteDto.Response();
|
||||
|
||||
response.setPasteId(paste.getPasteId());
|
||||
response.setPasteKey(paste.getPasteKey());
|
||||
|
||||
response.setTitle(paste.getTitle());
|
||||
response.setContent(paste.getContent());
|
||||
response.setSyntax(paste.getSyntax());
|
||||
|
||||
response.setViews(paste.getViews());
|
||||
|
||||
response.setBurnAfter(paste.isBurnAfter());
|
||||
response.setIsPrivate(paste.isPrivate());
|
||||
|
||||
response.setCreatedAt(paste.getCreatedAt());
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
158
mpaste/src/main/java/net/miarma/backend/mpaste/model/Paste.java
Normal file
158
mpaste/src/main/java/net/miarma/backend/mpaste/model/Paste.java
Normal file
@@ -0,0 +1,158 @@
|
||||
package net.miarma.backend.mpaste.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import net.miarma.backlib.util.UuidUtil;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "mpaste_pastes")
|
||||
public class Paste {
|
||||
@Id
|
||||
@Column(name = "paste_id", columnDefinition = "BINARY(16)")
|
||||
private byte[] pasteIdBin;
|
||||
|
||||
@Transient
|
||||
private UUID pasteId;
|
||||
|
||||
@Column(name = "owner_id", columnDefinition = "BINARY(16)")
|
||||
private byte[] ownerIdBin;
|
||||
|
||||
@Transient
|
||||
private UUID ownerId;
|
||||
|
||||
@Column(name = "paste_key", updatable = false, insertable = false, unique = true, nullable = false)
|
||||
private String pasteKey;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String content;
|
||||
|
||||
private String syntax;
|
||||
private Integer views;
|
||||
|
||||
@Column(name = "burn_after")
|
||||
private Boolean burnAfter;
|
||||
|
||||
@Column(name = "is_private")
|
||||
private Boolean isPrivate;
|
||||
|
||||
private String password;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private Instant createdAt;
|
||||
|
||||
@PrePersist
|
||||
@PreUpdate
|
||||
private void prePersist() {
|
||||
if (pasteId != null) {
|
||||
pasteIdBin = UuidUtil.uuidToBin(pasteId);
|
||||
}
|
||||
|
||||
if (ownerId != null) {
|
||||
ownerIdBin = UuidUtil.uuidToBin(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
@PostLoad
|
||||
private void postLoad() {
|
||||
if (pasteIdBin != null) {
|
||||
pasteId = UuidUtil.binToUUID(pasteIdBin);
|
||||
}
|
||||
|
||||
if (ownerIdBin != null) {
|
||||
ownerId = UuidUtil.binToUUID(ownerIdBin);
|
||||
}
|
||||
}
|
||||
|
||||
public UUID getPasteId() {
|
||||
return pasteId;
|
||||
}
|
||||
|
||||
public void setPasteId(UUID pasteId) {
|
||||
this.pasteId = pasteId;
|
||||
}
|
||||
|
||||
public UUID getOwnerId() {
|
||||
return ownerId;
|
||||
}
|
||||
|
||||
public void setOwnerId(UUID ownerId) {
|
||||
this.ownerId = ownerId;
|
||||
}
|
||||
|
||||
public String getPasteKey() {
|
||||
return pasteKey;
|
||||
}
|
||||
|
||||
public void setPasteKey(String pasteKey) {
|
||||
this.pasteKey = pasteKey;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getSyntax() {
|
||||
return syntax;
|
||||
}
|
||||
|
||||
public void setSyntax(String syntax) {
|
||||
this.syntax = syntax;
|
||||
}
|
||||
|
||||
public Integer getViews() {
|
||||
return views;
|
||||
}
|
||||
|
||||
public void setViews(Integer views) {
|
||||
this.views = views;
|
||||
}
|
||||
|
||||
public Boolean isBurnAfter() {
|
||||
return burnAfter;
|
||||
}
|
||||
|
||||
public void setBurnAfter(Boolean burnAfter) {
|
||||
this.burnAfter = burnAfter;
|
||||
}
|
||||
|
||||
public Boolean isPrivate() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivate(Boolean aPrivate) {
|
||||
isPrivate = aPrivate;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package net.miarma.backend.mpaste.repository;
|
||||
|
||||
import net.miarma.backend.mpaste.model.Paste;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PasteRepository extends JpaRepository<Paste, byte[]> {
|
||||
Optional<Paste> findByPasteKey(String pasteKey);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package net.miarma.backend.mpaste.service;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import net.miarma.backend.mpaste.model.Paste;
|
||||
import net.miarma.backend.mpaste.repository.PasteRepository;
|
||||
import net.miarma.backend.mpaste.validation.PasteValidator;
|
||||
import net.miarma.backlib.exception.ForbiddenException;
|
||||
import net.miarma.backlib.exception.NotFoundException;
|
||||
import net.miarma.backlib.util.UuidUtil;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class PasteService {
|
||||
private final PasteRepository pasteRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final TaskScheduler taskScheduler;
|
||||
|
||||
public PasteService(PasteRepository pasteRepository, PasswordEncoder passwordEncoder, TaskScheduler taskScheduler) {
|
||||
this.pasteRepository = pasteRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
public List<Paste> getAll() {
|
||||
return pasteRepository.findAll().stream()
|
||||
.filter(p -> !Boolean.TRUE.equals(p.isPrivate()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public Paste getById(UUID pasteId) {
|
||||
byte[] idBytes = UuidUtil.uuidToBin(pasteId);
|
||||
return pasteRepository.findById(idBytes)
|
||||
.orElseThrow(() -> new NotFoundException("Paste not found"));
|
||||
}
|
||||
|
||||
public Paste getByKey(String pasteKey, String password) {
|
||||
Paste paste = pasteRepository.findByPasteKey(pasteKey)
|
||||
.orElseThrow(() -> new NotFoundException("Paste not found"));
|
||||
|
||||
if(Boolean.TRUE.equals(paste.isPrivate())) {
|
||||
if(password == null || passwordEncoder.matches(password, paste.getPassword())) {
|
||||
throw new ForbiddenException("Incorrect password");
|
||||
}
|
||||
}
|
||||
|
||||
if(Boolean.TRUE.equals(paste.isBurnAfter())) {
|
||||
taskScheduler.schedule(
|
||||
() -> pasteRepository.delete(paste),
|
||||
Instant.now().plusSeconds(5)
|
||||
);
|
||||
}
|
||||
|
||||
return paste;
|
||||
}
|
||||
|
||||
public Paste create(Paste paste) {
|
||||
PasteValidator.validate(paste);
|
||||
return pasteRepository.save(paste);
|
||||
}
|
||||
|
||||
public Paste update(UUID pasteId, Paste changes) {
|
||||
throw new UnsupportedOperationException("Pastes cannot be updated");
|
||||
}
|
||||
|
||||
public Paste delete(UUID pasteId) {
|
||||
byte[] idBytes = UuidUtil.uuidToBin(pasteId);
|
||||
if(!pasteRepository.existsById(idBytes)) {
|
||||
throw new NotFoundException("Paste not found");
|
||||
}
|
||||
Paste paste = getById(pasteId);
|
||||
pasteRepository.deleteById(idBytes);
|
||||
return paste;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.miarma.backend.mpaste.util;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class PasteKeyGenerator {
|
||||
private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
public static String generate(int length) {
|
||||
StringBuilder sb = new StringBuilder(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
int index = RANDOM.nextInt(ALPHABET.length());
|
||||
sb.append(ALPHABET.charAt(index));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.miarma.backend.mpaste.validation;
|
||||
|
||||
import net.miarma.backend.mpaste.model.Paste;
|
||||
import net.miarma.backlib.exception.ValidationException;
|
||||
|
||||
public class PasteValidator {
|
||||
public static void validate(Paste paste) {
|
||||
if (paste.getTitle() == null || paste.getTitle().trim().isEmpty()) {
|
||||
throw new ValidationException("title", "The title cannot be empty");
|
||||
}
|
||||
|
||||
if (paste.getContent() == null || paste.getContent().trim().isEmpty()) {
|
||||
throw new ValidationException("content", "The content cannot be empty");
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(paste.isPrivate())) {
|
||||
if (paste.getPassword() == null || paste.getPassword().trim().isEmpty()) {
|
||||
throw new ValidationException("password", "Private pastes require password");
|
||||
}
|
||||
}
|
||||
|
||||
if (paste.getTitle() != null && paste.getTitle().length() > 128) {
|
||||
throw new ValidationException("title", "Title too long (128 characters max.)");
|
||||
}
|
||||
}
|
||||
}
|
||||
16
mpaste/src/main/resources/application-dev.yml
Normal file
16
mpaste/src/main/resources/application-dev.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
server:
|
||||
port: 8081
|
||||
servlet:
|
||||
context-path: /v2/mpaste
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mariadb://localhost:3306/miarma_v2
|
||||
username: admin
|
||||
password: ${DB_PASS}
|
||||
driver-class-name: org.mariadb.jdbc.Driver
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.hibernate.SQL: DEBUG
|
||||
org.hibernate.orm.jdbc.bind: TRACE
|
||||
15
mpaste/src/main/resources/application-prod.yml
Normal file
15
mpaste/src/main/resources/application-prod.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
server:
|
||||
port: 8081
|
||||
servlet:
|
||||
context-path: /v2/mpaste
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mariadb://mariadb:3306/miarma_v2
|
||||
username: ${DB_USER}
|
||||
password: ${DB_PASS}
|
||||
driver-class-name: org.mariadb.jdbc.Driver
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.hibernate.SQL: WARN
|
||||
25
mpaste/src/main/resources/application.yml
Normal file
25
mpaste/src/main/resources/application.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
spring:
|
||||
application:
|
||||
name: mpaste-service
|
||||
|
||||
jpa:
|
||||
open-in-view: false
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
jdbc:
|
||||
time_zone: UTC
|
||||
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
time-zone: Europe/Madrid
|
||||
|
||||
jwt:
|
||||
expiration-ms: 3600000
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
Reference in New Issue
Block a user