diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8fab6f..4646b17 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,9 @@ jobs: java-version: '23' distribution: 'temurin' + - name: Up docker compose + run: docker compose up -d + - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/.gitignore b/.gitignore index f904454..47a6148 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ build/ .classpath .factorypath .project -.settings +.authSettings .springBeans .sts4-cache bin/ diff --git a/build.gradle b/build.gradle index be376b9..15138a3 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-webmvc' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'software.amazon.awssdk:s3:2.20.0' runtimeOnly 'org.postgresql:postgresql' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/compose.yaml b/compose.yaml index ccdc4c7..10a35a5 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,6 +1,6 @@ services: postgres: - container_name: codzilla_postgres + container_name: codzilla-postgres image: postgres:15 ports: - "5433:5432" @@ -16,5 +16,43 @@ services: timeout: 5s retries: 5 + redis: + image: redis:7.2-alpine + container_name: codzilla-redis + restart: always + ports: + - "6379:6379" + command: > + redis-server + --requirepass redis-password + --maxmemory 512mb + --maxmemory-policy allkeys-lru + --appendonly yes + volumes: + - redis_data:/data + + redis-insight: + image: redis/redisinsight:latest + container_name: codzilla-redis-insight + restart: always + environment: + - RDI_PORT=5540 + ports: + - "8001:5540" + depends_on: + - redis + + minio: + image: minio/minio + container_name: codzilla-minio + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: minio-user + MINIO_ROOT_PASSWORD: minio-password + command: server /data --console-address ":9001" + volumes: + redis_data: postgres_data: \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/Submissions/RedisSubmissionRepository.java b/src/main/java/com/codzilla/backend/Submissions/RedisSubmissionRepository.java new file mode 100644 index 0000000..21a8824 --- /dev/null +++ b/src/main/java/com/codzilla/backend/Submissions/RedisSubmissionRepository.java @@ -0,0 +1,30 @@ +package com.codzilla.backend.Submissions; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; +import tools.jackson.databind.ObjectMapper; + +import java.util.Optional; + +@Repository +public class RedisSubmissionRepository implements SubmissionRepository { + + @Autowired + protected ObjectMapper objectMapper; + + @Autowired + RedisTemplate redisTemplate; + + @Override + public void save(Submission submission) { + redisTemplate.opsForValue().set("submission:" + submission.id(), submission); + } + + @Override + public Optional get(String id) { + Object value = redisTemplate.opsForValue().get("submission:"+id); + if (value == null) return Optional.empty(); + return Optional.of(objectMapper.convertValue(value, Submission.class)); + } +} diff --git a/src/main/java/com/codzilla/backend/Submissions/S3Initialization.java b/src/main/java/com/codzilla/backend/Submissions/S3Initialization.java new file mode 100644 index 0000000..d83d480 --- /dev/null +++ b/src/main/java/com/codzilla/backend/Submissions/S3Initialization.java @@ -0,0 +1,34 @@ +package com.codzilla.backend.Submissions; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; + + +@Slf4j +@Component +public class S3Initialization { + + @Autowired + SubmissionSettings settings; + + @Autowired + S3Client s3Client; + + @EventListener(ApplicationReadyEvent.class) + public void initBucket() { + try { + s3Client.headBucket(HeadBucketRequest.builder().bucket(settings.s3().bucketName()).build()); + log.info("Already have bucket: " + settings.s3().bucketName()); + } catch (NoSuchBucketException e) { + s3Client.createBucket(CreateBucketRequest.builder().bucket(settings.s3().bucketName()).build()); + log.info("Create bucket: " + settings.s3().bucketName()); + } + } +} diff --git a/src/main/java/com/codzilla/backend/Submissions/S3SubmissionRepository.java b/src/main/java/com/codzilla/backend/Submissions/S3SubmissionRepository.java new file mode 100644 index 0000000..aa36715 --- /dev/null +++ b/src/main/java/com/codzilla/backend/Submissions/S3SubmissionRepository.java @@ -0,0 +1,53 @@ +package com.codzilla.backend.Submissions; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.util.Optional; + + +@Repository +public class S3SubmissionRepository implements SubmissionRepository { + @Autowired + S3Client s3Client; + + @Autowired + SubmissionSettings settings; + + @Override + public void save(Submission submission) { + s3Client.putObject( + PutObjectRequest.builder() + .contentType("text/plain") + .bucket(settings.s3().bucketName()) + .key("submissions/" + submission.id() + ".cpp") + .build(), + RequestBody.fromString(submission.code()) + ); + } + + @Override + public Optional get(String id) { + try { + String code = s3Client.getObject( + GetObjectRequest.builder() + .bucket(settings.s3().bucketName()) + .key("submissions/" + id + ".cpp") + .build(), + ResponseTransformer.toBytes() + + ).asUtf8String(); + Submission result = new Submission(id, code, ""); + return Optional.of(result); + } catch (NoSuchKeyException ex) { + return Optional.empty(); + } + + } +} diff --git a/src/main/java/com/codzilla/backend/Submissions/Submission.java b/src/main/java/com/codzilla/backend/Submissions/Submission.java new file mode 100644 index 0000000..4feb836 --- /dev/null +++ b/src/main/java/com/codzilla/backend/Submissions/Submission.java @@ -0,0 +1,8 @@ +package com.codzilla.backend.Submissions; + +public record Submission( + String id, + String code, + String userEmail +) { +} diff --git a/src/main/java/com/codzilla/backend/Submissions/SubmissionConfiguration.java b/src/main/java/com/codzilla/backend/Submissions/SubmissionConfiguration.java new file mode 100644 index 0000000..83061e3 --- /dev/null +++ b/src/main/java/com/codzilla/backend/Submissions/SubmissionConfiguration.java @@ -0,0 +1,63 @@ +package com.codzilla.backend.Submissions; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; + +import java.net.URI; + + +@Slf4j +@Configuration +@EnableConfigurationProperties(SubmissionSettings.class) +public class SubmissionConfiguration { + + @Autowired + SubmissionSettings settings; + + @Bean + RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + var redisTemplate = new RedisTemplate(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + + redisTemplate.setKeySerializer(new StringRedisSerializer()); + + JacksonJsonRedisSerializer serializer = new JacksonJsonRedisSerializer<>(Object.class); + + redisTemplate.setValueSerializer(serializer); + return redisTemplate; + } + + + @Bean + public S3Client s3Client() { + log.info(settings.toString()); + return S3Client.builder() + .endpointOverride(URI.create(settings.s3().endpoint())) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create( + settings.s3().accessKey(), + settings.s3().secretKey()))) + .region(Region.of(settings.s3().region())) + .forcePathStyle(true) + .build(); + } + + +} diff --git a/src/main/java/com/codzilla/backend/Submissions/SubmissionRepository.java b/src/main/java/com/codzilla/backend/Submissions/SubmissionRepository.java new file mode 100644 index 0000000..dbe5284 --- /dev/null +++ b/src/main/java/com/codzilla/backend/Submissions/SubmissionRepository.java @@ -0,0 +1,10 @@ +package com.codzilla.backend.Submissions; + +import java.util.Optional; + +public interface SubmissionRepository { + + + void save(Submission submission); // returns id + Optional get(String id); +} diff --git a/src/main/java/com/codzilla/backend/Submissions/SubmissionService.java b/src/main/java/com/codzilla/backend/Submissions/SubmissionService.java new file mode 100644 index 0000000..5c49c69 --- /dev/null +++ b/src/main/java/com/codzilla/backend/Submissions/SubmissionService.java @@ -0,0 +1,4 @@ +package com.codzilla.backend.Submissions; + +public class SubmissionService { +} diff --git a/src/main/java/com/codzilla/backend/Submissions/SubmissionSettings.java b/src/main/java/com/codzilla/backend/Submissions/SubmissionSettings.java new file mode 100644 index 0000000..62297fc --- /dev/null +++ b/src/main/java/com/codzilla/backend/Submissions/SubmissionSettings.java @@ -0,0 +1,18 @@ +package com.codzilla.backend.Submissions; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@ConfigurationProperties(prefix = "app.submission") +public record SubmissionSettings (S3 s3) { + public record S3( + String endpoint, + String accessKey, + String secretKey, + String region, + String bucketName + ) {} +} + diff --git a/src/main/java/com/codzilla/backend/auth/AuthController/AuthController.java b/src/main/java/com/codzilla/backend/auth/AuthController/AuthController.java index 20c056d..99fd3f9 100644 --- a/src/main/java/com/codzilla/backend/auth/AuthController/AuthController.java +++ b/src/main/java/com/codzilla/backend/auth/AuthController/AuthController.java @@ -3,7 +3,7 @@ import com.codzilla.backend.auth.JWTUtils.JWTUtils; import com.codzilla.backend.auth.User; import com.codzilla.backend.auth.UserService; -import com.codzilla.backend.auth.config.Settings; +import com.codzilla.backend.auth.config.AuthSettings; import com.codzilla.backend.auth.dto.LoginRequestDTO; import com.codzilla.backend.auth.dto.LoginResponseDTO; import com.codzilla.backend.auth.dto.RegisterRequestDTO; @@ -26,16 +26,16 @@ public class AuthController { private final AuthenticationManager authManager; private final JWTUtils jwtUtils; - private final Settings settings; + private final AuthSettings authSettings; private final UserService userService; @Autowired public AuthController(AuthenticationManager authManager, - JWTUtils jwtUtils, Settings settings, UserService userService) { + JWTUtils jwtUtils, AuthSettings authSettings, UserService userService) { this.userService = userService; this.authManager = authManager; this.jwtUtils = jwtUtils; - this.settings = settings; + this.authSettings = authSettings; } @PostMapping("/login") @@ -51,14 +51,14 @@ public ResponseEntity login(@RequestBody LoginRequestDTO request, HttpServlet jwtCookie.setHttpOnly(true); jwtCookie.setSecure(false); jwtCookie.setPath("/"); - jwtCookie.setMaxAge((int) settings.getRefreshTokenTtl().toSeconds()); + jwtCookie.setMaxAge((int) authSettings.getRefreshTokenTtl().toSeconds()); response.addCookie(jwtCookie); var refreshToken = jwtUtils.generateRefreshToken(auth); Cookie refreshCookie = new Cookie("refresh_jwt", refreshToken); refreshCookie.setPath("/"); refreshCookie.setHttpOnly(true); - refreshCookie.setMaxAge((int) settings.getRefreshTokenTtl().toSeconds()); + refreshCookie.setMaxAge((int) authSettings.getRefreshTokenTtl().toSeconds()); refreshCookie.setSecure(false); response.addCookie(refreshCookie); @@ -115,7 +115,7 @@ public ResponseEntity refreshToken(HttpServletRequest request, HttpServletRes cookie.setHttpOnly(true); cookie.setSecure(false); cookie.setPath("/"); - cookie.setMaxAge((int) settings.getRefreshTokenTtl().toSeconds()); + cookie.setMaxAge((int) authSettings.getRefreshTokenTtl().toSeconds()); response.addCookie(cookie); return ResponseEntity.ok("Jwt access was updated."); diff --git a/src/main/java/com/codzilla/backend/auth/JWTUtils/JWTUtils.java b/src/main/java/com/codzilla/backend/auth/JWTUtils/JWTUtils.java index f790117..4189ad3 100644 --- a/src/main/java/com/codzilla/backend/auth/JWTUtils/JWTUtils.java +++ b/src/main/java/com/codzilla/backend/auth/JWTUtils/JWTUtils.java @@ -1,6 +1,6 @@ package com.codzilla.backend.auth.JWTUtils; -import com.codzilla.backend.auth.config.Settings; +import com.codzilla.backend.auth.config.AuthSettings; import io.jsonwebtoken.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -15,10 +15,10 @@ public class JWTUtils { private static SecretKey secret = Jwts.SIG.HS256.key().build(); - private final Settings settings; + private final AuthSettings authSettings; - public JWTUtils(Settings settings) { - this.settings = settings; + public JWTUtils(AuthSettings authSettings) { + this.authSettings = authSettings; } public String generateAccessToken(Authentication authentication) { @@ -31,7 +31,7 @@ public String generateAccessToken(Authentication authentication) { .subject(authentication.getName()) .claim("roles", roles) .issuedAt(new Date()) - .expiration(new Date(System.currentTimeMillis() + settings.getAccessTokenTtl().toMillis())) + .expiration(new Date(System.currentTimeMillis() + authSettings.getAccessTokenTtl().toMillis())) .signWith(secret) .compact(); } @@ -41,7 +41,7 @@ public String generateRefreshToken(Authentication authentication) { .subject(authentication.getName()) .setId(UUID.randomUUID().toString()) .issuedAt(new Date()) - .expiration(new Date(System.currentTimeMillis() + settings.getRefreshTokenTtl().toMillis())) + .expiration(new Date(System.currentTimeMillis() + authSettings.getRefreshTokenTtl().toMillis())) .signWith(secret) .compact(); } diff --git a/src/main/java/com/codzilla/backend/auth/config/Settings.java b/src/main/java/com/codzilla/backend/auth/config/AuthSettings.java similarity index 93% rename from src/main/java/com/codzilla/backend/auth/config/Settings.java rename to src/main/java/com/codzilla/backend/auth/config/AuthSettings.java index 80ee5a7..acfc788 100644 --- a/src/main/java/com/codzilla/backend/auth/config/Settings.java +++ b/src/main/java/com/codzilla/backend/auth/config/AuthSettings.java @@ -9,7 +9,7 @@ @Configuration @ConfigurationProperties(prefix = "app.security.jwt") @Data -public class Settings { +public class AuthSettings { private Duration accessTokenTtl; private Duration refreshTokenTtl; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f78f4e8..512bf66 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,14 @@ -spring.application.name=AuthService +spring.application.name=Backend + app.security.jwt.access-token-ttl=10s app.security.jwt.refresh-token-ttl=60s +app.submission.s3.endpoint=http://localhost:9000 +app.submission.s3.access-key=minio-user +app.submission.s3.secret-key=minio-password +app.submission.s3.region=us-east-1 +app.submission.s3.bucket-name=codzilla + #spring.datasource.url=${DB_URL} #spring.datasource.username=${DB_USERNAME} #spring.datasource.password=${DB_PASSWORD} @@ -11,8 +18,10 @@ spring.datasource.username=myuser spring.datasource.password=secret spring.jpa.hibernate.ddl-auto=update -spring.application.name=Backend spring.jpa.show-sql=true +spring.data.redis.host=localhost +spring.data.redis.port=6379 +spring.data.redis.password=redis-password logging.level.org.springframework.security=DEBUG diff --git a/src/test/java/com/codzilla/backend/auth/JwtIntegrationTest.java b/src/test/java/com/codzilla/backend/auth/JwtIntegrationTest.java index d4a4442..620b986 100644 --- a/src/test/java/com/codzilla/backend/auth/JwtIntegrationTest.java +++ b/src/test/java/com/codzilla/backend/auth/JwtIntegrationTest.java @@ -2,7 +2,7 @@ import com.codzilla.backend.auth.JWTUtils.JWTUtils; -import com.codzilla.backend.auth.config.Settings; +import com.codzilla.backend.auth.config.AuthSettings; import com.codzilla.backend.auth.dto.LoginRequestDTO; import com.codzilla.backend.auth.dto.RegisterRequestDTO; import jakarta.transaction.Transactional; @@ -11,13 +11,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; import java.time.Duration; import java.util.List; -import java.util.Timer; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -33,8 +31,9 @@ public class JwtIntegrationTest extends BaseIntegrationTest { @Autowired JWTUtils jwtUtils; + @Autowired - Settings settings; + AuthSettings authSettings; @BeforeEach void setUp() throws Exception { @@ -76,7 +75,7 @@ void testGetAccessWithJwt() throws Exception { @Test void testExpirationToken() throws Exception { - settings.setAccessTokenTtl(Duration.ZERO); + authSettings.setAccessTokenTtl(Duration.ZERO); RegisterRequestDTO registerRequestDTO = new RegisterRequestDTO("nick", "email", "password"); LoginRequestDTO loginRequestDTO = new LoginRequestDTO( registerRequestDTO.email(), @@ -102,7 +101,7 @@ void testExpirationToken() throws Exception { @Test void testTokenShouldExpiredAndRefreshed() throws Exception { - settings.setAccessTokenTtl(Duration.ofMillis(1000)); + authSettings.setAccessTokenTtl(Duration.ofMillis(1000)); RegisterRequestDTO registerRequestDTO = new RegisterRequestDTO("nick", "email", "password"); LoginRequestDTO loginRequestDTO = new LoginRequestDTO( registerRequestDTO.email(), diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 525401f..5ee2e87 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -4,6 +4,12 @@ spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= +app.submission.s3.endpoint=http://localhost:9000 +app.submission.s3.access-key=minio-user +app.submission.s3.secret-key=minio-password +app.submission.s3.region=us-east-1 +app.submission.s3.bucket-name=codzilla + spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true