diff --git a/build-upload.sh b/build-upload.sh
index 098d6d2..35d21c2 100755
--- a/build-upload.sh
+++ b/build-upload.sh
@@ -6,5 +6,9 @@ cd ..
cd huertos/
mvn clean package
cd ..
+cd mpaste/
+mvn clean package
+cd ..
scp core/target/core-1.0.0.jar jomaa@10.0.0.254:/home/jomaa/transfer
scp huertos/target/huertos-1.0.0.jar jomaa@10.0.0.254:/home/jomaa/transfer
+scp mpaste/target/mpaste-1.0.0.jar jomaa@10.0.0.254:/home/jomaa/transfer
\ No newline at end of file
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