diff --git a/README.md b/README.md
index 1ddad2f4..e4bd4f02 100644
--- a/README.md
+++ b/README.md
@@ -51,13 +51,20 @@ If you have at least Docker, the Java JDK and Gradle installed in appropriate ve
# the following command should return a JSON array with just all packages visible for the admin of the customer aab:
curl \
- -H 'current-user: mike@hostsharing.net' \
- -H 'assumed-roles: customer#aab.admin' \
+ -H 'current-user: mike@hostsharing.net' -H 'assumed-roles: customer#aab.admin' \
http://localhost:8080/api/packages
-The latter `curl` command actually goes through the database server.
+ # add a new customer
+ curl \
+ -H 'current-user: mike@hostsharing.net' -H "Content-Type: application/json" \
+ -d '{ "prefix":"baa", "reference":80001, "adminUserName":"admin@baa.example.com" }' \
+ -X POST http://localhost:8080/api/customers
-ⓘ
+**ⓘ**
+'mike@hostsharing.net' and 'sven@hostsharing.net' are Hostsharing hostmaster accounts coming from the example data which is automatically inserted in Testcontainers and Development environments.
+Also try for example 'admin@aaa.example.com' or 'unknown@example.org'.
+
+**ⓘ**
If you want a formatted JSON output, you can pipe the result to `jq` or similar.
If you still need to install some of these tools, find some hints in the next chapters.
@@ -245,3 +252,26 @@ If the persistent database and the temporary database show different results, on
e.g. from a previous run of tests or manually applied.
It's best to run `pg-sql-reset && gw bootRun` before each test run, to have a clean database.
+## How to Amend Liquibase SQL Changesets?
+
+Liquibase changesets are meant to be immutable and based on each other.
+That means, once a changeset is written, it never changes, not even a whitespace or comment.
+Liquibase is a *database migration tool*, not a *database initialization tool*.
+
+This, if you need to add change a table, stored procedure or whatever,
+create a new changeset and apply `ALTER`, `DROP`, `CREATE OR REPLACE` or whatever SQL commands to perform your changes.
+These changes will be automatically applied once the application starts up again.
+This way, any staging or production database will always match the application code.
+
+But, during initial development that can be a big hassle because the database structure changes a lot in that stage.
+Also, the actual structure of the database won't be easily recognized anymore through lots of migration changesets.
+
+Therefore, during initial development, it's good approach just to amend the existing changesets and delete the database:
+
+```shell
+pg-sql-reset
+gw bootRun
+```
+
+**⚠**
+Just don't forget switching to the migration mode, once there is a production database!
diff --git a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java
new file mode 100644
index 00000000..64ab4095
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java
@@ -0,0 +1,57 @@
+package net.hostsharing.hsadminng.errors;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Getter;
+import org.springframework.core.NestedExceptionUtils;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.orm.jpa.JpaSystemException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+import java.time.LocalDateTime;
+
+@ControllerAdvice
+public class RestResponseEntityExceptionHandler
+ extends ResponseEntityExceptionHandler {
+
+ @ExceptionHandler(DataIntegrityViolationException.class)
+ protected ResponseEntity handleConflict(
+ final RuntimeException exc, final WebRequest request) {
+
+ return new ResponseEntity<>(
+ new CustomErrorResponse(exc, HttpStatus.CONFLICT), HttpStatus.CONFLICT);
+ }
+
+ @ExceptionHandler(JpaSystemException.class)
+ protected ResponseEntity handleJpaExceptions(
+ final RuntimeException exc, final WebRequest request) {
+
+ return new ResponseEntity<>(
+ new CustomErrorResponse(exc, HttpStatus.FORBIDDEN), HttpStatus.FORBIDDEN);
+ }
+}
+
+@Getter
+class CustomErrorResponse {
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
+ private final LocalDateTime timestamp;
+
+ private final HttpStatus status;
+
+ private final String message;
+
+ public CustomErrorResponse(final RuntimeException exc, final HttpStatus status) {
+ this.timestamp = LocalDateTime.now();
+ this.status = status;
+ this.message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage());
+ }
+
+ private String firstLine(final String message) {
+ return message.split("\\r|\\n|\\r\\n", 0)[0];
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerController.java b/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerController.java
index ffed4130..27aaf23a 100644
--- a/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerController.java
@@ -2,16 +2,14 @@ package net.hostsharing.hsadminng.hscustomer;
import net.hostsharing.hsadminng.context.Context;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RequestHeader;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.*;
import javax.transaction.Transactional;
import java.util.List;
+import java.util.UUID;
+
+@RestController
-@Controller
public class CustomerController {
@Autowired
@@ -20,18 +18,35 @@ public class CustomerController {
@Autowired
private CustomerRepository customerRepository;
- @ResponseBody
- @RequestMapping(value = "/api/customers", method = RequestMethod.GET)
+ @GetMapping(value = "/api/customers")
@Transactional
public List listCustomers(
- @RequestHeader(value = "current-user") String userName,
- @RequestHeader(value="assumed-roles", required=false) String assumedRoles
+ @RequestHeader(value = "current-user") String userName,
+ @RequestHeader(value = "assumed-roles", required = false) String assumedRoles
) {
context.setCurrentUser(userName);
- if ( assumedRoles != null && !assumedRoles.isBlank() ) {
+ if (assumedRoles != null && !assumedRoles.isBlank()) {
context.assumeRoles(assumedRoles);
}
return customerRepository.findAll();
}
+ @PostMapping(value = "/api/customers")
+ @ResponseStatus
+ @Transactional
+ public CustomerEntity addCustomer(
+ @RequestHeader(value = "current-user") String userName,
+ @RequestHeader(value = "assumed-roles", required = false) String assumedRoles,
+ @RequestBody CustomerEntity customer
+ ) {
+ context.setCurrentUser(userName);
+ if (assumedRoles != null && !assumedRoles.isBlank()) {
+ context.assumeRoles(assumedRoles);
+ }
+ if (customer.getUuid() == null) {
+ customer.setUuid(UUID.randomUUID());
+ }
+ return customerRepository.save(customer);
+ }
+
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerEntity.java b/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerEntity.java
index 8a5f99b3..ccc97488 100644
--- a/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hscustomer/CustomerEntity.java
@@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hscustomer;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
+import lombok.Setter;
import javax.persistence.*;
import java.util.UUID;
@@ -10,6 +11,7 @@ import java.util.UUID;
@Entity
@Table(name = "customer_rv")
@Getter
+@Setter
@NoArgsConstructor
@AllArgsConstructor
public class CustomerEntity {
diff --git a/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql b/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql
index c7baea62..8524edab 100644
--- a/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql
+++ b/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql
@@ -210,7 +210,7 @@ create or replace function addCustomerNotAllowedForCurrentSubjects()
language PLPGSQL
as $$
begin
- raise exception 'add-customer not permitted for %', array_to_string(currentSubjects());
+ raise exception 'add-customer not permitted for %', array_to_string(currentSubjects(), ';', 'null');
end; $$;
/**