Initial commit with basic CRUD functionality:

* GET all properties
* GET one property by id
* CREATE one property
* DELETE one property by id
This commit is contained in:
2025-08-28 12:57:36 +02:00
parent 32e41f710b
commit 9735f1f398
14 changed files with 241 additions and 18 deletions

View File

@@ -0,0 +1,48 @@
meta {
name: Auth
type: http
seq: 2
}
post {
url: {{KEYCLOAK_BASE_URL}}/realms/propify/protocol/openid-connect/token
body: formUrlEncoded
auth: inherit
}
headers {
Content-Type: application/x-www-form-urlencoded
}
body:form-urlencoded {
grant_type: password
client_id: {{KEYCLOAK_CLIENT_ID}}
username: {{ADMIN_USERNAME}}
password: {{ADMIN_PASSWORD}}
}
script:post-response {
// Parse die JSON-Antwort
const jsonResponse = res.body;
if (jsonResponse.access_token) {
// Schreibe den access_token in eine Umgebungsvariable
// oder in eine collection-Variable
// Option 1: In eine Umgebungsvariable schreiben
// (z.B. für eine bestimmte Umgebung wie "Development")
bru.setEnvVar("BEARER_TOKEN", jsonResponse.access_token);
// Option 2: In eine Collection-Variable schreiben
// (Diese Variable ist global für alle Anfragen in deiner Collection)
// bru.setVar("bearerToken", "Bearer " + jsonResponse.access_token);
} else {
// optional: Error Handling, falls der Token nicht in der Antwort ist
console.log("Error: access_token not found in the response.");
}
}
settings {
encodeUrl: false
}

View File

@@ -0,0 +1,8 @@
meta {
name: Authenticate
seq: 2
}
auth {
mode: inherit
}

View File

@@ -5,9 +5,13 @@ meta {
} }
post { post {
url: {{BASE_URL}}/api/{{API_VERSION}}/properties url: {{API_BASE_URL}}/api/{{API_VERSION}}/properties
body: json body: json
auth: inherit auth: bearer
}
auth:bearer {
token: {{BEARER_TOKEN}}
} }
body:json { body:json {

View File

@@ -5,9 +5,13 @@ meta {
} }
delete { delete {
url: {{BASE_URL}}/api/{{API_VERSION}}/properties/e64d5e53-cd45-45fb-9237-46078077bf22 url: API_BASE_URL}}/api/{{API_VERSION}}/properties/e64d5e53-cd45-45fb-9237-46078077bf22
body: json body: json
auth: inherit auth: bearer
}
auth:bearer {
token: {{BEARER_TOKEN}}
} }
body:json { body:json {

View File

@@ -5,9 +5,13 @@ meta {
} }
get { get {
url: {{BASE_URL}}/api/{{API_VERSION}}/properties url: {{API_BASE_URL}}/api/{{API_VERSION}}/properties
body: none body: none
auth: inherit auth: bearer
}
auth:bearer {
token: {{BEARER_TOKEN}}
} }
settings { settings {

View File

@@ -5,9 +5,13 @@ meta {
} }
get { get {
url: {{BASE_URL}}/api/{{API_VERSION}}/properties/b6ab79d8-f7eb-4fb7-acde-e3310cb0166c url: {{API_BASE_URL}}/api/{{API_VERSION}}/properties/5dba067e-d7fd-4d79-a08a-ec379834938a
body: none body: none
auth: inherit auth: bearer
}
auth:bearer {
token: {{BEARER_TOKEN}}
} }
settings { settings {

View File

@@ -1,4 +1,11 @@
vars { vars {
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
DEV_USERNAME: dev@example.com
DEV_PASSWORD: dev
ADMIN_USERNAME: admin@example.com
ADMIN_PASSWORD: admin
KEYCLOAK_CLIENT_ID: propify-app
} }

View File

@@ -75,6 +75,15 @@
<version>2.6.0</version> <version>2.6.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -3,6 +3,7 @@ package de.iwomm.propify_api.controller;
import de.iwomm.propify_api.entity.Property; import de.iwomm.propify_api.entity.Property;
import de.iwomm.propify_api.service.PropertyService; import de.iwomm.propify_api.service.PropertyService;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
@@ -18,24 +19,34 @@ public class PropertyController {
this.propertyService = propertyService; this.propertyService = propertyService;
} }
@GetMapping("/info")
@PreAuthorize("isAuthenticated()")
public String infoEndpoint() {
return "Hello, you are authenticated!";
}
@GetMapping @GetMapping
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
public List<Property> getAllProperties() { public List<Property> getAllProperties() {
return propertyService.findAll(); return propertyService.findAll();
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
public Property getPropertyById(@PathVariable UUID id) { public Property getPropertyById(@PathVariable UUID id) {
return propertyService.findById(id).orElse(null); return propertyService.findById(id).orElse(null);
} }
@PostMapping @PostMapping
@ResponseStatus(HttpStatus.CREATED) @ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('ADMIN')")
public Property createProperty(@RequestBody Property property) { public Property createProperty(@RequestBody Property property) {
return propertyService.save(property); return propertyService.save(property);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ADMIN')")
public void deleteProperty(@PathVariable UUID id) { public void deleteProperty(@PathVariable UUID id) {
Optional<Property> property = propertyService.findById(id); Optional<Property> property = propertyService.findById(id);
if (property.isEmpty()) { if (property.isEmpty()) {

View File

@@ -0,0 +1,28 @@
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;
public CorsConfig(CorsProperties corsProperties) {
this.corsProperties = corsProperties;
}
@Override
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")
.allowedHeaders(corsProperties.getAllowedHeaders())
.allowCredentials(true); // Allow cookies and authentication headers
}
}

View File

@@ -0,0 +1,29 @@
package de.iwomm.propify_api.security;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
@ConfigurationProperties(prefix = "cors")
public class CorsProperties {
private List<String> allowedOrigins;
private String allowedHeaders;
public List<String> getAllowedOrigins() {
return allowedOrigins;
}
public void setAllowedOrigins(List<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
}
public String getAllowedHeaders() {
return allowedHeaders;
}
public void setAllowedHeaders(String allowedHeaders) {
this.allowedHeaders = allowedHeaders;
}
}

View File

@@ -0,0 +1,33 @@
package de.iwomm.propify_api.security;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class KeycloakRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
// Holen Sie sich das "realm_access" Feld aus dem Token
Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get("realm_access");
if (realmAccess == null || realmAccess.isEmpty()) {
return List.of();
}
// Holen Sie sich die Liste der Rollen
List<String> roles = (List<String>) realmAccess.get("roles");
// Konvertieren Sie die Rollen in Spring Security GrantedAuthority-Objekte
return roles.stream()
.map(roleName -> "ROLE_" + roleName.toUpperCase()) // Empfohlene Namenskonvention
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}

View File

@@ -2,27 +2,44 @@ package de.iwomm.propify_api.security;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity // Wichtig für die Verwendung von @PreAuthorize
public class SecurityConfig { public class SecurityConfig {
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
.csrf(csrf -> csrf.disable()) // CSRF deaktivieren für APIs .csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth .cors(cors -> {})
.requestMatchers("/api/**").permitAll() // API ist offen
.anyRequest().permitAll() // Alles andere auch offen .authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
) )
.httpBasic(httpBasic -> httpBasic.disable()) // Kein Basic Auth
.formLogin(form -> form.disable()); // Kein Login-Formular .oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build(); return http.build();
} }
// Konvertiert die Keycloak-Rollen (im JWT) in Spring Security Authorities
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());
return jwtAuthenticationConverter;
}
} }

View File

@@ -13,3 +13,20 @@ spring:
h2: h2:
console: console:
enabled: true enabled: true
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8280/realms/propify
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: "*"