Some improvements:

* Switches to PostgreSQL
* Added Minio storage
* Added attachments to properties
* Introduced DTOs for improved security
This commit is contained in:
2025-09-03 15:40:26 +02:00
parent 9735f1f398
commit 5eb6b6e738
26 changed files with 879 additions and 67 deletions

10
README.md Normal file
View File

@@ -0,0 +1,10 @@
# SKAMP Base API
## Environment
The default profile is `dev`.
In production, set it to `prod` via environment variables:
```bash
java ...
```

View File

@@ -5,7 +5,7 @@ meta {
} }
post { post {
url: {{KEYCLOAK_BASE_URL}}/realms/propify/protocol/openid-connect/token url: {{KEYCLOAK_BASE_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token
body: formUrlEncoded body: formUrlEncoded
auth: inherit auth: inherit
} }

View File

@@ -16,12 +16,12 @@ auth:bearer {
body:json { body:json {
{ {
"name": "Mustername 1", "name": "Bungalow",
"street": "Musterstraße", "street": "Hebbelstraße",
"houseNumber": "1", "houseNumber": "30",
"zipCode": "55123", "zipCode": "55127",
"city": "Musterstadt", "city": "Mainz",
"country": "de", "country": "DE",
"notes": "Lorem ipsum" "notes": "Lorem ipsum"
} }
} }

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"version": "1", "version": "1",
"name": "propify", "name": "skamp-api",
"type": "collection", "type": "collection",
"ignore": [ "ignore": [
"node_modules", "node_modules",

View File

@@ -1,11 +1,14 @@
vars { vars {
API_BASE_URL: http://localhost:8080 API_BASE_URL: http://localhost:8080
API_VERSION: v1 API_VERSION: v1
BEARER_TOKEN: -
KEYCLOAK_BASE_URL: http://localhost:8280 KEYCLOAK_BASE_URL: http://localhost:8280
DEV_USERNAME: dev@example.com DEV_USERNAME: dev@example.com
DEV_PASSWORD: dev DEV_PASSWORD: dev
ADMIN_USERNAME: admin@example.com ADMIN_USERNAME: admin@example.com
ADMIN_PASSWORD: admin ADMIN_PASSWORD: admin
KEYCLOAK_CLIENT_ID: propify-app KEYCLOAK_CLIENT_ID: skamp-app
KEYCLOAK_REALM: skamp
} }
vars:secret [
BEARER_TOKEN
]

20
pom.xml
View File

@@ -9,10 +9,10 @@
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>de.iwomm</groupId> <groupId>de.iwomm</groupId>
<artifactId>propify-api</artifactId> <artifactId>skamp-api</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<name>propify-api</name> <name>skamp-api</name>
<description>Propify API</description> <description>SKAMP API</description>
<url/> <url/>
<licenses> <licenses>
<license/> <license/>
@@ -84,6 +84,20 @@
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/software.amazon.awssdk/s3 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.33.0</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -4,10 +4,10 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
public class PropifyApiApplication { public class SkampApiApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(PropifyApiApplication.class, args); SpringApplication.run(SkampApiApplication.class, args);
} }
} }

View File

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

View File

@@ -1,58 +1,185 @@
package de.iwomm.propify_api.controller; 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.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 de.iwomm.propify_api.service.PropertyService;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; 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.io.IOException;
import java.util.Optional; import java.net.URI;
import java.util.UUID; import java.util.*;
@RestController @RestController
@RequestMapping("/api/v1/properties") @RequestMapping("/api/v1/properties")
public class PropertyController { public class PropertyController {
private final PropertyService propertyService; 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; this.propertyService = propertyService;
} this.attachmentService = attachmentService;
this.s3service = s3Service;
@GetMapping("/info")
@PreAuthorize("isAuthenticated()")
public String infoEndpoint() {
return "Hello, you are authenticated!";
} }
@GetMapping @GetMapping
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')") @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
public List<Property> getAllProperties() { public ResponseEntity<?> getAllProperties() {
return propertyService.findAll(); List<PropertyDTO> propertiesDTO = propertyService.toDTOs(propertyService.findAll());
return ResponseEntity
.ok(propertiesDTO);
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("hasAnyRole('ADMIN', 'USER')") @PreAuthorize("hasAnyRole('ADMIN', 'USER')")
public Property getPropertyById(@PathVariable UUID id) { public ResponseEntity<?> getPropertyById(@PathVariable UUID id) {
return propertyService.findById(id).orElse(null); 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<MultipartFile> files, @PathVariable UUID id) {
List<String> 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 @PostMapping
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasRole('ADMIN')")
public Property createProperty(@RequestBody Property property) { public ResponseEntity<?> createProperty(@RequestBody PropertyDTO propertyDTO) {
return propertyService.save(property); 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}") @DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasRole('ADMIN')")
public void deleteProperty(@PathVariable UUID id) { public ResponseEntity<?> deleteProperty(@PathVariable UUID id) {
Optional<Property> property = propertyService.findById(id); try {
if (property.isEmpty()) { propertyService.deleteById(id);
return;
}
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();
}
} }
} }

View File

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

View File

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

View File

@@ -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<UUID> ids = new ArrayList<>();
public List<UUID> getIds() {
return ids;
}
public void setIds(List<UUID> ids) {
this.ids = ids;
}
}

View File

@@ -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<AttachmentDTO> attachments;
public PropertyDTO(UUID id, String name, String street, String houseNumber, String zipCode, String city, String country, String notes, List<AttachmentDTO> 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<AttachmentDTO> getAttachments() {
return attachments;
}
public void setAttachments(List<AttachmentDTO> attachments) {
this.attachments = attachments;
}
}

View File

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

View File

@@ -1,9 +1,9 @@
package de.iwomm.propify_api.entity; package de.iwomm.propify_api.entity;
import jakarta.persistence.Entity; import jakarta.persistence.*;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Entity @Entity
@@ -12,16 +12,83 @@ public class Property {
@GeneratedValue @GeneratedValue
private UUID id; private UUID id;
@Column(nullable = false)
private String name; private String name;
@Column(nullable = false)
private String street; private String street;
@Column(nullable = false)
private String houseNumber; private String houseNumber;
@Column(nullable = false)
private String zipCode; private String zipCode;
@Column(nullable = false)
private String city; private String city;
private String country;
private String notes; private String notes;
@Column(nullable = false)
private String country;
@OneToMany(mappedBy = "property", orphanRemoval = true)
@OrderBy("fileName")
private List<Attachment> attachments;
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public List<Attachment> getAttachments() {
return attachments;
}
public void setAttachments(List<Attachment> attachment) {
this.attachments = attachment;
}
public Property() {} 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) { public void setId(UUID id) {
this.id = id; this.id = id;
} }

View File

@@ -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<Attachment, UUID> {
}

View File

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

View File

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

View File

@@ -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<String> messages;
public UploadResponse(HttpStatus status, List<String> 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<String> getMessages() {
return messages;
}
public void setMessages(List<String> messages) {
this.messages = messages;
}
public void addMessage(String message) {
this.messages.add(message);
}
}

View File

@@ -1,14 +1,9 @@
package de.iwomm.propify_api.security; 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.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
@Configuration @Configuration
public class CorsConfig implements WebMvcConfigurer { public class CorsConfig implements WebMvcConfigurer {
private final CorsProperties corsProperties; private final CorsProperties corsProperties;
@@ -21,7 +16,7 @@ public class CorsConfig implements WebMvcConfigurer {
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // Apply rules to all endpoints 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" .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()) .allowedHeaders(corsProperties.getAllowedHeaders())
.allowCredentials(true); // Allow cookies and authentication headers .allowCredentials(true); // Allow cookies and authentication headers
} }

View File

@@ -14,17 +14,17 @@ public class KeycloakRoleConverter implements Converter<Jwt, Collection<GrantedA
@Override @Override
public Collection<GrantedAuthority> convert(Jwt jwt) { public Collection<GrantedAuthority> convert(Jwt jwt) {
// Holen Sie sich das "realm_access" Feld aus dem Token // Get the "realm_access" field from token
Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get("realm_access"); Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get("realm_access");
if (realmAccess == null || realmAccess.isEmpty()) { if (realmAccess == null || realmAccess.isEmpty()) {
return List.of(); return List.of();
} }
// Holen Sie sich die Liste der Rollen // Get list of roles
List<String> roles = (List<String>) realmAccess.get("roles"); List<String> roles = (List<String>) realmAccess.get("roles");
// Konvertieren Sie die Rollen in Spring Security GrantedAuthority-Objekte // KConvert roles to Spring Security GrantedAuthority-Objects
return roles.stream() return roles.stream()
.map(roleName -> "ROLE_" + roleName.toUpperCase()) // Empfohlene Namenskonvention .map(roleName -> "ROLE_" + roleName.toUpperCase()) // Empfohlene Namenskonvention
.map(SimpleGrantedAuthority::new) .map(SimpleGrantedAuthority::new)

View File

@@ -9,7 +9,6 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -36,7 +35,7 @@ public class SecurityConfig {
return http.build(); 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() { private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter()); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());

View File

@@ -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<AttachmentDTO> toDTOs(List<Attachment> attachments) {
List<AttachmentDTO> dtos = new ArrayList<>();
attachments.forEach(attachment -> {
dtos.add(new AttachmentDTO(
attachment.getId(),
attachment.getStoragePath(),
attachment.getFileName()
));
});
return dtos;
}
}

View File

@@ -1,9 +1,15 @@
package de.iwomm.propify_api.service; 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.entity.Property;
import de.iwomm.propify_api.repository.PropertyRepository; import de.iwomm.propify_api.repository.PropertyRepository;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@@ -32,7 +38,65 @@ public class PropertyService {
return propertyRepository.save(property); 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); 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<PropertyDTO> toDTOs(List<Property> properties) {
List<PropertyDTO> dtos = new ArrayList<>();
properties.forEach(property -> {
List<AttachmentDTO> 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;
}
} }

View File

@@ -1,32 +1,60 @@
spring: 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: application:
name: propify-api name: skamp-api
datasource: datasource:
url: jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1 url: jdbc:postgresql://localhost:5432/skamp
driver-class-name: org.h2.Driver username: dev
username: sa password: dev
password: driver-class-name: org.postgresql.Driver
jpa: jpa:
hibernate: hibernate:
ddl-auto: update ddl-auto: update # möglich: validate, update, create, create-drop
show-sql: true show-sql: true
h2: properties:
console: hibernate:
enabled: true 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: security:
oauth2: oauth2:
resourceserver: resourceserver:
jwt: 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 jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs
logging: logging:
level: level:
org: org:
springframework: springframework:
security: DEBUG security: DEBUG
cors: cors:
allowed-origins: allowed-origins:
- http://localhost:4200 - http://localhost:4200
- http://localhost:8080 allowed-headers: "*"
allowed-headers: "*"
s3:
access-key: dev
secret-key: dev123456
endpoint: http://localhost:9000