diff --git a/bruno/propify/Authenticate/Auth App.bru b/bruno/propify/Authenticate/Auth App.bru deleted file mode 100644 index 3fa271b..0000000 --- a/bruno/propify/Authenticate/Auth App.bru +++ /dev/null @@ -1,48 +0,0 @@ -meta { - name: Auth App - type: http - seq: 1 -} - -post { - url: {{KEYCLOAK_BASE_URL}}/realms/{{KEYCLOAK_REALM}}/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 -} diff --git a/bruno/propify/DataImport/Auth Bruno.bru b/bruno/propify/Authenticate/Auth Bruno.bru similarity index 96% rename from bruno/propify/DataImport/Auth Bruno.bru rename to bruno/propify/Authenticate/Auth Bruno.bru index d24f723..a9fe9b7 100644 --- a/bruno/propify/DataImport/Auth Bruno.bru +++ b/bruno/propify/Authenticate/Auth Bruno.bru @@ -33,6 +33,7 @@ script:post-response { // (z.B. für eine bestimmte Umgebung wie "Development") bru.setEnvVar("BEARER_TOKEN", jsonResponse.access_token); + console.log("Updated 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); diff --git a/bruno/propify/DataImport/Upload Organizations.bru b/bruno/propify/DataImport/Upload Organizations.bru index 2934ebb..fa494d2 100644 --- a/bruno/propify/DataImport/Upload Organizations.bru +++ b/bruno/propify/DataImport/Upload Organizations.bru @@ -1,7 +1,7 @@ meta { name: Upload Organizations type: http - seq: 3 + seq: 2 } post { diff --git a/bruno/propify/DataImport/Upload Persons.bru b/bruno/propify/DataImport/Upload Persons.bru index 2a42436..0f657ee 100644 --- a/bruno/propify/DataImport/Upload Persons.bru +++ b/bruno/propify/DataImport/Upload Persons.bru @@ -1,7 +1,7 @@ meta { name: Upload Persons type: http - seq: 2 + seq: 1 } post { diff --git a/bruno/propify/Industries/Create new.bru b/bruno/propify/Industries/Create new.bru new file mode 100644 index 0000000..16ed702 --- /dev/null +++ b/bruno/propify/Industries/Create new.bru @@ -0,0 +1,31 @@ +meta { + name: Create new + type: http + seq: 3 +} + +post { + url: {{API_BASE_URL}}/api/{{API_VERSION}}/industries + body: json + 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" + } +} + +settings { + encodeUrl: true +} diff --git a/bruno/propify/Industries/Delete one by ID.bru b/bruno/propify/Industries/Delete one by ID.bru new file mode 100644 index 0000000..ca77999 --- /dev/null +++ b/bruno/propify/Industries/Delete one by ID.bru @@ -0,0 +1,31 @@ +meta { + name: Delete one by ID + type: http + seq: 4 +} + +delete { + url: API_BASE_URL}}/api/{{API_VERSION}}/industries/e64d5e53-cd45-45fb-9237-46078077bf22 + body: json + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} +} + +body:json { + { + "name": "Mustername 1", + "street": "Musterstraße", + "houseNumber": "1", + "zipCode": "55123", + "city": "Musterstadt", + "country": "de", + "notes": "Lorem ipsum" + } +} + +settings { + encodeUrl: true +} diff --git a/bruno/propify/Industries/Get all.bru b/bruno/propify/Industries/Get all.bru new file mode 100644 index 0000000..364a2de --- /dev/null +++ b/bruno/propify/Industries/Get all.bru @@ -0,0 +1,19 @@ +meta { + name: Get all + type: http + seq: 1 +} + +get { + url: {{API_BASE_URL}}/api/{{API_VERSION}}/industries + body: none + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} +} + +settings { + encodeUrl: true +} diff --git a/bruno/propify/Industries/Get one by ID.bru b/bruno/propify/Industries/Get one by ID.bru new file mode 100644 index 0000000..4902953 --- /dev/null +++ b/bruno/propify/Industries/Get one by ID.bru @@ -0,0 +1,19 @@ +meta { + name: Get one by ID + type: http + seq: 2 +} + +get { + url: {{API_BASE_URL}}/api/{{API_VERSION}}/industries/da002464-33e9-4bfd-9ae2-836b26955502 + body: none + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} +} + +settings { + encodeUrl: true +} diff --git a/bruno/propify/Industries/Upload files.bru b/bruno/propify/Industries/Upload files.bru new file mode 100644 index 0000000..4752c85 --- /dev/null +++ b/bruno/propify/Industries/Upload files.bru @@ -0,0 +1,39 @@ +meta { + name: Upload files + type: http + seq: 5 +} + +post { + url: {{API_BASE_URL}}/api/{{API_VERSION}}/industries/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/Industries/folder.bru b/bruno/propify/Industries/folder.bru new file mode 100644 index 0000000..dd5bf73 --- /dev/null +++ b/bruno/propify/Industries/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Industries + seq: 9 +} + +auth { + mode: inherit +} diff --git a/bruno/propify/Organizations/Create new.bru b/bruno/propify/Organizations/Create new.bru new file mode 100644 index 0000000..8c8fac4 --- /dev/null +++ b/bruno/propify/Organizations/Create new.bru @@ -0,0 +1,31 @@ +meta { + name: Create new + type: http + seq: 3 +} + +post { + url: {{API_BASE_URL}}/api/{{API_VERSION}}/organizations + body: json + 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" + } +} + +settings { + encodeUrl: true +} diff --git a/bruno/propify/Organizations/Delete one by ID.bru b/bruno/propify/Organizations/Delete one by ID.bru new file mode 100644 index 0000000..7a85bdf --- /dev/null +++ b/bruno/propify/Organizations/Delete one by ID.bru @@ -0,0 +1,31 @@ +meta { + name: Delete one by ID + type: http + seq: 4 +} + +delete { + url: API_BASE_URL}}/api/{{API_VERSION}}/organizations/e64d5e53-cd45-45fb-9237-46078077bf22 + body: json + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} +} + +body:json { + { + "name": "Mustername 1", + "street": "Musterstraße", + "houseNumber": "1", + "zipCode": "55123", + "city": "Musterstadt", + "country": "de", + "notes": "Lorem ipsum" + } +} + +settings { + encodeUrl: true +} diff --git a/bruno/propify/Organizations/Get all.bru b/bruno/propify/Organizations/Get all.bru new file mode 100644 index 0000000..aad5299 --- /dev/null +++ b/bruno/propify/Organizations/Get all.bru @@ -0,0 +1,19 @@ +meta { + name: Get all + type: http + seq: 1 +} + +get { + url: {{API_BASE_URL}}/api/{{API_VERSION}}/organizations + body: none + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} +} + +settings { + encodeUrl: true +} diff --git a/bruno/propify/Organizations/Get one by ID.bru b/bruno/propify/Organizations/Get one by ID.bru new file mode 100644 index 0000000..610c7e8 --- /dev/null +++ b/bruno/propify/Organizations/Get one by ID.bru @@ -0,0 +1,19 @@ +meta { + name: Get one by ID + type: http + seq: 2 +} + +get { + url: {{API_BASE_URL}}/api/{{API_VERSION}}/organizations/5dba067e-d7fd-4d79-a08a-ec379834938a + body: none + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} +} + +settings { + encodeUrl: true +} diff --git a/bruno/propify/Organizations/Upload files.bru b/bruno/propify/Organizations/Upload files.bru new file mode 100644 index 0000000..d26ab57 --- /dev/null +++ b/bruno/propify/Organizations/Upload files.bru @@ -0,0 +1,39 @@ +meta { + name: Upload files + type: http + seq: 5 +} + +post { + url: {{API_BASE_URL}}/api/{{API_VERSION}}/organizations/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/Organizations/folder.bru b/bruno/propify/Organizations/folder.bru new file mode 100644 index 0000000..cdeaa4a --- /dev/null +++ b/bruno/propify/Organizations/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Organizations + seq: 8 +} + +auth { + mode: inherit +} diff --git a/bruno/propify/environments/local.bru b/bruno/propify/environments/local.bru index 1da1817..7b74e8a 100644 --- a/bruno/propify/environments/local.bru +++ b/bruno/propify/environments/local.bru @@ -9,8 +9,8 @@ vars { KEYCLOAK_CLIENT_ID: skamp-app KEYCLOAK_REALM: skamp KEYCLOAK_BRUNO_CLIENT_ID: skamp-bruno - KEYCLOAK_BRUNO_CLIENT_SECRET: sNQpCVyVckGo5AZw7FqeW0POtgWuXzJt } vars:secret [ - BEARER_TOKEN + BEARER_TOKEN, + KEYCLOAK_BRUNO_CLIENT_SECRET ] diff --git a/src/main/java/de/iwomm/propify_api/controller/IndustryController.java b/src/main/java/de/iwomm/propify_api/controller/IndustryController.java new file mode 100644 index 0000000..b91d68a --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/controller/IndustryController.java @@ -0,0 +1,134 @@ +package de.iwomm.propify_api.controller; + +import de.iwomm.propify_api.dto.*; +import de.iwomm.propify_api.entity.Industry; +import de.iwomm.propify_api.mapper.IndustryMapper; +import de.iwomm.propify_api.service.IndustryService; +import jakarta.persistence.EntityNotFoundException; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/industries") +public class IndustryController { + private final IndustryService industryService; + private final IndustryMapper industryMapper; + + public IndustryController(IndustryService industryService, IndustryMapper industryMapper) { + this.industryService = industryService; + this.industryMapper = industryMapper; + } + + @GetMapping + @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')") + public ResponseEntity getAll() { + List industryDTOs = industryService.toDTOs(industryService.findAll()); + + return ResponseEntity + .ok(industryDTOs); + } + + @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')") + public ResponseEntity getById(@PathVariable UUID id) { + try { + Industry industry = industryService.findById(id).orElseThrow(EntityNotFoundException::new); + IndustryDTO projectDTO = industryMapper.toDto(industry); + + return ResponseEntity + .ok(projectDTO); + } catch (EntityNotFoundException e) { + return ResponseEntity + .notFound() + .build(); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } + } + + @PostMapping + @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')") + public ResponseEntity create(@RequestBody IndustryDTO newIndustryDTO) { + try { + Industry newItem = industryService.save(newIndustryDTO); + + 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("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')") + public ResponseEntity update(@PathVariable UUID id, @RequestBody IndustryDTO industryDTO) { + try { + return ResponseEntity + .ok(industryService.update(id, industryDTO)); + } catch (EntityNotFoundException e) { + return ResponseEntity + .notFound() + .build(); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')") + public ResponseEntity delete(@PathVariable UUID id) { + try { + industryService.deleteById(id); + + return ResponseEntity + .noContent() + .build(); + } catch (EntityNotFoundException e) { + return ResponseEntity + .notFound() + .build(); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } + } + + @PostMapping("/bulk-delete") + @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')") + public ResponseEntity deleteMany(@RequestBody BulkDeleteIdsDTO bulkDeleteIdsDTO) { + try { + industryService.deleteByIds(bulkDeleteIdsDTO); + + 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/controller/OrganizationController.java b/src/main/java/de/iwomm/propify_api/controller/OrganizationController.java new file mode 100644 index 0000000..c256885 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/controller/OrganizationController.java @@ -0,0 +1,102 @@ +package de.iwomm.propify_api.controller; + +import de.iwomm.propify_api.dto.OrganizationDTO; +import de.iwomm.propify_api.dto.ProjectDetailsDTO; +import de.iwomm.propify_api.entity.Organization; +import de.iwomm.propify_api.entity.Project; +import de.iwomm.propify_api.mapper.OrganizationMapper; +import de.iwomm.propify_api.service.OrganisationService; +import jakarta.persistence.EntityNotFoundException; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/organizations") +public class OrganizationController { + + private final OrganizationMapper organizationMapper; + private OrganisationService organisationService; + + public OrganizationController(OrganisationService organisationService, OrganizationMapper organizationMapper) { + this.organisationService = organisationService; + this.organizationMapper = organizationMapper; + } + + @GetMapping + @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')") + public ResponseEntity getAll() { + List organizationDTOs = organisationService.toDTOs(organisationService.findAll()); + + return ResponseEntity + .ok(organizationDTOs); + } + + @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')") + public ResponseEntity getById(@PathVariable UUID id) { + try { + Organization organization = organisationService.findById(id).orElseThrow(EntityNotFoundException::new); + OrganizationDTO organizationDTO = organizationMapper.toDto(organization); + + return ResponseEntity + .ok(organizationDTO); + } catch (EntityNotFoundException e) { + return ResponseEntity + .notFound() + .build(); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')") + public ResponseEntity deleteById(@PathVariable UUID id) { + try { + Organization organization = organisationService.findById(id).orElseThrow(EntityNotFoundException::new); + + organisationService.delete(organization); + + return ResponseEntity + .ok() + .build(); + } catch (EntityNotFoundException e) { + return ResponseEntity + .notFound() + .build(); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } + } + + @PostMapping + @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_DEV')") + public ResponseEntity create(@RequestBody OrganizationDTO organizationDTO) { + try { + Organization newItem = organisationService.save(organizationDTO); + + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{id}") + .buildAndExpand(newItem.getId()) + .toUri(); + + return ResponseEntity + .created(location) + .body(newItem); + } catch (Exception e) { + return ResponseEntity + .internalServerError() + .build(); + } + } +} diff --git a/src/main/java/de/iwomm/propify_api/dto/BulkDeleteIdsDTO.java b/src/main/java/de/iwomm/propify_api/dto/BulkDeleteIdsDTO.java new file mode 100644 index 0000000..b8a7d48 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/dto/BulkDeleteIdsDTO.java @@ -0,0 +1,9 @@ +package de.iwomm.propify_api.dto; + +import java.util.List; +import java.util.UUID; + +public record BulkDeleteIdsDTO( + List ids +) { +} diff --git a/src/main/java/de/iwomm/propify_api/dto/IndustryDTO.java b/src/main/java/de/iwomm/propify_api/dto/IndustryDTO.java new file mode 100644 index 0000000..4c50660 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/dto/IndustryDTO.java @@ -0,0 +1,9 @@ +package de.iwomm.propify_api.dto; + +import java.util.UUID; + +public record IndustryDTO( + UUID id, + String name +) { +} diff --git a/src/main/java/de/iwomm/propify_api/dto/NewOrganizationDTO.java b/src/main/java/de/iwomm/propify_api/dto/NewOrganizationDTO.java new file mode 100644 index 0000000..202f673 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/dto/NewOrganizationDTO.java @@ -0,0 +1,10 @@ +package de.iwomm.propify_api.dto; + +import java.util.UUID; + +public record NewOrganizationDTO( + String name, + String owner, + UUID industryId +) { +} diff --git a/src/main/java/de/iwomm/propify_api/dto/OrganizationDTO.java b/src/main/java/de/iwomm/propify_api/dto/OrganizationDTO.java new file mode 100644 index 0000000..4db84ac --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/dto/OrganizationDTO.java @@ -0,0 +1,14 @@ +package de.iwomm.propify_api.dto; + +import de.iwomm.propify_api.entity.Industry; + +import java.util.Set; +import java.util.UUID; + +public record OrganizationDTO( + UUID id, + String name, + IndustryDTO industry, + String owner, + Set labels +) { } diff --git a/src/main/java/de/iwomm/propify_api/dto/OrganizationLabelDTO.java b/src/main/java/de/iwomm/propify_api/dto/OrganizationLabelDTO.java new file mode 100644 index 0000000..01f8894 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/dto/OrganizationLabelDTO.java @@ -0,0 +1,9 @@ +package de.iwomm.propify_api.dto; + +import java.util.UUID; + +public record OrganizationLabelDTO( + UUID id, + String name +) { +} diff --git a/src/main/java/de/iwomm/propify_api/entity/MultiTenantEntity.java b/src/main/java/de/iwomm/propify_api/entity/MultiTenantEntity.java new file mode 100644 index 0000000..e03d89c --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/entity/MultiTenantEntity.java @@ -0,0 +1,24 @@ +package de.iwomm.propify_api.entity; + + +import de.iwomm.propify_api.entitylistener.TenantEntityListener; +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +@EntityListeners(TenantEntityListener.class) +public abstract class MultiTenantEntity { + + @Column(name = "tenant_id") + private String tenantId; + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + +} diff --git a/src/main/java/de/iwomm/propify_api/entity/Organization.java b/src/main/java/de/iwomm/propify_api/entity/Organization.java index 6a1bf7a..e9cac0c 100644 --- a/src/main/java/de/iwomm/propify_api/entity/Organization.java +++ b/src/main/java/de/iwomm/propify_api/entity/Organization.java @@ -2,6 +2,8 @@ package de.iwomm.propify_api.entity; import jakarta.persistence.*; import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; import java.util.UUID; @Entity @@ -24,6 +26,15 @@ public class Organization { @Column(nullable = false) private LocalDateTime createdAt; + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable( + name = "organization_labels", + joinColumns = @JoinColumn(name = "organization_id"), + inverseJoinColumns = @JoinColumn(name = "label_id"), + foreignKey = @ForeignKey(name = "fk_organization_labels_organization", foreignKeyDefinition = "FOREIGN KEY (organization_id) REFERENCES organization(id) ON DELETE CASCADE") + ) + private Set labels = new HashSet<>(); + @PrePersist protected void onCreate() { this.createdAt = LocalDateTime.now(); @@ -68,4 +79,12 @@ public class Organization { public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public Set getLabels() { + return labels; + } + + public void setLabels(Set labels) { + this.labels = labels; + } } diff --git a/src/main/java/de/iwomm/propify_api/entity/OrganizationLabel.java b/src/main/java/de/iwomm/propify_api/entity/OrganizationLabel.java new file mode 100644 index 0000000..6b0ae24 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/entity/OrganizationLabel.java @@ -0,0 +1,31 @@ +package de.iwomm.propify_api.entity; + +import jakarta.persistence.*; +import java.util.UUID; + +@Entity +public class OrganizationLabel { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(nullable = false) + private UUID id; + + @Column(nullable = false) + private String name; + + 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; + } +} diff --git a/src/main/java/de/iwomm/propify_api/entity/Project.java b/src/main/java/de/iwomm/propify_api/entity/Project.java index 8743ddb..131e40c 100644 --- a/src/main/java/de/iwomm/propify_api/entity/Project.java +++ b/src/main/java/de/iwomm/propify_api/entity/Project.java @@ -2,6 +2,9 @@ package de.iwomm.propify_api.entity; import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; +import org.hibernate.annotations.Filter; +import org.hibernate.annotations.FilterDef; +import org.hibernate.annotations.ParamDef; import java.time.LocalDate; import java.time.LocalDateTime; @@ -10,7 +13,9 @@ import java.util.List; import java.util.UUID; @Entity -public class Project { +@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = String.class)) +@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId") +public class Project extends MultiTenantEntity { @Id @GeneratedValue(strategy = GenerationType.UUID) @Column(nullable = false) diff --git a/src/main/java/de/iwomm/propify_api/entitylistener/TenantEntityListener.java b/src/main/java/de/iwomm/propify_api/entitylistener/TenantEntityListener.java new file mode 100644 index 0000000..0d32bda --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/entitylistener/TenantEntityListener.java @@ -0,0 +1,18 @@ +package de.iwomm.propify_api.entitylistener; + +import de.iwomm.propify_api.entity.MultiTenantEntity; +import de.iwomm.propify_api.security.TenantContext; +import jakarta.persistence.PrePersist; + +public class TenantEntityListener { + + @PrePersist + public void setTenant(Object entity) { + if (entity instanceof MultiTenantEntity mte) { + String tenantId = TenantContext.getTenantId(); + if (tenantId != null && mte.getTenantId() == null) { + mte.setTenantId(tenantId); + } + } + } +} diff --git a/src/main/java/de/iwomm/propify_api/mapper/IndustryMapper.java b/src/main/java/de/iwomm/propify_api/mapper/IndustryMapper.java new file mode 100644 index 0000000..7b14027 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/mapper/IndustryMapper.java @@ -0,0 +1,11 @@ +package de.iwomm.propify_api.mapper; + +import de.iwomm.propify_api.dto.IndustryDTO; +import de.iwomm.propify_api.entity.Industry; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface IndustryMapper { + IndustryDTO toDto(Industry industry); + Industry fromDto(IndustryDTO industryDTO); +} diff --git a/src/main/java/de/iwomm/propify_api/mapper/OrganizationLabelMapper.java b/src/main/java/de/iwomm/propify_api/mapper/OrganizationLabelMapper.java new file mode 100644 index 0000000..febe026 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/mapper/OrganizationLabelMapper.java @@ -0,0 +1,11 @@ +package de.iwomm.propify_api.mapper; + +import de.iwomm.propify_api.dto.OrganizationLabelDTO; +import de.iwomm.propify_api.entity.OrganizationLabel; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface OrganizationLabelMapper { + OrganizationLabelDTO toDto(OrganizationLabel organizationLabel); + OrganizationLabel fromDto(OrganizationLabelDTO organizationLabelDTO); +} diff --git a/src/main/java/de/iwomm/propify_api/mapper/OrganizationMapper.java b/src/main/java/de/iwomm/propify_api/mapper/OrganizationMapper.java new file mode 100644 index 0000000..97716e8 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/mapper/OrganizationMapper.java @@ -0,0 +1,12 @@ +package de.iwomm.propify_api.mapper; + +import de.iwomm.propify_api.dto.NewOrganizationDTO; +import de.iwomm.propify_api.dto.OrganizationDTO; +import de.iwomm.propify_api.entity.Organization; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring", uses = { OrganizationLabelMapper.class, IndustryMapper.class }) +public interface OrganizationMapper { + OrganizationDTO toDto(Organization organization); + Organization fromDto(OrganizationDTO organizationDTO); +} diff --git a/src/main/java/de/iwomm/propify_api/repository/OrganizationLabelRepository.java b/src/main/java/de/iwomm/propify_api/repository/OrganizationLabelRepository.java new file mode 100644 index 0000000..ae2eb74 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/repository/OrganizationLabelRepository.java @@ -0,0 +1,9 @@ +package de.iwomm.propify_api.repository; + +import de.iwomm.propify_api.entity.OrganizationLabel; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface OrganizationLabelRepository extends JpaRepository { +} diff --git a/src/main/java/de/iwomm/propify_api/security/TenantContext.java b/src/main/java/de/iwomm/propify_api/security/TenantContext.java new file mode 100644 index 0000000..a470407 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/security/TenantContext.java @@ -0,0 +1,17 @@ +package de.iwomm.propify_api.security; + +public class TenantContext { + private static final ThreadLocal CURRENT_TENANT = new ThreadLocal<>(); + + public static void setTenantId(String tenantId) { + CURRENT_TENANT.set(tenantId); + } + + public static String getTenantId() { + return CURRENT_TENANT.get(); + } + + public static void clear() { + CURRENT_TENANT.remove(); + } +} diff --git a/src/main/java/de/iwomm/propify_api/security/TenantFilter.java b/src/main/java/de/iwomm/propify_api/security/TenantFilter.java new file mode 100644 index 0000000..41f0e20 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/security/TenantFilter.java @@ -0,0 +1,75 @@ +package de.iwomm.propify_api.security; + +import jakarta.persistence.EntityManager; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.hibernate.Session; +import org.springframework.core.annotation.Order; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Component +@Order(1) +public class TenantFilter extends OncePerRequestFilter { + + private final EntityManager entityManager; + + public TenantFilter(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + String tenantId = null; + + // 1️⃣ Tenant-ID aus JWT lesen + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth instanceof JwtAuthenticationToken jwtAuth) { + Jwt jwt = jwtAuth.getToken(); + + // tenant_id Claim + tenantId = jwt.getClaimAsString("tenant_id"); + + // Rollen prüfen, Superadmin darf alles sehen + List roles = jwt.getClaimAsStringList("roles"); + if (roles != null && roles.contains("ROLE_SUPERADMIN")) { + tenantId = null; // kein Filter für Superadmin + } + } + + // 2️⃣ Fallback: Header (optional, falls JWT fehlt) + if (tenantId == null) { + tenantId = request.getHeader("X-Tenant-ID"); + } + + // 3️⃣ TenantContext für diesen Thread setzen + TenantContext.setTenantId(tenantId); + + // 4️⃣ Hibernate Filter aktivieren (falls Tenant-ID vorhanden) + if (tenantId != null) { + Session session = entityManager.unwrap(Session.class); + session.enableFilter("tenantFilter") + .setParameter("tenantId", tenantId); + } + + try { + filterChain.doFilter(request, response); + } finally { + // 5️⃣ Aufräumen nach Request + TenantContext.clear(); + } + } +} diff --git a/src/main/java/de/iwomm/propify_api/service/IndustryService.java b/src/main/java/de/iwomm/propify_api/service/IndustryService.java new file mode 100644 index 0000000..bfb7ddd --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/service/IndustryService.java @@ -0,0 +1,69 @@ +package de.iwomm.propify_api.service; + +import de.iwomm.propify_api.dto.*; +import de.iwomm.propify_api.entity.Industry; +import de.iwomm.propify_api.entity.Organization; +import de.iwomm.propify_api.entity.Project; +import de.iwomm.propify_api.entity.Property; +import de.iwomm.propify_api.mapper.IndustryMapper; +import de.iwomm.propify_api.repository.IndustryRepository; +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; + +@Service +public class IndustryService { + private IndustryRepository industryRepository; + private IndustryMapper industryMapper; + + + public IndustryService(IndustryRepository industryRepository, IndustryMapper industryMapper) { + this.industryRepository = industryRepository; + this.industryMapper = industryMapper; + } + + public List findAll() { + return industryRepository.findAll(); + } + + public Optional findById(UUID id) { + return industryRepository.findById(id); + } + + public Industry save(IndustryDTO dto) { + return industryRepository.save(industryMapper.fromDto(dto)); + } + + public void deleteByIds(BulkDeleteIdsDTO bulkDeleteIdsDTO) { + industryRepository.deleteAllById(bulkDeleteIdsDTO.ids()); + } + + public List toDTOs(List industries) { + List dtos = new ArrayList<>(); + + industries.forEach(industry -> { + dtos.add(this.industryMapper.toDto(industry)); + }); + + return dtos; + } + + public Industry update(UUID id, IndustryDTO industryDTO) { + Industry updated = industryRepository.findById(id).orElseThrow(EntityNotFoundException::new); + + updated.setName(industryDTO.name()); + + industryRepository.save(updated); + + return updated; + } + + public void deleteById(UUID id) { + Industry industry = industryRepository.findById(id).orElseThrow(EntityNotFoundException::new); + industryRepository.delete(industry); + } +} diff --git a/src/main/java/de/iwomm/propify_api/service/OrganisationService.java b/src/main/java/de/iwomm/propify_api/service/OrganisationService.java new file mode 100644 index 0000000..b48b72d --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/service/OrganisationService.java @@ -0,0 +1,59 @@ +package de.iwomm.propify_api.service; + +import de.iwomm.propify_api.dto.NewOrganizationDTO; +import de.iwomm.propify_api.dto.OrganizationDTO; +import de.iwomm.propify_api.dto.ProjectDTO; +import de.iwomm.propify_api.entity.Organization; +import de.iwomm.propify_api.entity.Project; +import de.iwomm.propify_api.mapper.OrganizationMapper; +import de.iwomm.propify_api.repository.OrganizationRepository; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +public class OrganisationService { + private final OrganizationRepository organizationRepository; + private final OrganizationMapper organizationMapper; + + public OrganisationService( + OrganizationRepository organizationRepository, + OrganizationMapper organizationMapper + ) { + this.organizationRepository = organizationRepository; + this.organizationMapper = organizationMapper; + } + + public List findAll() { + return organizationRepository.findAll(); + } + + public Optional findById(UUID id) { + return organizationRepository.findById(id); + } + + public void deleteBbyId(UUID id) { + organizationRepository.deleteById(id); + } + + public void delete(Organization organization) { + organizationRepository.delete(organization); + } + + public List toDTOs(List organizations) { + List dtos = new ArrayList<>(); + + organizations.forEach(organization -> { + dtos.add(this.organizationMapper.toDto(organization)); + }); + + return dtos; + } + + public Organization save(OrganizationDTO dto) { + return organizationRepository.save(organizationMapper.fromDto(dto)); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b915283..9c67aa5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -39,6 +39,7 @@ spring: resourceserver: jwt: issuer-uri: ${KEYCLOAK_ISSUER_URI:https://kc.dev.localhost/realms/skamp} + #issuer-uri: ${KEYCLOAK_ISSUER_URI:http://localhost:8888/realms/skamp} jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs logging: