From f579af11a72134b9f80aaee7fedc439470291d1b Mon Sep 17 00:00:00 2001 From: Jose Date: Tue, 24 Feb 2026 02:39:01 +0100 Subject: [PATCH] Add: complete MPaste service rewritten in Spring --- mpaste/pom.xml | 12 +- .../backend/mpaste/MPasteApplication.java | 15 ++ .../mpaste/config/NoSecurityConfig.java | 17 ++ .../mpaste/config/SchedulingConfig.java | 9 + .../mpaste/controller/PasteController.java | 69 ++++++++ .../miarma/backend/mpaste/dto/PasteDto.java | 157 +++++++++++++++++ .../backend/mpaste/dto/PastePassword.java | 3 + .../backend/mpaste/mapper/PasteMapper.java | 51 ++++++ .../miarma/backend/mpaste/model/Paste.java | 158 ++++++++++++++++++ .../mpaste/repository/PasteRepository.java | 10 ++ .../backend/mpaste/service/PasteService.java | 81 +++++++++ .../mpaste/util/PasteKeyGenerator.java | 17 ++ .../mpaste/validation/PasteValidator.java | 26 +++ mpaste/src/main/resources/application-dev.yml | 16 ++ .../src/main/resources/application-prod.yml | 15 ++ mpaste/src/main/resources/application.yml | 25 +++ 16 files changed, 676 insertions(+), 5 deletions(-) create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/MPasteApplication.java create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/config/NoSecurityConfig.java create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/config/SchedulingConfig.java create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/controller/PasteController.java create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/dto/PasteDto.java create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/dto/PastePassword.java create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/mapper/PasteMapper.java create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/model/Paste.java create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/repository/PasteRepository.java create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/service/PasteService.java create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/util/PasteKeyGenerator.java create mode 100644 mpaste/src/main/java/net/miarma/backend/mpaste/validation/PasteValidator.java create mode 100644 mpaste/src/main/resources/application-dev.yml create mode 100644 mpaste/src/main/resources/application-prod.yml create mode 100644 mpaste/src/main/resources/application.yml diff --git a/mpaste/pom.xml b/mpaste/pom.xml index fbff362..07e5b75 100644 --- a/mpaste/pom.xml +++ b/mpaste/pom.xml @@ -31,10 +31,6 @@ org.springframework.boot spring-boot-starter-data-jpa - - org.springframework.boot - spring-boot-starter-security - org.mariadb.jdbc mariadb-java-client @@ -58,7 +54,13 @@ 0.11.5 runtime - + + net.miarma + backlib + 1.1.1 + compile + + \ No newline at end of file diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/MPasteApplication.java b/mpaste/src/main/java/net/miarma/backend/mpaste/MPasteApplication.java new file mode 100644 index 0000000..549d04e --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/MPasteApplication.java @@ -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); + } +} + diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/config/NoSecurityConfig.java b/mpaste/src/main/java/net/miarma/backend/mpaste/config/NoSecurityConfig.java new file mode 100644 index 0000000..0767efb --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/config/NoSecurityConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/config/SchedulingConfig.java b/mpaste/src/main/java/net/miarma/backend/mpaste/config/SchedulingConfig.java new file mode 100644 index 0000000..dca4f1a --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/config/SchedulingConfig.java @@ -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 { +} \ No newline at end of file diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/controller/PasteController.java b/mpaste/src/main/java/net/miarma/backend/mpaste/controller/PasteController.java new file mode 100644 index 0000000..a4524a8 --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/controller/PasteController.java @@ -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> getAll() { + return ResponseEntity.ok( + pasteService.getAll().stream() + .map(PasteMapper::toResponse) + .toList() + ); + } + + @GetMapping("/by-id/{paste_id}") + public ResponseEntity getById(@PathVariable("paste_id") UUID pasteId) { + return ResponseEntity.ok( + PasteMapper.toResponse(pasteService.getById(pasteId)) + ); + } + + @GetMapping("/{paste_key}") + public ResponseEntity getByKey(@PathVariable("paste_key") String pasteKey, @RequestBody PastePassword dto) { + return ResponseEntity.ok( + PasteMapper.toResponse(pasteService.getByKey(pasteKey, dto.password())) + ); + } + + @PostMapping + public ResponseEntity 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 delete(@PathVariable("paste_id") UUID pasteId) { + return ResponseEntity.ok( + PasteMapper.toResponse( + pasteService.delete(pasteId) + ) + ); + } +} diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/dto/PasteDto.java b/mpaste/src/main/java/net/miarma/backend/mpaste/dto/PasteDto.java new file mode 100644 index 0000000..98f937c --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/dto/PasteDto.java @@ -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; + } + } +} \ No newline at end of file diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/dto/PastePassword.java b/mpaste/src/main/java/net/miarma/backend/mpaste/dto/PastePassword.java new file mode 100644 index 0000000..244eebb --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/dto/PastePassword.java @@ -0,0 +1,3 @@ +package net.miarma.backend.mpaste.dto; + +public record PastePassword(String password) {} diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/mapper/PasteMapper.java b/mpaste/src/main/java/net/miarma/backend/mpaste/mapper/PasteMapper.java new file mode 100644 index 0000000..44b8861 --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/mapper/PasteMapper.java @@ -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; + } +} \ No newline at end of file diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/model/Paste.java b/mpaste/src/main/java/net/miarma/backend/mpaste/model/Paste.java new file mode 100644 index 0000000..1cf2420 --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/model/Paste.java @@ -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; + } +} diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/repository/PasteRepository.java b/mpaste/src/main/java/net/miarma/backend/mpaste/repository/PasteRepository.java new file mode 100644 index 0000000..6657898 --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/repository/PasteRepository.java @@ -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 { + Optional findByPasteKey(String pasteKey); +} diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/service/PasteService.java b/mpaste/src/main/java/net/miarma/backend/mpaste/service/PasteService.java new file mode 100644 index 0000000..bed6213 --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/service/PasteService.java @@ -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 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; + } +} diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/util/PasteKeyGenerator.java b/mpaste/src/main/java/net/miarma/backend/mpaste/util/PasteKeyGenerator.java new file mode 100644 index 0000000..13783aa --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/util/PasteKeyGenerator.java @@ -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(); + } +} \ No newline at end of file diff --git a/mpaste/src/main/java/net/miarma/backend/mpaste/validation/PasteValidator.java b/mpaste/src/main/java/net/miarma/backend/mpaste/validation/PasteValidator.java new file mode 100644 index 0000000..ac28ac2 --- /dev/null +++ b/mpaste/src/main/java/net/miarma/backend/mpaste/validation/PasteValidator.java @@ -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.)"); + } + } +} diff --git a/mpaste/src/main/resources/application-dev.yml b/mpaste/src/main/resources/application-dev.yml new file mode 100644 index 0000000..b97b183 --- /dev/null +++ b/mpaste/src/main/resources/application-dev.yml @@ -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 \ No newline at end of file diff --git a/mpaste/src/main/resources/application-prod.yml b/mpaste/src/main/resources/application-prod.yml new file mode 100644 index 0000000..3d5fa65 --- /dev/null +++ b/mpaste/src/main/resources/application-prod.yml @@ -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 \ No newline at end of file diff --git a/mpaste/src/main/resources/application.yml b/mpaste/src/main/resources/application.yml new file mode 100644 index 0000000..e563b0f --- /dev/null +++ b/mpaste/src/main/resources/application.yml @@ -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