diff --git a/README.md b/README.md new file mode 100644 index 0000000..1470a6c --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# SKAMP Base API + +## Environment + +The default profile is `dev`. +In production, set it to `prod` via environment variables: + +```bash +java ... +``` \ No newline at end of file diff --git a/bruno/propify/Authenticate/Auth.bru b/bruno/propify/Authenticate/Auth.bru index 2b1b156..c223dd4 100644 --- a/bruno/propify/Authenticate/Auth.bru +++ b/bruno/propify/Authenticate/Auth.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{KEYCLOAK_BASE_URL}}/realms/propify/protocol/openid-connect/token + url: {{KEYCLOAK_BASE_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token body: formUrlEncoded auth: inherit } diff --git a/bruno/propify/Properties/Create new.bru b/bruno/propify/Properties/Create new.bru index a79b3d5..4309d4e 100644 --- a/bruno/propify/Properties/Create new.bru +++ b/bruno/propify/Properties/Create new.bru @@ -16,12 +16,12 @@ auth:bearer { body:json { { - "name": "Mustername 1", - "street": "Musterstraße", - "houseNumber": "1", - "zipCode": "55123", - "city": "Musterstadt", - "country": "de", + "name": "Bungalow", + "street": "Hebbelstraße", + "houseNumber": "30", + "zipCode": "55127", + "city": "Mainz", + "country": "DE", "notes": "Lorem ipsum" } } diff --git a/bruno/propify/Properties/Upload files.bru b/bruno/propify/Properties/Upload files.bru new file mode 100644 index 0000000..439c459 --- /dev/null +++ b/bruno/propify/Properties/Upload files.bru @@ -0,0 +1,39 @@ +meta { + name: Upload files + type: http + seq: 5 +} + +post { + url: {{API_BASE_URL}}/api/{{API_VERSION}}/properties/f4eb0c54-7c8f-4e60-b71f-1d08b8b6e2d4/upload + body: multipartForm + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} +} + +body:json { + { + "name": "Bungalow", + "street": "Hebbelstraße", + "houseNumber": "30", + "zipCode": "55127", + "city": "Mainz", + "country": "DE", + "notes": "Lorem ipsum" + } +} + +body:multipart-form { + attachments: @file(/Users/murat/Pictures/IMG_0229.jpeg|/Users/murat/Pictures/schnürsenkel technik.gif) +} + +body:file { + file: @file(/Users/murat/Pictures/IMG_0229.jpeg) @contentType(image/jpeg) +} + +settings { + encodeUrl: true +} diff --git a/bruno/propify/bruno.json b/bruno/propify/bruno.json index 5066d2c..d425f89 100644 --- a/bruno/propify/bruno.json +++ b/bruno/propify/bruno.json @@ -1,6 +1,6 @@ { "version": "1", - "name": "propify", + "name": "skamp-api", "type": "collection", "ignore": [ "node_modules", diff --git a/bruno/propify/environments/local.bru b/bruno/propify/environments/local.bru index de3d934..917650f 100644 --- a/bruno/propify/environments/local.bru +++ b/bruno/propify/environments/local.bru @@ -1,11 +1,14 @@ vars { API_BASE_URL: http://localhost:8080 API_VERSION: v1 - BEARER_TOKEN: - KEYCLOAK_BASE_URL: http://localhost:8280 DEV_USERNAME: dev@example.com DEV_PASSWORD: dev ADMIN_USERNAME: admin@example.com ADMIN_PASSWORD: admin - KEYCLOAK_CLIENT_ID: propify-app + KEYCLOAK_CLIENT_ID: skamp-app + KEYCLOAK_REALM: skamp } +vars:secret [ + BEARER_TOKEN +] diff --git a/pom.xml b/pom.xml index d3cb5cf..2a89485 100644 --- a/pom.xml +++ b/pom.xml @@ -9,10 +9,10 @@ de.iwomm - propify-api + skamp-api 0.0.1-SNAPSHOT - propify-api - Propify API + skamp-api + SKAMP API @@ -84,6 +84,20 @@ spring-boot-starter-security + + org.postgresql + postgresql + 42.6.0 + + + + + software.amazon.awssdk + s3 + 2.33.0 + compile + + diff --git a/src/main/java/de/iwomm/propify_api/PropifyApiApplication.java b/src/main/java/de/iwomm/propify_api/SkampApiApplication.java similarity index 69% rename from src/main/java/de/iwomm/propify_api/PropifyApiApplication.java rename to src/main/java/de/iwomm/propify_api/SkampApiApplication.java index fe197f3..48b9017 100644 --- a/src/main/java/de/iwomm/propify_api/PropifyApiApplication.java +++ b/src/main/java/de/iwomm/propify_api/SkampApiApplication.java @@ -4,10 +4,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class PropifyApiApplication { +public class SkampApiApplication { public static void main(String[] args) { - SpringApplication.run(PropifyApiApplication.class, args); + SpringApplication.run(SkampApiApplication.class, args); } } diff --git a/src/main/java/de/iwomm/propify_api/controller/AttachmentController.java b/src/main/java/de/iwomm/propify_api/controller/AttachmentController.java new file mode 100644 index 0000000..25bb253 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/controller/AttachmentController.java @@ -0,0 +1,36 @@ +package de.iwomm.propify_api.controller; + +import de.iwomm.propify_api.service.AttachmentService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/attachments") +public class AttachmentController { + private final AttachmentService attachmentService; + + public AttachmentController(AttachmentService attachmentService) { + this.attachmentService = attachmentService; + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity delete(@PathVariable UUID id) { + try { + this.attachmentService.deleteById(id); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } + return ResponseEntity + .noContent() + .build(); + } +} diff --git a/src/main/java/de/iwomm/propify_api/controller/PropertyController.java b/src/main/java/de/iwomm/propify_api/controller/PropertyController.java index 809bdfa..269751f 100644 --- a/src/main/java/de/iwomm/propify_api/controller/PropertyController.java +++ b/src/main/java/de/iwomm/propify_api/controller/PropertyController.java @@ -1,58 +1,185 @@ package de.iwomm.propify_api.controller; +import de.iwomm.propify_api.dto.BulkDeletePropertyIdsDTO; +import de.iwomm.propify_api.dto.PropertyDTO; +import de.iwomm.propify_api.entity.Attachment; import de.iwomm.propify_api.entity.Property; +import de.iwomm.propify_api.s3.S3Service; +import de.iwomm.propify_api.s3.UploadResponse; +import de.iwomm.propify_api.service.AttachmentService; import de.iwomm.propify_api.service.PropertyService; +import jakarta.persistence.EntityNotFoundException; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.io.IOException; +import java.net.URI; +import java.util.*; @RestController @RequestMapping("/api/v1/properties") public class PropertyController { private final PropertyService propertyService; + private final AttachmentService attachmentService; + private final S3Service s3service; - public PropertyController(PropertyService propertyService) { + public PropertyController(PropertyService propertyService, AttachmentService attachmentService, S3Service s3Service) { this.propertyService = propertyService; - } - - @GetMapping("/info") - @PreAuthorize("isAuthenticated()") - public String infoEndpoint() { - return "Hello, you are authenticated!"; + this.attachmentService = attachmentService; + this.s3service = s3Service; } @GetMapping @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')") - public List getAllProperties() { - return propertyService.findAll(); + public ResponseEntity getAllProperties() { + List propertiesDTO = propertyService.toDTOs(propertyService.findAll()); + + return ResponseEntity + .ok(propertiesDTO); } @GetMapping("/{id}") @PreAuthorize("hasAnyRole('ADMIN', 'USER')") - public Property getPropertyById(@PathVariable UUID id) { - return propertyService.findById(id).orElse(null); + public ResponseEntity getPropertyById(@PathVariable UUID id) { + try { + return ResponseEntity + .ok(propertyService.findById(id).orElseThrow(EntityNotFoundException::new)); + } catch (EntityNotFoundException e) { + return ResponseEntity + .notFound() + .build(); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } + } + + @PostMapping("/{id}/upload") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity upload(@RequestParam("attachments") List files, @PathVariable UUID id) { + List messages = new ArrayList<>(); + + if (files.isEmpty()) { + return ResponseEntity + .ok() + .body(new UploadResponse(HttpStatus.BAD_REQUEST, "No attachments provided.")); + } + + Property property; + + try { + property = propertyService.findById(id).orElseThrow(EntityNotFoundException::new); + } catch (EntityNotFoundException e) { + return ResponseEntity + .unprocessableEntity() + .build(); + } + + files.forEach(file -> { + String originalFileName = file.getOriginalFilename(); + String uniqueFileName = id + "/" + UUID.randomUUID() + "_" + originalFileName; + try { + s3service.putObject("skamp", uniqueFileName, file); + + Attachment newAttachment = new Attachment(); + newAttachment.setProperty(property); + newAttachment.setStoragePath(uniqueFileName); + newAttachment.setFileName(originalFileName); + this.attachmentService.save(newAttachment); + + property.getAttachments().add(newAttachment); + + messages.add("Successfully uploaded " + originalFileName + " to " + uniqueFileName); + } catch (IOException e) { + messages.add("Failed uploading " + originalFileName); + } + }); + + propertyService.save(property); + + return ResponseEntity.ok(new UploadResponse(HttpStatus.OK, messages)); } @PostMapping - @ResponseStatus(HttpStatus.CREATED) @PreAuthorize("hasRole('ADMIN')") - public Property createProperty(@RequestBody Property property) { - return propertyService.save(property); + public ResponseEntity createProperty(@RequestBody PropertyDTO propertyDTO) { + try { + Property newItem = propertyService.saveDTO(propertyDTO); + + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{id}") + .buildAndExpand(newItem.getId()) + .toUri(); + + return ResponseEntity + .created(location) + .body(newItem); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } + } + + @PatchMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity updateProperty(@PathVariable UUID id, @RequestBody PropertyDTO propertyDTO) { + try { + return ResponseEntity + .ok(propertyService.update(id, propertyDTO)); + } catch (EntityNotFoundException e) { + return ResponseEntity + .notFound() + .build(); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } } @DeleteMapping("/{id}") - @ResponseStatus(HttpStatus.NO_CONTENT) @PreAuthorize("hasRole('ADMIN')") - public void deleteProperty(@PathVariable UUID id) { - Optional property = propertyService.findById(id); - if (property.isEmpty()) { - return; - } + public ResponseEntity deleteProperty(@PathVariable UUID id) { + try { + propertyService.deleteById(id); - propertyService.delete(property.get()); + return ResponseEntity + .noContent() + .build(); + } catch (EntityNotFoundException e) { + return ResponseEntity + .notFound() + .build(); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } + } + + @PostMapping("/bulk-delete") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity deleteProperties(@RequestBody BulkDeletePropertyIdsDTO propertiesDTO) { + try { + propertyService.deleteByIds(propertiesDTO); + + return ResponseEntity + .noContent() + .build(); + } catch (EntityNotFoundException e) { + return ResponseEntity + .notFound() + .build(); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } } } diff --git a/src/main/java/de/iwomm/propify_api/database/DatabaseSeeder.java b/src/main/java/de/iwomm/propify_api/database/DatabaseSeeder.java new file mode 100644 index 0000000..5c46d2b --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/database/DatabaseSeeder.java @@ -0,0 +1,29 @@ +package de.iwomm.propify_api.database; + +import de.iwomm.propify_api.entity.Property; +import de.iwomm.propify_api.repository.PropertyRepository; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile("dev") // Runs only in "dev" environments +public class DatabaseSeeder implements CommandLineRunner { + private final PropertyRepository propertyRepository; + + public DatabaseSeeder(PropertyRepository propertyRepository) { + this.propertyRepository = propertyRepository; + } + + @Override + public void run(String... args) throws Exception { + if (propertyRepository.count() == 0) { + propertyRepository.save(new Property("Mustergebäude 1", "Musterstraße", "1", "12345", "Musterstadt", "DE", "Musterbemerkung 1")); + propertyRepository.save(new Property("Mustergebäude 2", "Dagobertstraße", "2", "22345", "Entenhausen", "AT", "Musterbemerkung 2")); + propertyRepository.save(new Property("Mustergebäude 3", "Mustersteet", "3", "32345", "New York", "CH", "Musterbemerkung 3")); + System.out.println("Countries seeded."); + } else { + System.out.println("Countries already seeded."); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/iwomm/propify_api/dto/AttachmentDTO.java b/src/main/java/de/iwomm/propify_api/dto/AttachmentDTO.java new file mode 100644 index 0000000..c260b1b --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/dto/AttachmentDTO.java @@ -0,0 +1,39 @@ +package de.iwomm.propify_api.dto; + +import java.util.UUID; + +public class AttachmentDTO { + private UUID id; + private String storagePath; + private String fileName; + + public AttachmentDTO(UUID id, String storagePath, String fileName) { + this.id = id; + this.storagePath = storagePath; + this.fileName = fileName; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getStoragePath() { + return storagePath; + } + + public void setStoragePath(String storagePath) { + this.storagePath = storagePath; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } +} diff --git a/src/main/java/de/iwomm/propify_api/dto/BulkDeletePropertyIdsDTO.java b/src/main/java/de/iwomm/propify_api/dto/BulkDeletePropertyIdsDTO.java new file mode 100644 index 0000000..5dade0c --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/dto/BulkDeletePropertyIdsDTO.java @@ -0,0 +1,17 @@ +package de.iwomm.propify_api.dto; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class BulkDeletePropertyIdsDTO { + List ids = new ArrayList<>(); + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } +} diff --git a/src/main/java/de/iwomm/propify_api/dto/PropertyDTO.java b/src/main/java/de/iwomm/propify_api/dto/PropertyDTO.java new file mode 100644 index 0000000..d12d9fe --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/dto/PropertyDTO.java @@ -0,0 +1,101 @@ +package de.iwomm.propify_api.dto; + +import java.util.List; +import java.util.UUID; + +public class PropertyDTO { + private UUID id; + private String name; + private String street; + private String houseNumber; + private String zipCode; + private String city; + private String country; + private String notes; + private List attachments; + + + public PropertyDTO(UUID id, String name, String street, String houseNumber, String zipCode, String city, String country, String notes, List attachments) { + this.id = id; + this.name = name; + this.street = street; + this.houseNumber = houseNumber; + this.zipCode = zipCode; + this.city = city; + this.country = country; + this.notes = notes; + this.attachments = attachments; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getHouseNumber() { + return houseNumber; + } + + public void setHouseNumber(String houseNumber) { + this.houseNumber = houseNumber; + } + + public String getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public List getAttachments() { + return attachments; + } + + public void setAttachments(List attachments) { + this.attachments = attachments; + } +} diff --git a/src/main/java/de/iwomm/propify_api/entity/Attachment.java b/src/main/java/de/iwomm/propify_api/entity/Attachment.java new file mode 100644 index 0000000..68fccff --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/entity/Attachment.java @@ -0,0 +1,72 @@ +package de.iwomm.propify_api.entity; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +public class Attachment { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(nullable = false) + private UUID id; + + @Column(nullable = false) + private String storagePath; + + @Column(nullable = false) + private String fileName; + + @ManyToOne() + @JoinColumn(name = "property_id") + private Property property; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public void setProperty(Property property) { + this.property = property; + } + + public Property getProperty() { + return property; + } + + public String getStoragePath() { + return storagePath; + } + + public void setStoragePath(String storagePath) { + this.storagePath = storagePath; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } +} diff --git a/src/main/java/de/iwomm/propify_api/entity/Property.java b/src/main/java/de/iwomm/propify_api/entity/Property.java index cbfe165..0dc27a9 100644 --- a/src/main/java/de/iwomm/propify_api/entity/Property.java +++ b/src/main/java/de/iwomm/propify_api/entity/Property.java @@ -1,9 +1,9 @@ package de.iwomm.propify_api.entity; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; @Entity @@ -12,16 +12,83 @@ public class Property { @GeneratedValue private UUID id; + @Column(nullable = false) private String name; + + @Column(nullable = false) private String street; + + @Column(nullable = false) private String houseNumber; + + @Column(nullable = false) private String zipCode; + + @Column(nullable = false) private String city; - private String country; + private String notes; + @Column(nullable = false) + private String country; + + @OneToMany(mappedBy = "property", orphanRemoval = true) + @OrderBy("fileName") + private List attachments; + + @Column(nullable = false) + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; + + public List getAttachments() { + return attachments; + } + + public void setAttachments(List attachment) { + this.attachments = attachment; + } + + public Property() {} + public Property(String name, String street, String houseNumber, String zipCode, String city, String country, String notes) { + this.name = name; + this.street = street; + this.houseNumber = houseNumber; + this.zipCode = zipCode; + this.city = city; + this.country = country; + this.notes = notes; + } + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + public void setId(UUID id) { this.id = id; } diff --git a/src/main/java/de/iwomm/propify_api/repository/AttachmentRepository.java b/src/main/java/de/iwomm/propify_api/repository/AttachmentRepository.java new file mode 100644 index 0000000..21034cf --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/repository/AttachmentRepository.java @@ -0,0 +1,10 @@ +package de.iwomm.propify_api.repository; + +import de.iwomm.propify_api.entity.Attachment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface AttachmentRepository extends JpaRepository { +} diff --git a/src/main/java/de/iwomm/propify_api/s3/S3ClientConfig.java b/src/main/java/de/iwomm/propify_api/s3/S3ClientConfig.java new file mode 100644 index 0000000..e0d93dc --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/s3/S3ClientConfig.java @@ -0,0 +1,37 @@ +package de.iwomm.propify_api.s3; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "s3") +public class S3ClientConfig { + + private String accessKey; + private String secretKey; + private String endpoint; + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } +} \ No newline at end of file diff --git a/src/main/java/de/iwomm/propify_api/s3/S3Service.java b/src/main/java/de/iwomm/propify_api/s3/S3Service.java new file mode 100644 index 0000000..0f2df2a --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/s3/S3Service.java @@ -0,0 +1,46 @@ +package de.iwomm.propify_api.s3; + +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.File; +import java.io.IOException; +import java.net.URI; + +@Service +public class S3Service { + + private final S3Client s3Client; + + public S3Service(S3ClientConfig s3ClientConfig) { + AwsBasicCredentials credentials = AwsBasicCredentials.create(s3ClientConfig.getAccessKey(), s3ClientConfig.getSecretKey()); + + this.s3Client = S3Client.builder() + .endpointOverride(URI.create(s3ClientConfig.getEndpoint())) + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .region(Region.US_EAST_1) + .serviceConfiguration(S3Configuration.builder() + .pathStyleAccessEnabled(true) + .build()) + .build(); + } + + + public void putObject(String bucket, String key, MultipartFile file) throws IOException { + PutObjectRequest request = PutObjectRequest.builder() + .bucket(bucket) + .key(key) + .contentType(file.getContentType()) + .build(); + + s3Client.putObject(request, RequestBody.fromBytes(file.getBytes())); + } +} diff --git a/src/main/java/de/iwomm/propify_api/s3/UploadResponse.java b/src/main/java/de/iwomm/propify_api/s3/UploadResponse.java new file mode 100644 index 0000000..1d4025e --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/s3/UploadResponse.java @@ -0,0 +1,33 @@ +package de.iwomm.propify_api.s3; + +import org.springframework.http.HttpStatus; + +import java.util.ArrayList; +import java.util.List; + +public class UploadResponse { + private final HttpStatus status; + private List messages; + + public UploadResponse(HttpStatus status, List messages) { + this.status = status; + this.messages = messages; + } + + public UploadResponse(HttpStatus status, String message) { + this.status = status; + this.messages = new ArrayList<>(); + this.messages.add(message); + } + + public List getMessages() { + return messages; + } + + public void setMessages(List messages) { + this.messages = messages; + } + public void addMessage(String message) { + this.messages.add(message); + } +} diff --git a/src/main/java/de/iwomm/propify_api/security/CorsConfig.java b/src/main/java/de/iwomm/propify_api/security/CorsConfig.java index 6705614..ccc8ae6 100644 --- a/src/main/java/de/iwomm/propify_api/security/CorsConfig.java +++ b/src/main/java/de/iwomm/propify_api/security/CorsConfig.java @@ -1,14 +1,9 @@ package de.iwomm.propify_api.security; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import java.util.ArrayList; -import java.util.List; - @Configuration public class CorsConfig implements WebMvcConfigurer { private final CorsProperties corsProperties; @@ -21,7 +16,7 @@ public class CorsConfig implements WebMvcConfigurer { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // Apply rules to all endpoints .allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0])) // This targets the frontend app's URLs (you can allow multiple URLs, e.g. "http://localhost:4200,http://example.com" - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedMethods("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS") .allowedHeaders(corsProperties.getAllowedHeaders()) .allowCredentials(true); // Allow cookies and authentication headers } diff --git a/src/main/java/de/iwomm/propify_api/security/KeycloakRoleConverter.java b/src/main/java/de/iwomm/propify_api/security/KeycloakRoleConverter.java index d09984e..625cd4a 100644 --- a/src/main/java/de/iwomm/propify_api/security/KeycloakRoleConverter.java +++ b/src/main/java/de/iwomm/propify_api/security/KeycloakRoleConverter.java @@ -14,17 +14,17 @@ public class KeycloakRoleConverter implements Converter convert(Jwt jwt) { - // Holen Sie sich das "realm_access" Feld aus dem Token + // Get the "realm_access" field from token Map realmAccess = (Map) jwt.getClaims().get("realm_access"); if (realmAccess == null || realmAccess.isEmpty()) { return List.of(); } - // Holen Sie sich die Liste der Rollen + // Get list of roles List roles = (List) realmAccess.get("roles"); - // Konvertieren Sie die Rollen in Spring Security GrantedAuthority-Objekte + // KConvert roles to Spring Security GrantedAuthority-Objects return roles.stream() .map(roleName -> "ROLE_" + roleName.toUpperCase()) // Empfohlene Namenskonvention .map(SimpleGrantedAuthority::new) diff --git a/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java b/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java index 361dde4..b74fd57 100644 --- a/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java +++ b/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java @@ -9,7 +9,6 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity @@ -36,7 +35,7 @@ public class SecurityConfig { return http.build(); } - // Konvertiert die Keycloak-Rollen (im JWT) in Spring Security Authorities + // Convert Keycloak-Roles (in JWT) in Spring Security Authorities private JwtAuthenticationConverter jwtAuthenticationConverter() { JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter()); diff --git a/src/main/java/de/iwomm/propify_api/service/AttachmentService.java b/src/main/java/de/iwomm/propify_api/service/AttachmentService.java new file mode 100644 index 0000000..270f8bc --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/service/AttachmentService.java @@ -0,0 +1,46 @@ +package de.iwomm.propify_api.service; + +import de.iwomm.propify_api.dto.AttachmentDTO; +import de.iwomm.propify_api.dto.PropertyDTO; +import de.iwomm.propify_api.entity.Attachment; +import de.iwomm.propify_api.repository.AttachmentRepository; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Service +public class AttachmentService { + private final AttachmentRepository attachmentRepository; + + public AttachmentService(AttachmentRepository attachmentRepository) { + this.attachmentRepository = attachmentRepository; + } + + public void save(Attachment attachment) { + attachmentRepository.save(attachment); + } + + public void delete(Attachment attachment) { + attachmentRepository.delete(attachment); + } + + public void deleteById(UUID id) { + attachmentRepository.deleteById(id); + } + + public List toDTOs(List attachments) { + List dtos = new ArrayList<>(); + + attachments.forEach(attachment -> { + dtos.add(new AttachmentDTO( + attachment.getId(), + attachment.getStoragePath(), + attachment.getFileName() + )); + }); + + return dtos; + } +} diff --git a/src/main/java/de/iwomm/propify_api/service/PropertyService.java b/src/main/java/de/iwomm/propify_api/service/PropertyService.java index 7e51012..a392ba9 100644 --- a/src/main/java/de/iwomm/propify_api/service/PropertyService.java +++ b/src/main/java/de/iwomm/propify_api/service/PropertyService.java @@ -1,9 +1,15 @@ package de.iwomm.propify_api.service; +import de.iwomm.propify_api.dto.AttachmentDTO; +import de.iwomm.propify_api.dto.BulkDeletePropertyIdsDTO; +import de.iwomm.propify_api.dto.PropertyDTO; +import de.iwomm.propify_api.entity.Attachment; import de.iwomm.propify_api.entity.Property; import de.iwomm.propify_api.repository.PropertyRepository; +import jakarta.persistence.EntityNotFoundException; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -32,7 +38,65 @@ public class PropertyService { return propertyRepository.save(property); } - public void delete(Property property) { + public void deleteById(UUID id) { + Property property = propertyRepository.findById(id).orElseThrow(EntityNotFoundException::new); propertyRepository.delete(property); } + + public void deleteByIds(BulkDeletePropertyIdsDTO propertyIdsDTO) { + propertyRepository.deleteAllById(propertyIdsDTO.getIds()); + } + + public Property update(UUID id, PropertyDTO propertyDto) { + Property updated = propertyRepository.findById(id).orElseThrow(EntityNotFoundException::new); + + updated.setName(propertyDto.getName()); + updated.setStreet(propertyDto.getStreet()); + updated.setHouseNumber(propertyDto.getHouseNumber()); + updated.setZipCode(propertyDto.getZipCode()); + updated.setCity(propertyDto.getCity()); + updated.setCountry(propertyDto.getCountry()); + updated.setNotes(propertyDto.getNotes()); + + propertyRepository.save(updated); + + return updated; + } + + public Property saveDTO(PropertyDTO dto) { + return this.save(new Property( + dto.getName(), + dto.getStreet(), + dto.getHouseNumber(), + dto.getZipCode(), + dto.getCity(), + dto.getCountry(), + dto.getNotes() + )); + } + + public List toDTOs(List properties) { + List dtos = new ArrayList<>(); + + properties.forEach(property -> { + List attachments = new ArrayList<>(); + property.getAttachments().forEach(attachment -> { + attachments.add(new AttachmentDTO(attachment.getId(), attachment.getStoragePath(), attachment.getFileName())); + }); + + dtos.add(new PropertyDTO( + property.getId(), + property.getName(), + property.getStreet(), + property.getHouseNumber(), + property.getZipCode(), + property.getCity(), + property.getCountry(), + property.getNotes(), + attachments + )); + }); + + return dtos; + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 872f6f7..9ae804c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,32 +1,60 @@ spring: + + servlet: + multipart: + max-file-size: 1000MB # Maximal zulässige Größe pro Datei (z.B. 10 MB) + max-request-size: 5000MB # Maximal zulässige Größe der gesamten Anfrage (z.B. 50 MB) + profiles: + active: dev application: - name: propify-api + name: skamp-api datasource: - url: jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1 - driver-class-name: org.h2.Driver - username: sa - password: + url: jdbc:postgresql://localhost:5432/skamp + username: dev + password: dev + driver-class-name: org.postgresql.Driver jpa: hibernate: - ddl-auto: update + ddl-auto: update # möglich: validate, update, create, create-drop show-sql: true - h2: - console: - enabled: true + properties: + hibernate: + format_sql: true + database-platform: org.hibernate.dialect.PostgreSQLDialect +# datasource: +# url: jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1 +# driver-class-name: org.h2.Driver +# username: sa +# password: +# jpa: +# hibernate: +# ddl-auto: update +# show-sql: true +# h2: +# console: +# enabled: true + security: oauth2: resourceserver: jwt: - issuer-uri: http://localhost:8280/realms/propify + issuer-uri: http://localhost:8280/realms/skamp jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs + logging: level: org: springframework: security: DEBUG + + cors: allowed-origins: - http://localhost:4200 - - http://localhost:8080 - allowed-headers: "*" \ No newline at end of file + allowed-headers: "*" + +s3: + access-key: dev + secret-key: dev123456 + endpoint: http://localhost:9000 \ No newline at end of file