Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- SpringDoc OpenAPI (Swagger) for API documentation -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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<Customer> getAllCustomers() {
return jdbcTemplate.query(
Expand All @@ -28,8 +65,32 @@ public List<Customer> 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<Customer> getCustomerById(@PathVariable Long id) {
public ResponseEntity<Customer> getCustomerById(
@Parameter(description = "Unique identifier of the customer", required = true, example = "1")
@PathVariable Long id) {
List<Customer> customers = jdbcTemplate.query(
"SELECT id, first_name, last_name FROM customers WHERE id = ?",
(rs, rowNum) -> new Customer(
Expand All @@ -47,8 +108,44 @@ public ResponseEntity<Customer> 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");
Expand Down Expand Up @@ -99,7 +196,28 @@ public Customer createCustomer(@RequestBody Customer customer) {
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> 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<Void> 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) {
Expand Down
10 changes: 9 additions & 1 deletion server/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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}
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