diff --git a/backend/pom.xml b/backend/pom.xml index 3594ce4..5bcbe95 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -120,6 +120,11 @@ ${jsonwebtoken.version} runtime + + software.amazon.awssdk + s3 + 2.20.26 + diff --git a/backend/src/main/java/com/amigoscode/Main.java b/backend/src/main/java/com/amigoscode/Main.java index 9efcc7b..7c9a671 100644 --- a/backend/src/main/java/com/amigoscode/Main.java +++ b/backend/src/main/java/com/amigoscode/Main.java @@ -3,6 +3,8 @@ import com.amigoscode.customer.Customer; import com.amigoscode.customer.CustomerRepository; import com.amigoscode.customer.Gender; +import com.amigoscode.s3.S3Buckets; +import com.amigoscode.s3.S3Service; import com.github.javafaker.Faker; import com.github.javafaker.Name; import org.springframework.boot.CommandLineRunner; @@ -26,23 +28,41 @@ CommandLineRunner runner( CustomerRepository customerRepository, PasswordEncoder passwordEncoder) { return args -> { - var faker = new Faker(); - Random random = new Random(); - Name name = faker.name(); - String firstName = name.firstName(); - String lastName = name.lastName(); - int age = random.nextInt(16, 99); - Gender gender = age % 2 == 0 ? Gender.MALE : Gender.FEMALE; - String email = firstName.toLowerCase() + "." + lastName.toLowerCase() + "@amigoscode.com"; - Customer customer = new Customer( - firstName + " " + lastName, - email, - passwordEncoder.encode("password"), - age, - gender); - customerRepository.save(customer); - System.out.println(email); + createRandomCustomer(customerRepository, passwordEncoder); +// testBucketUploadAndDownload(s3Service, s3Buckets); }; } + private static void testBucketUploadAndDownload(S3Service s3Service, S3Buckets s3Buckets) { + s3Service.putObject( + s3Buckets.getCustomer(), + "foo", + "Hello World!".getBytes() + ); + byte[] obj = s3Service.getObject( + s3Buckets.getCustomer(), + "foo" + ); + System.out.println("Hooray " + new String(obj)); + } + + private static void createRandomCustomer(CustomerRepository customerRepository, PasswordEncoder passwordEncoder) { + var faker = new Faker(); + Random random = new Random(); + Name name = faker.name(); + String firstName = name.firstName(); + String lastName = name.lastName(); + int age = random.nextInt(16, 99); + Gender gender = age % 2 == 0 ? Gender.MALE : Gender.FEMALE; + String email = firstName.toLowerCase() + "." + lastName.toLowerCase() + "@amigoscode.com"; + Customer customer = new Customer( + firstName + " " + lastName, + email, + passwordEncoder.encode("password"), + age, + gender); + customerRepository.save(customer); + System.out.println(email); + } + } diff --git a/backend/src/main/java/com/amigoscode/customer/Customer.java b/backend/src/main/java/com/amigoscode/customer/Customer.java index 05eccf8..e3e257a 100644 --- a/backend/src/main/java/com/amigoscode/customer/Customer.java +++ b/backend/src/main/java/com/amigoscode/customer/Customer.java @@ -17,6 +17,10 @@ @UniqueConstraint( name = "customer_email_unique", columnNames = "email" + ), + @UniqueConstraint( + name = "profile_image_id_unique", + columnNames = "profileImageId" ) } ) @@ -57,9 +61,25 @@ public class Customer implements UserDetails { ) private String password; + @Column( + unique = true + ) + private String profileImageId; + public Customer() { } + public Customer(Integer id, + String name, + String email, + String password, + Integer age, + Gender gender, + String profileImageId) { + this(id, name, email, password, age, gender); + this.profileImageId = profileImageId; + } + public Customer(Integer id, String name, String email, @@ -126,28 +146,12 @@ public void setGender(Gender gender) { this.gender = gender; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Customer customer = (Customer) o; - return Objects.equals(id, customer.id) && Objects.equals(name, customer.name) && Objects.equals(email, customer.email) && Objects.equals(age, customer.age) && gender == customer.gender; + public String getProfileImageId() { + return profileImageId; } - @Override - public int hashCode() { - return Objects.hash(id, name, email, age, gender); - } - - @Override - public String toString() { - return "Customer{" + - "id=" + id + - ", name='" + name + '\'' + - ", email='" + email + '\'' + - ", age=" + age + - ", gender=" + gender + - '}'; + public void setProfileImageId(String profileImageId) { + this.profileImageId = profileImageId; } @Override @@ -184,4 +188,30 @@ public boolean isCredentialsNonExpired() { public boolean isEnabled() { return true; } + + @Override + public boolean equals(Object o) { + if(this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Customer customer = (Customer) o; + return Objects.equals(id, customer.id) && Objects.equals(name, customer.name) && Objects.equals(email, customer.email) && Objects.equals(age, customer.age) && gender == customer.gender && Objects.equals(password, customer.password) && Objects.equals(profileImageId, customer.profileImageId); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, email, age, gender, password, profileImageId); + } + + @Override + public String toString() { + return "Customer{" + + "id=" + id + + ", name='" + name + '\'' + + ", email='" + email + '\'' + + ", age=" + age + + ", gender=" + gender + + ", password='" + password + '\'' + + ", profileImageId='" + profileImageId + '\'' + + '}'; + } } diff --git a/backend/src/main/java/com/amigoscode/customer/CustomerController.java b/backend/src/main/java/com/amigoscode/customer/CustomerController.java index a5abe1d..320347a 100644 --- a/backend/src/main/java/com/amigoscode/customer/CustomerController.java +++ b/backend/src/main/java/com/amigoscode/customer/CustomerController.java @@ -2,8 +2,10 @@ import com.amigoscode.jwt.JWTUtil; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -54,4 +56,20 @@ public void updateCustomer( customerService.updateCustomer(customerId, updateRequest); } + @PostMapping ( + value = "{customerId}/profile-image", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE + ) + public void uploadCustomerProfileImage( + @PathVariable("customerId") Integer customerId, + @RequestParam("file") MultipartFile file) { + customerService.uploadCustomerProfileImage(customerId, file); + } + + @GetMapping (value = "{customerId}/profile-image") + public byte[] getCustomerProfileImage( + @PathVariable("customerId") Integer customerId) { + return customerService.getCustomerProfileImage(customerId); + } + } diff --git a/backend/src/main/java/com/amigoscode/customer/CustomerDTO.java b/backend/src/main/java/com/amigoscode/customer/CustomerDTO.java index 21b0a2a..fe7441a 100644 --- a/backend/src/main/java/com/amigoscode/customer/CustomerDTO.java +++ b/backend/src/main/java/com/amigoscode/customer/CustomerDTO.java @@ -9,7 +9,8 @@ public record CustomerDTO ( Gender gender, Integer age, List roles, - String username + String username, + String profileImageId ){ } diff --git a/backend/src/main/java/com/amigoscode/customer/CustomerDTOMapper.java b/backend/src/main/java/com/amigoscode/customer/CustomerDTOMapper.java index b01d52d..9e122f8 100644 --- a/backend/src/main/java/com/amigoscode/customer/CustomerDTOMapper.java +++ b/backend/src/main/java/com/amigoscode/customer/CustomerDTOMapper.java @@ -20,7 +20,8 @@ public CustomerDTO apply(Customer customer) { .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()), - customer.getUsername() + customer.getUsername(), + customer.getProfileImageId() ); } } diff --git a/backend/src/main/java/com/amigoscode/customer/CustomerDao.java b/backend/src/main/java/com/amigoscode/customer/CustomerDao.java index a0bda72..fdd7836 100644 --- a/backend/src/main/java/com/amigoscode/customer/CustomerDao.java +++ b/backend/src/main/java/com/amigoscode/customer/CustomerDao.java @@ -5,11 +5,12 @@ public interface CustomerDao { List selectAllCustomers(); - Optional selectCustomerById(Integer id); + Optional selectCustomerById(Integer customerId); void insertCustomer(Customer customer); boolean existsCustomerWithEmail(String email); - boolean existsCustomerById(Integer id); + boolean existsCustomerById(Integer customerId); void deleteCustomerById(Integer customerId); void updateCustomer(Customer update); Optional selectUserByEmail(String email); + void updateCustomerProfileImageId(String profileImageId, Integer customerId); } diff --git a/backend/src/main/java/com/amigoscode/customer/CustomerJDBCDataAccessService.java b/backend/src/main/java/com/amigoscode/customer/CustomerJDBCDataAccessService.java index 3dffcd3..58f5fd1 100644 --- a/backend/src/main/java/com/amigoscode/customer/CustomerJDBCDataAccessService.java +++ b/backend/src/main/java/com/amigoscode/customer/CustomerJDBCDataAccessService.java @@ -21,7 +21,7 @@ public CustomerJDBCDataAccessService(JdbcTemplate jdbcTemplate, @Override public List selectAllCustomers() { var sql = """ - SELECT id, name, email, password, age, gender + SELECT id, name, email, password, age, gender, profile_image_id FROM customer LIMIT 1000 """; @@ -32,7 +32,7 @@ public List selectAllCustomers() { @Override public Optional selectCustomerById(Integer id) { var sql = """ - SELECT id, name, email, password, age, gender + SELECT id, name, email, password, age, gender, profile_image_id FROM customer WHERE id = ? """; @@ -125,7 +125,7 @@ public void updateCustomer(Customer update) { @Override public Optional selectUserByEmail(String email) { var sql = """ - SELECT id, name, email, password, age, gender + SELECT id, name, email, password, age, gender, profile_image_id FROM customer WHERE email = ? """; @@ -133,4 +133,13 @@ public Optional selectUserByEmail(String email) { .stream() .findFirst(); } + + @Override + public void updateCustomerProfileImageId(String profileImageId, Integer customerId) { + var sql = """ + UPDATE customer + SET profile_image_id = ? + """; + jdbcTemplate.update(sql, profileImageId, customerId); + } } diff --git a/backend/src/main/java/com/amigoscode/customer/CustomerJPADataAccessService.java b/backend/src/main/java/com/amigoscode/customer/CustomerJPADataAccessService.java index 7c08e70..80b1c08 100644 --- a/backend/src/main/java/com/amigoscode/customer/CustomerJPADataAccessService.java +++ b/backend/src/main/java/com/amigoscode/customer/CustomerJPADataAccessService.java @@ -57,4 +57,9 @@ public Optional selectUserByEmail(String email) { return customerRepository.findCustomerByEmail(email); } + @Override + public void updateCustomerProfileImageId(String profileImageId, Integer customerId) { + customerRepository.updateProfileImageId(profileImageId, customerId); + } + } diff --git a/backend/src/main/java/com/amigoscode/customer/CustomerListDataAccessService.java b/backend/src/main/java/com/amigoscode/customer/CustomerListDataAccessService.java index cbc138d..72c4060 100644 --- a/backend/src/main/java/com/amigoscode/customer/CustomerListDataAccessService.java +++ b/backend/src/main/java/com/amigoscode/customer/CustomerListDataAccessService.java @@ -83,4 +83,9 @@ public Optional selectUserByEmail(String email) { .findFirst(); } + @Override + public void updateCustomerProfileImageId(String profileImageId, Integer customerId) { + //TODO Implement this + } + } diff --git a/backend/src/main/java/com/amigoscode/customer/CustomerRepository.java b/backend/src/main/java/com/amigoscode/customer/CustomerRepository.java index 41e54be..9b28eff 100644 --- a/backend/src/main/java/com/amigoscode/customer/CustomerRepository.java +++ b/backend/src/main/java/com/amigoscode/customer/CustomerRepository.java @@ -1,6 +1,9 @@ package com.amigoscode.customer; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import software.amazon.awssdk.services.s3.endpoints.internal.Value; import java.util.Optional; @@ -10,4 +13,7 @@ public interface CustomerRepository boolean existsCustomerByEmail(String email); boolean existsCustomerById(Integer id); Optional findCustomerByEmail(String email); + @Modifying + @Query("UPDATE Customer c SET c.profileImageId = ?1 WHERE c.id = ?2") + int updateProfileImageId(String profileImageId, Integer customerId); } diff --git a/backend/src/main/java/com/amigoscode/customer/CustomerRowMapper.java b/backend/src/main/java/com/amigoscode/customer/CustomerRowMapper.java index c004b54..03ea033 100644 --- a/backend/src/main/java/com/amigoscode/customer/CustomerRowMapper.java +++ b/backend/src/main/java/com/amigoscode/customer/CustomerRowMapper.java @@ -16,6 +16,7 @@ public Customer mapRow(ResultSet rs, int rowNum) throws SQLException { rs.getString("email"), rs.getString("password"), rs.getInt("age"), - Gender.valueOf(rs.getString("gender"))); + Gender.valueOf(rs.getString("gender")), + rs.getString("profile_image_id")); } } diff --git a/backend/src/main/java/com/amigoscode/customer/CustomerService.java b/backend/src/main/java/com/amigoscode/customer/CustomerService.java index 05ecfa2..210a23f 100644 --- a/backend/src/main/java/com/amigoscode/customer/CustomerService.java +++ b/backend/src/main/java/com/amigoscode/customer/CustomerService.java @@ -3,11 +3,16 @@ import com.amigoscode.exception.DuplicateResourceException; import com.amigoscode.exception.RequestValidationException; import com.amigoscode.exception.ResourceNotFoundException; +import com.amigoscode.s3.S3Buckets; +import com.amigoscode.s3.S3Service; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; @Service @@ -16,13 +21,17 @@ public class CustomerService { private final CustomerDao customerDao; private final CustomerDTOMapper customerDTOMapper; private final PasswordEncoder passwordEncoder; + private final S3Service s3service; + private final S3Buckets s3Buckets; public CustomerService(@Qualifier("jdbc") CustomerDao customerDao, CustomerDTOMapper customerDTOMapper, - PasswordEncoder passwordEncoder) { + PasswordEncoder passwordEncoder, S3Service s3service, S3Buckets s3Buckets) { this.customerDao = customerDao; this.customerDTOMapper = customerDTOMapper; this.passwordEncoder = passwordEncoder; + this.s3service = s3service; + this.s3Buckets = s3Buckets; } public List getAllCustomers() { @@ -61,13 +70,17 @@ public void addCustomer(CustomerRegistrationRequest customerRegistrationRequest) } public void deleteCustomerById(Integer customerId) { + checkIfCustomerExistsOrThrow(customerId); + + customerDao.deleteCustomerById(customerId); + } + + private void checkIfCustomerExistsOrThrow(Integer customerId) { if (!customerDao.existsCustomerById(customerId)) { throw new ResourceNotFoundException( "customer with id [%s] not found".formatted(customerId) ); } - - customerDao.deleteCustomerById(customerId); } public void updateCustomer(Integer customerId, @@ -106,5 +119,38 @@ public void updateCustomer(Integer customerId, customerDao.updateCustomer(customer); } + + public void uploadCustomerProfileImage(Integer customerId, MultipartFile file) { + checkIfCustomerExistsOrThrow(customerId); + String profileImageId = UUID.randomUUID().toString(); + try { + s3service.putObject( + s3Buckets.getCustomer(), + "profile-images/%s/%s".formatted(customerId, profileImageId), + file.getBytes() + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + customerDao.updateCustomerProfileImageId(profileImageId, customerId); + } + + public byte[] getCustomerProfileImage(Integer customerId) { + var customer = customerDao.selectCustomerById(customerId) + .map(customerDTOMapper) + .orElseThrow(() -> new ResourceNotFoundException( + "customer with id [%s] not found".formatted(customerId) + )); + //TODO: Check if ProfileImageId is empty or null + if(customer.profileImageId().isBlank()){ + throw new ResourceNotFoundException( + "customer with id [%s] profile image not found".formatted(customerId)); + } + byte[] profileImage = s3service.getObject( + s3Buckets.getCustomer(), + "profile-images/%s/%s".formatted(customerId, customer.profileImageId()) + ); + return profileImage; + } } diff --git a/backend/src/main/java/com/amigoscode/s3/S3Buckets.java b/backend/src/main/java/com/amigoscode/s3/S3Buckets.java new file mode 100644 index 0000000..7568f3a --- /dev/null +++ b/backend/src/main/java/com/amigoscode/s3/S3Buckets.java @@ -0,0 +1,19 @@ +package com.amigoscode.s3; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "aws.s3.buckets") +public class S3Buckets { + + private String customer; + + public String getCustomer() { + return customer; + } + + public void setCustomer(String customer) { + this.customer = customer; + } +} diff --git a/backend/src/main/java/com/amigoscode/s3/S3Config.java b/backend/src/main/java/com/amigoscode/s3/S3Config.java new file mode 100644 index 0000000..5b1a287 --- /dev/null +++ b/backend/src/main/java/com/amigoscode/s3/S3Config.java @@ -0,0 +1,22 @@ +package com.amigoscode.s3; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@Configuration +public class S3Config { + + @Value("${aws.region}") + private String awsRegion; + + @Bean + public S3Client s3Client(){ + S3Client client = S3Client.builder() + .region(Region.of(awsRegion)) + .build(); + return client; + } +} diff --git a/backend/src/main/java/com/amigoscode/s3/S3Service.java b/backend/src/main/java/com/amigoscode/s3/S3Service.java new file mode 100644 index 0000000..a3c5635 --- /dev/null +++ b/backend/src/main/java/com/amigoscode/s3/S3Service.java @@ -0,0 +1,43 @@ +package com.amigoscode.s3; + +import org.springframework.stereotype.Service; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import java.io.IOException; + + +@Service +public class S3Service { + private final S3Client s3; + + public S3Service(S3Client s3){ + this.s3 = s3; + } + + public void putObject(String bucketName, String key, byte[] file){ + PutObjectRequest objectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + s3.putObject(objectRequest, RequestBody.fromBytes(file)); + } + + public byte[] getObject(String bucketName, String key){ + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + + ResponseInputStream res = s3.getObject(getObjectRequest); + try{ + return res.readAllBytes(); + } + catch (IOException e){ + throw new RuntimeException(e); + } + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index d3fbbe6..9ed83c6 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -9,6 +9,12 @@ cors: allowed-headers: "*" exposed-headers: "*" +aws: + region: eu-west-1 + s3: + buckets: + customer: image-service-amigoscode-customer-test + management: endpoints: web: diff --git a/backend/src/main/resources/db/migration/V2__Add_Customer_Profile_Image.sql b/backend/src/main/resources/db/migration/V2__Add_Customer_Profile_Image.sql new file mode 100644 index 0000000..fd53c19 --- /dev/null +++ b/backend/src/main/resources/db/migration/V2__Add_Customer_Profile_Image.sql @@ -0,0 +1,6 @@ +ALTER TABLE customer +ADD COLUMN profile_image_id VARCHAR(36); + +ALTER TABLE customer +ADD CONSTRAINT profile_image_id_unique +UNIQUE (profile_image_id); \ No newline at end of file diff --git a/backend/src/test/java/com/amigoscode/customer/CustomerRowMapperTest.java b/backend/src/test/java/com/amigoscode/customer/CustomerRowMapperTest.java index 81fd38f..3222719 100644 --- a/backend/src/test/java/com/amigoscode/customer/CustomerRowMapperTest.java +++ b/backend/src/test/java/com/amigoscode/customer/CustomerRowMapperTest.java @@ -22,14 +22,22 @@ void mapRow() throws SQLException { when(resultSet.getString("name")).thenReturn("Jamila"); when(resultSet.getString("email")).thenReturn("jamila@gmail.com"); when(resultSet.getString("gender")).thenReturn("FEMALE"); + when(resultSet.getString("password")).thenReturn("password"); + when(resultSet.getString("profile_image_id")).thenReturn("22222"); // When Customer actual = customerRowMapper.mapRow(resultSet, 1); // Then Customer expected = new Customer( - 1, "Jamila", "jamila@gmail.com", "password", 19, - Gender.FEMALE); + 1, + "Jamila", + "jamila@gmail.com", + "password", + 19, + Gender.FEMALE, + "22222" + ); assertThat(actual).isEqualTo(expected); } } \ No newline at end of file diff --git a/backend/src/test/java/com/amigoscode/customer/CustomerServiceTest.java b/backend/src/test/java/com/amigoscode/customer/CustomerServiceTest.java index 87190c4..e569c1b 100644 --- a/backend/src/test/java/com/amigoscode/customer/CustomerServiceTest.java +++ b/backend/src/test/java/com/amigoscode/customer/CustomerServiceTest.java @@ -3,6 +3,8 @@ import com.amigoscode.exception.DuplicateResourceException; import com.amigoscode.exception.RequestValidationException; import com.amigoscode.exception.ResourceNotFoundException; +import com.amigoscode.s3.S3Buckets; +import com.amigoscode.s3.S3Service; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,12 +26,21 @@ class CustomerServiceTest { private CustomerDao customerDao; @Mock private PasswordEncoder passwordEncoder; + @Mock + private S3Service s3Service; + @Mock + private S3Buckets s3Buckets; private CustomerService underTest; private final CustomerDTOMapper customerDTOMapper = new CustomerDTOMapper(); @BeforeEach void setUp() { - underTest = new CustomerService(customerDao, customerDTOMapper, passwordEncoder); + underTest = new CustomerService(customerDao, + customerDTOMapper, + passwordEncoder, + s3Service, + s3Buckets + ); } @Test diff --git a/backend/src/test/java/com/amigoscode/journey/CustomerIT.java b/backend/src/test/java/com/amigoscode/journey/CustomerIT.java index 0770f4e..f7c4f21 100644 --- a/backend/src/test/java/com/amigoscode/journey/CustomerIT.java +++ b/backend/src/test/java/com/amigoscode/journey/CustomerIT.java @@ -84,7 +84,8 @@ void canRegisterCustomer() { gender, age, List.of("ROLE_USER"), - email + email, + null ); assertThat(allCustomers).contains(expectedCustomer); @@ -266,7 +267,7 @@ void canUpdateCustomer() { .getResponseBody(); CustomerDTO expected = new CustomerDTO( - id, newName, email, gender, age, List.of("ROLE_USER"), email + id, newName, email, gender, age, List.of("ROLE_USER"), email, null ); assertThat(updatedCustomer).isEqualTo(expected); diff --git a/docker-compose.yml b/docker-compose.yml index 56e9949..594b5d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,30 +13,6 @@ services: networks: - db restart: unless-stopped - amigoscode-api: - container_name: amigoscode-api - image: amigoscode/amigoscode-api - environment: - SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/customer - ports: - - "8088:8080" - networks: - - db - depends_on: - - db - restart: unless-stopped - amigoscode-react: - container_name: amigoscode-react - image: amigoscode/amigoscode-react - build: - context: frontend/react - args: - api_base_url: http://localhost:8088 - ports: - - "3000:5173" - depends_on: - - amigoscode-api - restart: unless-stopped networks: db: