diff --git a/server/pom.xml b/server/pom.xml index df00ce6..9421e79 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -26,6 +26,13 @@ spring-boot-starter-web + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + + org.postgresql postgresql diff --git a/server/src/main/java/com/example/relationaldataaccess/Customer.java b/server/src/main/java/com/example/relationaldataaccess/Customer.java index 2c73e13..3e035c8 100644 --- a/server/src/main/java/com/example/relationaldataaccess/Customer.java +++ b/server/src/main/java/com/example/relationaldataaccess/Customer.java @@ -1,8 +1,17 @@ package com.example.relationaldataaccess; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Customer entity representing a customer in the system") public class Customer { + @Schema(description = "Unique identifier for the customer", example = "1", accessMode = Schema.AccessMode.READ_ONLY) private long id; - private String firstName, lastName; + + @Schema(description = "Customer's first name", example = "John", maxLength = 50, requiredMode = Schema.RequiredMode.REQUIRED) + private String firstName; + + @Schema(description = "Customer's last name", example = "Doe", maxLength = 50, requiredMode = Schema.RequiredMode.REQUIRED) + private String lastName; public Customer(long id, String firstName, String lastName) { this.id = id; diff --git a/server/src/main/java/com/example/relationaldataaccess/config/OpenApiConfig.java b/server/src/main/java/com/example/relationaldataaccess/config/OpenApiConfig.java new file mode 100644 index 0000000..5cd1ccd --- /dev/null +++ b/server/src/main/java/com/example/relationaldataaccess/config/OpenApiConfig.java @@ -0,0 +1,70 @@ +package com.example.relationaldataaccess.config; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.servers.Server; + +@Configuration +public class OpenApiConfig { + + @Value("${cors.allowed-origins:http://localhost:5173}") + private String allowedOrigins; + + @Bean + public OpenAPI customerManagementOpenAPI() { + // Parse the first allowed origin for the server URL + String serverUrl = allowedOrigins.split(",")[0].trim(); + + // For local development, use backend URL + if (serverUrl.contains("localhost:5173") || serverUrl.contains("localhost:5174")) { + serverUrl = "http://localhost:8080"; + } + + Server server = new Server() + .url(serverUrl) + .description("Customer Management API Server"); + + Contact contact = new Contact() + .name("Customer Management Team") + .email("support@customerapp.com") + .url("https://github.com/JohanCodeForFun/customer-management-aws-demo"); + + License license = new License() + .name("MIT License") + .url("https://opensource.org/licenses/MIT"); + + Info info = new Info() + .title("Customer Management API") + .description(""" + A comprehensive REST API for managing customer data with full CRUD operations. + + ## Features + - **Customer CRUD Operations**: Create, Read, Update, Delete customers + - **Search Functionality**: Search customers by name (case-insensitive) + - **Health Monitoring**: System health and connectivity checks + - **Input Validation**: Automatic sanitization and validation + - **CORS Support**: Cross-origin resource sharing enabled + + ## Authentication + Currently no authentication required. In production, implement proper authentication. + + ## Error Handling + All endpoints return appropriate HTTP status codes and error messages. + """) + .version("1.0.0") + .contact(contact) + .license(license); + + return new OpenAPI() + .info(info) + .servers(List.of(server)); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/example/relationaldataaccess/controller/CustomerController.java b/server/src/main/java/com/example/relationaldataaccess/controller/CustomerController.java index 2041f49..fd9cc66 100644 --- a/server/src/main/java/com/example/relationaldataaccess/controller/CustomerController.java +++ b/server/src/main/java/com/example/relationaldataaccess/controller/CustomerController.java @@ -1,21 +1,58 @@ package com.example.relationaldataaccess.controller; -import com.example.relationaldataaccess.Customer; +import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; -import java.util.List; +import com.example.relationaldataaccess.Customer; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; @RestController @RequestMapping("/api/customers") @CrossOrigin(origins = "${cors.allowed-origins:http://localhost:5173,http://localhost:5174}") +@Tag(name = "Customer Management", description = "REST API for managing customer data with full CRUD operations and search functionality") public class CustomerController { @Autowired private JdbcTemplate jdbcTemplate; + @Operation( + summary = "Get all customers", + description = "Retrieve a complete list of all customers in the system, ordered by ID" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Successfully retrieved customers", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Customer.class), + examples = @ExampleObject( + name = "Customer list example", + value = "[{\"id\":1,\"firstName\":\"John\",\"lastName\":\"Doe\"},{\"id\":2,\"firstName\":\"Jane\",\"lastName\":\"Smith\"}]" + ) + ) + ) + }) @GetMapping public List getAllCustomers() { return jdbcTemplate.query( @@ -28,8 +65,32 @@ public List getAllCustomers() { ); } + @Operation( + summary = "Get customer by ID", + description = "Retrieve a specific customer by their unique identifier" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Customer found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Customer.class), + examples = @ExampleObject( + name = "Customer example", + value = "{\"id\":1,\"firstName\":\"John\",\"lastName\":\"Doe\"}" + ) + ) + ), + @ApiResponse( + responseCode = "404", + description = "Customer not found with the specified ID" + ) + }) @GetMapping("/{id}") - public ResponseEntity getCustomerById(@PathVariable Long id) { + public ResponseEntity getCustomerById( + @Parameter(description = "Unique identifier of the customer", required = true, example = "1") + @PathVariable Long id) { List customers = jdbcTemplate.query( "SELECT id, first_name, last_name FROM customers WHERE id = ?", (rs, rowNum) -> new Customer( @@ -47,8 +108,44 @@ public ResponseEntity getCustomerById(@PathVariable Long id) { return ResponseEntity.ok(customers.get(0)); } + @Operation( + summary = "Create a new customer", + description = "Create a new customer with the provided first name and last name. Names are automatically sanitized and validated for security." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Customer successfully created", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Customer.class), + examples = @ExampleObject( + name = "Created customer example", + value = "{\"id\":3,\"firstName\":\"John\",\"lastName\":\"Doe\"}" + ) + ) + ), + @ApiResponse( + responseCode = "400", + description = "Invalid customer data provided (empty names, null data, etc.)" + ) + }) @PostMapping - public Customer createCustomer(@RequestBody Customer customer) { + public Customer createCustomer( + @Parameter(description = "Customer data with firstName and lastName", required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Customer object with first name and last name", + required = true, + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Customer.class), + examples = @ExampleObject( + name = "New customer example", + value = "{\"firstName\":\"John\",\"lastName\":\"Doe\"}" + ) + ) + ) + @RequestBody Customer customer) { // Input validation and sanitization if (customer == null) { throw new IllegalArgumentException("Customer data cannot be null"); @@ -99,7 +196,28 @@ public Customer createCustomer(@RequestBody Customer customer) { } @DeleteMapping("/{id}") - public ResponseEntity deleteCustomer(@PathVariable Long id) { + @Operation( + summary = "Delete a customer", + description = "Deletes a customer by their unique ID. Returns 200 OK if the customer was successfully deleted, or 404 Not Found if no customer exists with the specified ID." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Customer successfully deleted" + ), + @ApiResponse( + responseCode = "404", + description = "Customer not found with the specified ID", + content = @Content + ) + }) + public ResponseEntity deleteCustomer( + @Parameter( + description = "The unique identifier of the customer to delete", + required = true, + example = "1" + ) + @PathVariable Long id) { int rowsAffected = jdbcTemplate.update("DELETE FROM customers WHERE id = ?", id); if (rowsAffected > 0) { diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index f61aaf6..7e865c5 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -14,4 +14,12 @@ server.port=${PORT:8080} logging.level.com.example.relationaldataaccess=${LOG_LEVEL:DEBUG} # CORS Configuration -cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:5174} \ No newline at end of file +cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:5174} + +# OpenAPI/Swagger Configuration +springdoc.api-docs.path=/api/docs +springdoc.swagger-ui.path=/api/swagger-ui +springdoc.swagger-ui.operationsSorter=method +springdoc.swagger-ui.tagsSorter=alpha +springdoc.swagger-ui.tryItOutEnabled=true +springdoc.swagger-ui.filter=true \ No newline at end of file