From bfc61ed660489ebc32140ec817f7e2384feb8b07 Mon Sep 17 00:00:00 2001 From: Krrish Ghimire Date: Sat, 4 Jul 2026 23:38:17 +0545 Subject: [PATCH] publish to github or gitlab pages --- backend/build.gradle | 2 +- .../{indie => krrishg}/IndieApplication.java | 2 +- .../{indie => krrishg}/config/GitConfig.java | 2 +- .../config/GlobalExceptionHandler.java | 7 +- .../config/JwtAuthenticationToken.java | 2 +- .../{indie => krrishg}/config/JwtConfig.java | 4 +- .../config/RateLimitConfig.java | 2 +- .../config/SecurityConfig.java | 6 +- .../com/krrishg/config/StorageConfig.java | 41 ++ .../config/TokenService.java | 4 +- .../config/UserPrincipal.java | 2 +- .../controller/AuthController.java | 12 +- .../controller/DeploymentController.java | 44 +++ .../krrishg/controller/ExportController.java | 62 +++ .../com/krrishg/controller/GitController.java | 102 +++++ .../controller/LLMController.java | 14 +- .../krrishg/controller/MediaController.java | 49 +++ .../controller/SiteController.java | 12 +- .../{indie => krrishg}/dto/AuthResponse.java | 4 +- .../{indie => krrishg}/dto/LLMOptions.java | 2 +- .../com/{indie => krrishg}/dto/LLMResult.java | 2 +- .../{indie => krrishg}/dto/LoginRequest.java | 2 +- .../com/{indie => krrishg}/dto/Reference.java | 2 +- .../dto/RefreshTokenRequest.java | 2 +- .../dto/RegisterRequest.java | 2 +- .../{indie => krrishg}/dto/SiteRequest.java | 2 +- .../{indie => krrishg}/dto/SiteResponse.java | 8 +- .../{indie => krrishg}/dto/SiteStructure.java | 2 +- .../java/com/krrishg/engine/JsBundle.java | 74 ++++ .../com/krrishg/engine/StyleCompiler.java | 31 ++ .../model/AuthProvider.java | 2 +- .../model/GenerationVersion.java | 2 +- .../java/com/krrishg/model/GitRemote.java | 63 +++ .../com/{indie => krrishg}/model/Site.java | 4 +- .../{indie => krrishg}/model/SiteStatus.java | 2 +- .../com/{indie => krrishg}/model/User.java | 2 +- .../repository/GitRemoteRepository.java | 16 + .../repository/SiteRepository.java | 4 +- .../repository/UserRepository.java | 6 +- .../service/AuthService.java | 18 +- .../service/CustomOAuth2User.java | 4 +- .../krrishg/service/DeploymentService.java | 85 ++++ .../com/krrishg/service/DomainService.java | 60 +++ .../com/krrishg/service/ExportService.java | 44 +++ .../java/com/krrishg/service/GitService.java | 369 ++++++++++++++++++ .../java/com/krrishg/service/KeyManager.java | 91 +++++ .../service/LLMResponseParser.java | 4 +- .../service/LLMService.java | 16 +- .../com/krrishg/service/MediaService.java | 88 +++++ .../service/OAuth2SuccessHandler.java | 6 +- .../service/OAuth2UserService.java | 8 +- .../service/ReferenceService.java | 4 +- .../com/krrishg/service/RenderService.java | 347 ++++++++++++++++ .../service/llm/GeminiProvider.java | 8 +- .../service/llm/LLMFactory.java | 2 +- .../service/llm/LLMProvider.java | 8 +- .../service/llm/OpenAIProvider.java | 8 +- backend/src/main/resources/application.yml | 9 + .../config/GlobalExceptionHandlerTest.java | 2 +- .../config/RateLimitConfigTest.java | 2 +- .../config/UserPrincipalTest.java | 2 +- .../controller/AuthControllerTest.java | 20 +- .../controller/LLMControllerTest.java | 20 +- .../controller/SiteControllerTest.java | 18 +- .../dto/SiteResponseTest.java | 6 +- .../{indie => krrishg}/model/SiteTest.java | 2 +- .../{indie => krrishg}/model/UserTest.java | 2 +- .../repository/SiteRepositoryTest.java | 6 +- .../repository/UserRepositoryTest.java | 6 +- .../service/AuthServiceTest.java | 20 +- .../service/FakeReferenceService.java | 4 +- .../service/LLMResponseParserTest.java | 4 +- .../service/LLMServiceTest.java | 15 +- .../service/OAuth2UserServiceTest.java | 10 +- .../service/ReferenceServiceTest.java | 4 +- .../service/llm/LLMFactoryTest.java | 4 +- .../support/FakeJwtConfig.java | 6 +- .../support/FakeLLMProvider.java | 10 +- .../support/FakeSiteRepository.java | 6 +- .../support/FakeUserRepository.java | 8 +- .../support/InMemoryRepository.java | 2 +- .../support/StubPasswordEncoder.java | 2 +- .../support/TestOAuth2User.java | 2 +- .../support/TestSecurityConfig.java | 2 +- frontend/src/app/app.routes.ts | 10 + frontend/src/app/core/models/deployment.ts | 25 ++ frontend/src/app/core/models/site.ts | 1 + .../app/core/services/deployment.service.ts | 33 ++ .../create-site-dialog/create-site-dialog.ts | 1 - .../src/app/dashboard/dashboard.component.ts | 2 +- .../git-settings/git-settings.component.ts | 236 +++++++++++ .../llm-workspace/llm-workspace.component.ts | 7 + .../settings/settings.component.ts | 278 +++++++++++++ 93 files changed, 2367 insertions(+), 193 deletions(-) rename backend/src/main/java/com/{indie => krrishg}/IndieApplication.java (93%) rename backend/src/main/java/com/{indie => krrishg}/config/GitConfig.java (97%) rename backend/src/main/java/com/{indie => krrishg}/config/GlobalExceptionHandler.java (85%) rename backend/src/main/java/com/{indie => krrishg}/config/JwtAuthenticationToken.java (95%) rename backend/src/main/java/com/{indie => krrishg}/config/JwtConfig.java (98%) rename backend/src/main/java/com/{indie => krrishg}/config/RateLimitConfig.java (97%) rename backend/src/main/java/com/{indie => krrishg}/config/SecurityConfig.java (98%) create mode 100644 backend/src/main/java/com/krrishg/config/StorageConfig.java rename backend/src/main/java/com/{indie => krrishg}/config/TokenService.java (76%) rename backend/src/main/java/com/{indie => krrishg}/config/UserPrincipal.java (97%) rename backend/src/main/java/com/{indie => krrishg}/controller/AuthController.java (86%) create mode 100644 backend/src/main/java/com/krrishg/controller/DeploymentController.java create mode 100644 backend/src/main/java/com/krrishg/controller/ExportController.java create mode 100644 backend/src/main/java/com/krrishg/controller/GitController.java rename backend/src/main/java/com/{indie => krrishg}/controller/LLMController.java (95%) create mode 100644 backend/src/main/java/com/krrishg/controller/MediaController.java rename backend/src/main/java/com/{indie => krrishg}/controller/SiteController.java (95%) rename backend/src/main/java/com/{indie => krrishg}/dto/AuthResponse.java (92%) rename backend/src/main/java/com/{indie => krrishg}/dto/LLMOptions.java (92%) rename backend/src/main/java/com/{indie => krrishg}/dto/LLMResult.java (92%) rename backend/src/main/java/com/{indie => krrishg}/dto/LoginRequest.java (93%) rename backend/src/main/java/com/{indie => krrishg}/dto/Reference.java (91%) rename backend/src/main/java/com/{indie => krrishg}/dto/RefreshTokenRequest.java (88%) rename backend/src/main/java/com/{indie => krrishg}/dto/RegisterRequest.java (95%) rename backend/src/main/java/com/{indie => krrishg}/dto/SiteRequest.java (90%) rename backend/src/main/java/com/{indie => krrishg}/dto/SiteResponse.java (84%) rename backend/src/main/java/com/{indie => krrishg}/dto/SiteStructure.java (97%) create mode 100644 backend/src/main/java/com/krrishg/engine/JsBundle.java create mode 100644 backend/src/main/java/com/krrishg/engine/StyleCompiler.java rename backend/src/main/java/com/{indie => krrishg}/model/AuthProvider.java (70%) rename backend/src/main/java/com/{indie => krrishg}/model/GenerationVersion.java (97%) create mode 100644 backend/src/main/java/com/krrishg/model/GitRemote.java rename backend/src/main/java/com/{indie => krrishg}/model/Site.java (95%) rename backend/src/main/java/com/{indie => krrishg}/model/SiteStatus.java (64%) rename backend/src/main/java/com/{indie => krrishg}/model/User.java (97%) create mode 100644 backend/src/main/java/com/krrishg/repository/GitRemoteRepository.java rename backend/src/main/java/com/{indie => krrishg}/repository/SiteRepository.java (85%) rename backend/src/main/java/com/{indie => krrishg}/repository/UserRepository.java (78%) rename backend/src/main/java/com/{indie => krrishg}/service/AuthService.java (90%) rename backend/src/main/java/com/{indie => krrishg}/service/CustomOAuth2User.java (94%) create mode 100644 backend/src/main/java/com/krrishg/service/DeploymentService.java create mode 100644 backend/src/main/java/com/krrishg/service/DomainService.java create mode 100644 backend/src/main/java/com/krrishg/service/ExportService.java create mode 100644 backend/src/main/java/com/krrishg/service/GitService.java create mode 100644 backend/src/main/java/com/krrishg/service/KeyManager.java rename backend/src/main/java/com/{indie => krrishg}/service/LLMResponseParser.java (98%) rename backend/src/main/java/com/{indie => krrishg}/service/LLMService.java (96%) create mode 100644 backend/src/main/java/com/krrishg/service/MediaService.java rename backend/src/main/java/com/{indie => krrishg}/service/OAuth2SuccessHandler.java (93%) rename backend/src/main/java/com/{indie => krrishg}/service/OAuth2UserService.java (96%) rename backend/src/main/java/com/{indie => krrishg}/service/ReferenceService.java (97%) create mode 100644 backend/src/main/java/com/krrishg/service/RenderService.java rename backend/src/main/java/com/{indie => krrishg}/service/llm/GeminiProvider.java (97%) rename backend/src/main/java/com/{indie => krrishg}/service/llm/LLMFactory.java (98%) rename backend/src/main/java/com/{indie => krrishg}/service/llm/LLMProvider.java (58%) rename backend/src/main/java/com/{indie => krrishg}/service/llm/OpenAIProvider.java (96%) rename backend/src/test/java/com/{indie => krrishg}/config/GlobalExceptionHandlerTest.java (98%) rename backend/src/test/java/com/{indie => krrishg}/config/RateLimitConfigTest.java (98%) rename backend/src/test/java/com/{indie => krrishg}/config/UserPrincipalTest.java (97%) rename backend/src/test/java/com/{indie => krrishg}/controller/AuthControllerTest.java (92%) rename backend/src/test/java/com/{indie => krrishg}/controller/LLMControllerTest.java (97%) rename backend/src/test/java/com/{indie => krrishg}/controller/SiteControllerTest.java (95%) rename backend/src/test/java/com/{indie => krrishg}/dto/SiteResponseTest.java (94%) rename backend/src/test/java/com/{indie => krrishg}/model/SiteTest.java (98%) rename backend/src/test/java/com/{indie => krrishg}/model/UserTest.java (98%) rename backend/src/test/java/com/{indie => krrishg}/repository/SiteRepositoryTest.java (96%) rename backend/src/test/java/com/{indie => krrishg}/repository/UserRepositoryTest.java (96%) rename backend/src/test/java/com/{indie => krrishg}/service/AuthServiceTest.java (94%) rename backend/src/test/java/com/{indie => krrishg}/service/FakeReferenceService.java (94%) rename backend/src/test/java/com/{indie => krrishg}/service/LLMResponseParserTest.java (99%) rename backend/src/test/java/com/{indie => krrishg}/service/LLMServiceTest.java (97%) rename backend/src/test/java/com/{indie => krrishg}/service/OAuth2UserServiceTest.java (95%) rename backend/src/test/java/com/{indie => krrishg}/service/ReferenceServiceTest.java (97%) rename backend/src/test/java/com/{indie => krrishg}/service/llm/LLMFactoryTest.java (97%) rename backend/src/test/java/com/{indie => krrishg}/support/FakeJwtConfig.java (93%) rename backend/src/test/java/com/{indie => krrishg}/support/FakeLLMProvider.java (86%) rename backend/src/test/java/com/{indie => krrishg}/support/FakeSiteRepository.java (91%) rename backend/src/test/java/com/{indie => krrishg}/support/FakeUserRepository.java (88%) rename backend/src/test/java/com/{indie => krrishg}/support/InMemoryRepository.java (99%) rename backend/src/test/java/com/{indie => krrishg}/support/StubPasswordEncoder.java (94%) rename backend/src/test/java/com/{indie => krrishg}/support/TestOAuth2User.java (97%) rename backend/src/test/java/com/{indie => krrishg}/support/TestSecurityConfig.java (96%) create mode 100644 frontend/src/app/core/models/deployment.ts create mode 100644 frontend/src/app/core/services/deployment.service.ts create mode 100644 frontend/src/app/llm-workspace/git-settings/git-settings.component.ts create mode 100644 frontend/src/app/llm-workspace/settings/settings.component.ts diff --git a/backend/build.gradle b/backend/build.gradle index 4c100be..dfc62de 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -4,7 +4,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.4' } -group = 'com.indie' +group = 'com.krrishg' version = '0.1.0' java { diff --git a/backend/src/main/java/com/indie/IndieApplication.java b/backend/src/main/java/com/krrishg/IndieApplication.java similarity index 93% rename from backend/src/main/java/com/indie/IndieApplication.java rename to backend/src/main/java/com/krrishg/IndieApplication.java index 7e0f323..aab87fa 100644 --- a/backend/src/main/java/com/indie/IndieApplication.java +++ b/backend/src/main/java/com/krrishg/IndieApplication.java @@ -1,4 +1,4 @@ -package com.indie; +package com.krrishg; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/backend/src/main/java/com/indie/config/GitConfig.java b/backend/src/main/java/com/krrishg/config/GitConfig.java similarity index 97% rename from backend/src/main/java/com/indie/config/GitConfig.java rename to backend/src/main/java/com/krrishg/config/GitConfig.java index 7495ff2..b5bea5e 100644 --- a/backend/src/main/java/com/indie/config/GitConfig.java +++ b/backend/src/main/java/com/krrishg/config/GitConfig.java @@ -1,4 +1,4 @@ -package com.indie.config; +package com.krrishg.config; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; diff --git a/backend/src/main/java/com/indie/config/GlobalExceptionHandler.java b/backend/src/main/java/com/krrishg/config/GlobalExceptionHandler.java similarity index 85% rename from backend/src/main/java/com/indie/config/GlobalExceptionHandler.java rename to backend/src/main/java/com/krrishg/config/GlobalExceptionHandler.java index eedd51e..823941c 100644 --- a/backend/src/main/java/com/indie/config/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/krrishg/config/GlobalExceptionHandler.java @@ -1,5 +1,7 @@ -package com.indie.config; +package com.krrishg.config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.validation.FieldError; @@ -12,6 +14,8 @@ import java.util.stream.Collectors; @RestControllerAdvice public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @ExceptionHandler(IllegalArgumentException.class) public ProblemDetail handleBadRequest(IllegalArgumentException ex) { return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage()); @@ -27,6 +31,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ProblemDetail handleGeneral(Exception ex) { + log.error("Unhandled exception", ex); return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred"); } diff --git a/backend/src/main/java/com/indie/config/JwtAuthenticationToken.java b/backend/src/main/java/com/krrishg/config/JwtAuthenticationToken.java similarity index 95% rename from backend/src/main/java/com/indie/config/JwtAuthenticationToken.java rename to backend/src/main/java/com/krrishg/config/JwtAuthenticationToken.java index 63a53b4..c24ba9b 100644 --- a/backend/src/main/java/com/indie/config/JwtAuthenticationToken.java +++ b/backend/src/main/java/com/krrishg/config/JwtAuthenticationToken.java @@ -1,4 +1,4 @@ -package com.indie.config; +package com.krrishg.config; import org.springframework.security.authentication.AbstractAuthenticationToken; diff --git a/backend/src/main/java/com/indie/config/JwtConfig.java b/backend/src/main/java/com/krrishg/config/JwtConfig.java similarity index 98% rename from backend/src/main/java/com/indie/config/JwtConfig.java rename to backend/src/main/java/com/krrishg/config/JwtConfig.java index 3b963af..27294b4 100644 --- a/backend/src/main/java/com/indie/config/JwtConfig.java +++ b/backend/src/main/java/com/krrishg/config/JwtConfig.java @@ -1,6 +1,6 @@ -package com.indie.config; +package com.krrishg.config; -import com.indie.model.User; +import com.krrishg.model.User; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import jakarta.annotation.PostConstruct; diff --git a/backend/src/main/java/com/indie/config/RateLimitConfig.java b/backend/src/main/java/com/krrishg/config/RateLimitConfig.java similarity index 97% rename from backend/src/main/java/com/indie/config/RateLimitConfig.java rename to backend/src/main/java/com/krrishg/config/RateLimitConfig.java index 272677f..29ca797 100644 --- a/backend/src/main/java/com/indie/config/RateLimitConfig.java +++ b/backend/src/main/java/com/krrishg/config/RateLimitConfig.java @@ -1,4 +1,4 @@ -package com.indie.config; +package com.krrishg.config; import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bucket; diff --git a/backend/src/main/java/com/indie/config/SecurityConfig.java b/backend/src/main/java/com/krrishg/config/SecurityConfig.java similarity index 98% rename from backend/src/main/java/com/indie/config/SecurityConfig.java rename to backend/src/main/java/com/krrishg/config/SecurityConfig.java index 96c3b6f..a8d2423 100644 --- a/backend/src/main/java/com/indie/config/SecurityConfig.java +++ b/backend/src/main/java/com/krrishg/config/SecurityConfig.java @@ -1,7 +1,7 @@ -package com.indie.config; +package com.krrishg.config; -import com.indie.service.OAuth2SuccessHandler; -import com.indie.service.OAuth2UserService; +import com.krrishg.service.OAuth2SuccessHandler; +import com.krrishg.service.OAuth2UserService; import io.jsonwebtoken.Claims; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/backend/src/main/java/com/krrishg/config/StorageConfig.java b/backend/src/main/java/com/krrishg/config/StorageConfig.java new file mode 100644 index 0000000..d53b0cc --- /dev/null +++ b/backend/src/main/java/com/krrishg/config/StorageConfig.java @@ -0,0 +1,41 @@ +package com.krrishg.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.S3Configuration; + +import java.net.URI; + +@Configuration +public class StorageConfig { + + @Value("${indie.storage.endpoint}") + private String endpoint; + + @Value("${indie.storage.region}") + private String region; + + @Value("${indie.storage.access-key}") + private String accessKey; + + @Value("${indie.storage.secret-key}") + private String secretKey; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .endpointOverride(URI.create(endpoint)) + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey))) + .serviceConfiguration(S3Configuration.builder() + .pathStyleAccessEnabled(true) + .build()) + .build(); + } +} diff --git a/backend/src/main/java/com/indie/config/TokenService.java b/backend/src/main/java/com/krrishg/config/TokenService.java similarity index 76% rename from backend/src/main/java/com/indie/config/TokenService.java rename to backend/src/main/java/com/krrishg/config/TokenService.java index 62b8726..2d6db30 100644 --- a/backend/src/main/java/com/indie/config/TokenService.java +++ b/backend/src/main/java/com/krrishg/config/TokenService.java @@ -1,6 +1,6 @@ -package com.indie.config; +package com.krrishg.config; -import com.indie.model.User; +import com.krrishg.model.User; import io.jsonwebtoken.Claims; public interface TokenService { diff --git a/backend/src/main/java/com/indie/config/UserPrincipal.java b/backend/src/main/java/com/krrishg/config/UserPrincipal.java similarity index 97% rename from backend/src/main/java/com/indie/config/UserPrincipal.java rename to backend/src/main/java/com/krrishg/config/UserPrincipal.java index 6518e11..f64eafd 100644 --- a/backend/src/main/java/com/indie/config/UserPrincipal.java +++ b/backend/src/main/java/com/krrishg/config/UserPrincipal.java @@ -1,4 +1,4 @@ -package com.indie.config; +package com.krrishg.config; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; diff --git a/backend/src/main/java/com/indie/controller/AuthController.java b/backend/src/main/java/com/krrishg/controller/AuthController.java similarity index 86% rename from backend/src/main/java/com/indie/controller/AuthController.java rename to backend/src/main/java/com/krrishg/controller/AuthController.java index cb85468..369bf80 100644 --- a/backend/src/main/java/com/indie/controller/AuthController.java +++ b/backend/src/main/java/com/krrishg/controller/AuthController.java @@ -1,10 +1,10 @@ -package com.indie.controller; +package com.krrishg.controller; -import com.indie.dto.AuthResponse; -import com.indie.dto.LoginRequest; -import com.indie.dto.RefreshTokenRequest; -import com.indie.dto.RegisterRequest; -import com.indie.service.AuthService; +import com.krrishg.dto.AuthResponse; +import com.krrishg.dto.LoginRequest; +import com.krrishg.dto.RefreshTokenRequest; +import com.krrishg.dto.RegisterRequest; +import com.krrishg.service.AuthService; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/backend/src/main/java/com/krrishg/controller/DeploymentController.java b/backend/src/main/java/com/krrishg/controller/DeploymentController.java new file mode 100644 index 0000000..e5f5a7f --- /dev/null +++ b/backend/src/main/java/com/krrishg/controller/DeploymentController.java @@ -0,0 +1,44 @@ +package com.krrishg.controller; + +import com.krrishg.config.UserPrincipal; +import com.krrishg.service.DeploymentService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping("/api/sites/{id}") +public class DeploymentController { + + private final DeploymentService deploymentService; + + public DeploymentController(DeploymentService deploymentService) { + this.deploymentService = deploymentService; + } + + @PostMapping("/publish") + public ResponseEntity publish( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable UUID id) { + try { + String url = deploymentService.publish(id); + return ResponseEntity.ok(Map.of( + "message", "Site published successfully", + "url", url + )); + } catch (IllegalStateException e) { + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } + } + + @PostMapping("/unpublish") + public ResponseEntity unpublish( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable UUID id) { + deploymentService.unpublish(id); + return ResponseEntity.ok(Map.of("message", "Site unpublished successfully")); + } +} diff --git a/backend/src/main/java/com/krrishg/controller/ExportController.java b/backend/src/main/java/com/krrishg/controller/ExportController.java new file mode 100644 index 0000000..4ada0f8 --- /dev/null +++ b/backend/src/main/java/com/krrishg/controller/ExportController.java @@ -0,0 +1,62 @@ +package com.krrishg.controller; + +import com.krrishg.config.UserPrincipal; +import com.krrishg.dto.SiteStructure; +import com.krrishg.model.Site; +import com.krrishg.repository.SiteRepository; +import com.krrishg.service.ExportService; +import com.krrishg.service.LLMService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/sites/{id}") +public class ExportController { + + private final ExportService exportService; + private final LLMService llmService; + private final SiteRepository siteRepository; + + public ExportController(ExportService exportService, LLMService llmService, + SiteRepository siteRepository) { + this.exportService = exportService; + this.llmService = llmService; + this.siteRepository = siteRepository; + } + + @GetMapping("/export") + public ResponseEntity exportSite( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable UUID id) { + Site site = siteRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Site not found")); + + var versions = llmService.getVersions(id); + if (versions.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + SiteStructure structure = llmService.restoreVersion(versions.get(0).getId()); + try { + byte[] zip = exportService.exportSite(structure); + String filename = site.getSubdomain() != null ? site.getSubdomain() : site.getName().replaceAll("\\s+", "-"); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + sanitizeFilename(filename) + "-export.zip\"") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .contentLength(zip.length) + .body(zip); + } catch (Exception e) { + return ResponseEntity.internalServerError().build(); + } + } + + private String sanitizeFilename(String name) { + return name.replaceAll("[^a-zA-Z0-9_-]", "").toLowerCase(); + } +} diff --git a/backend/src/main/java/com/krrishg/controller/GitController.java b/backend/src/main/java/com/krrishg/controller/GitController.java new file mode 100644 index 0000000..b07f085 --- /dev/null +++ b/backend/src/main/java/com/krrishg/controller/GitController.java @@ -0,0 +1,102 @@ +package com.krrishg.controller; + +import com.krrishg.config.UserPrincipal; +import com.krrishg.model.GitRemote; +import com.krrishg.repository.GitRemoteRepository; +import com.krrishg.service.GitService; +import com.krrishg.service.KeyManager; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping("/api/sites/{id}/git") +public class GitController { + + private final GitService gitService; + private final GitRemoteRepository gitRemoteRepository; + private final KeyManager keyManager; + + public GitController(GitService gitService, + GitRemoteRepository gitRemoteRepository, + KeyManager keyManager) { + this.gitService = gitService; + this.gitRemoteRepository = gitRemoteRepository; + this.keyManager = keyManager; + } + + @Transactional + @PostMapping("/connect") + public ResponseEntity connectRemote( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable UUID id, + @RequestBody Map request) { + String provider = request.get("provider"); + String remoteUrl = request.get("remoteUrl"); + String token = request.get("token"); + + if (provider == null || remoteUrl == null || token == null) { + return ResponseEntity.badRequest().body(Map.of("error", "provider, remoteUrl, and token are required")); + } + + GitRemote.GitProvider gitProvider; + try { + gitProvider = GitRemote.GitProvider.valueOf(provider.toUpperCase()); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(Map.of("error", "Invalid provider. Must be GITHUB or GITLAB")); + } + + gitRemoteRepository.findBySiteId(id).ifPresent(existing -> + gitRemoteRepository.delete(existing)); + + String siteId = id.toString(); + gitService.initRepo(siteId); + String publicKey = keyManager.getPublicKey(siteId); + + GitRemote remote = GitRemote.builder() + .siteId(id) + .provider(gitProvider) + .remoteUrl(remoteUrl) + .encryptedToken(token) + .build(); + gitRemoteRepository.save(remote); + + return ResponseEntity.ok(Map.of( + "message", "Remote connected successfully", + "provider", provider, + "remoteUrl", remoteUrl, + "publicKey", publicKey + )); + } + + @Transactional + @PostMapping("/disconnect") + public ResponseEntity disconnectRemote( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable UUID id) { + gitRemoteRepository.deleteBySiteId(id); + keyManager.deleteKeys(id.toString()); + return ResponseEntity.ok(Map.of("message", "Remote disconnected")); + } + + @GetMapping("/status") + public ResponseEntity getStatus( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable UUID id) { + var remote = gitRemoteRepository.findBySiteId(id); + var log = gitService.getLog(id.toString(), 10); + + var response = new HashMap(); + response.put("connected", remote.isPresent()); + response.put("provider", remote.map(r -> r.getProvider().name()).orElse(null)); + response.put("remoteUrl", remote.map(GitRemote::getRemoteUrl).orElse(null)); + response.put("hasKeys", keyManager.hasKeyPair(id.toString())); + response.put("commits", log); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/com/indie/controller/LLMController.java b/backend/src/main/java/com/krrishg/controller/LLMController.java similarity index 95% rename from backend/src/main/java/com/indie/controller/LLMController.java rename to backend/src/main/java/com/krrishg/controller/LLMController.java index 8706fa4..a644bc7 100644 --- a/backend/src/main/java/com/indie/controller/LLMController.java +++ b/backend/src/main/java/com/krrishg/controller/LLMController.java @@ -1,11 +1,11 @@ -package com.indie.controller; +package com.krrishg.controller; -import com.indie.config.RateLimitConfig; -import com.indie.config.UserPrincipal; -import com.indie.dto.Reference; -import com.indie.dto.SiteStructure; -import com.indie.model.GenerationVersion; -import com.indie.service.LLMService; +import com.krrishg.config.RateLimitConfig; +import com.krrishg.config.UserPrincipal; +import com.krrishg.dto.Reference; +import com.krrishg.dto.SiteStructure; +import com.krrishg.model.GenerationVersion; +import com.krrishg.service.LLMService; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.HttpStatus; diff --git a/backend/src/main/java/com/krrishg/controller/MediaController.java b/backend/src/main/java/com/krrishg/controller/MediaController.java new file mode 100644 index 0000000..4be42fe --- /dev/null +++ b/backend/src/main/java/com/krrishg/controller/MediaController.java @@ -0,0 +1,49 @@ +package com.krrishg.controller; + +import com.krrishg.config.UserPrincipal; +import com.krrishg.service.MediaService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping("/api/media") +public class MediaController { + + private final MediaService mediaService; + + public MediaController(MediaService mediaService) { + this.mediaService = mediaService; + } + + @PostMapping("/upload") + public ResponseEntity upload( + @AuthenticationPrincipal UserPrincipal principal, + @RequestParam("file") MultipartFile file, + @RequestParam("siteId") UUID siteId) { + try { + MediaService.MediaUploadResult result = mediaService.upload(file, siteId); + return ResponseEntity.ok(Map.of( + "key", result.key(), + "url", result.url(), + "originalFilename", result.originalFilename(), + "contentType", result.contentType(), + "size", result.size() + )); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } + } + + @GetMapping("/{key}") + public ResponseEntity getMediaUrl( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable String key) { + String url = mediaService.getPublicUrl(key); + return ResponseEntity.ok(Map.of("key", key, "url", url)); + } +} diff --git a/backend/src/main/java/com/indie/controller/SiteController.java b/backend/src/main/java/com/krrishg/controller/SiteController.java similarity index 95% rename from backend/src/main/java/com/indie/controller/SiteController.java rename to backend/src/main/java/com/krrishg/controller/SiteController.java index 5bfa45a..ef975f9 100644 --- a/backend/src/main/java/com/indie/controller/SiteController.java +++ b/backend/src/main/java/com/krrishg/controller/SiteController.java @@ -1,10 +1,10 @@ -package com.indie.controller; +package com.krrishg.controller; -import com.indie.config.UserPrincipal; -import com.indie.dto.SiteRequest; -import com.indie.dto.SiteResponse; -import com.indie.model.Site; -import com.indie.repository.SiteRepository; +import com.krrishg.config.UserPrincipal; +import com.krrishg.dto.SiteRequest; +import com.krrishg.dto.SiteResponse; +import com.krrishg.model.Site; +import com.krrishg.repository.SiteRepository; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/backend/src/main/java/com/indie/dto/AuthResponse.java b/backend/src/main/java/com/krrishg/dto/AuthResponse.java similarity index 92% rename from backend/src/main/java/com/indie/dto/AuthResponse.java rename to backend/src/main/java/com/krrishg/dto/AuthResponse.java index a376df3..4c1a2f4 100644 --- a/backend/src/main/java/com/indie/dto/AuthResponse.java +++ b/backend/src/main/java/com/krrishg/dto/AuthResponse.java @@ -1,6 +1,6 @@ -package com.indie.dto; +package com.krrishg.dto; -import com.indie.model.AuthProvider; +import com.krrishg.model.AuthProvider; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/backend/src/main/java/com/indie/dto/LLMOptions.java b/backend/src/main/java/com/krrishg/dto/LLMOptions.java similarity index 92% rename from backend/src/main/java/com/indie/dto/LLMOptions.java rename to backend/src/main/java/com/krrishg/dto/LLMOptions.java index 6b39af6..181d659 100644 --- a/backend/src/main/java/com/indie/dto/LLMOptions.java +++ b/backend/src/main/java/com/krrishg/dto/LLMOptions.java @@ -1,4 +1,4 @@ -package com.indie.dto; +package com.krrishg.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/backend/src/main/java/com/indie/dto/LLMResult.java b/backend/src/main/java/com/krrishg/dto/LLMResult.java similarity index 92% rename from backend/src/main/java/com/indie/dto/LLMResult.java rename to backend/src/main/java/com/krrishg/dto/LLMResult.java index 0e9854b..9d2596c 100644 --- a/backend/src/main/java/com/indie/dto/LLMResult.java +++ b/backend/src/main/java/com/krrishg/dto/LLMResult.java @@ -1,4 +1,4 @@ -package com.indie.dto; +package com.krrishg.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/backend/src/main/java/com/indie/dto/LoginRequest.java b/backend/src/main/java/com/krrishg/dto/LoginRequest.java similarity index 93% rename from backend/src/main/java/com/indie/dto/LoginRequest.java rename to backend/src/main/java/com/krrishg/dto/LoginRequest.java index 30e2d1b..cbde960 100644 --- a/backend/src/main/java/com/indie/dto/LoginRequest.java +++ b/backend/src/main/java/com/krrishg/dto/LoginRequest.java @@ -1,4 +1,4 @@ -package com.indie.dto; +package com.krrishg.dto; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/backend/src/main/java/com/indie/dto/Reference.java b/backend/src/main/java/com/krrishg/dto/Reference.java similarity index 91% rename from backend/src/main/java/com/indie/dto/Reference.java rename to backend/src/main/java/com/krrishg/dto/Reference.java index 8d399af..c3a241f 100644 --- a/backend/src/main/java/com/indie/dto/Reference.java +++ b/backend/src/main/java/com/krrishg/dto/Reference.java @@ -1,4 +1,4 @@ -package com.indie.dto; +package com.krrishg.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/backend/src/main/java/com/indie/dto/RefreshTokenRequest.java b/backend/src/main/java/com/krrishg/dto/RefreshTokenRequest.java similarity index 88% rename from backend/src/main/java/com/indie/dto/RefreshTokenRequest.java rename to backend/src/main/java/com/krrishg/dto/RefreshTokenRequest.java index ce26a8f..380e7ce 100644 --- a/backend/src/main/java/com/indie/dto/RefreshTokenRequest.java +++ b/backend/src/main/java/com/krrishg/dto/RefreshTokenRequest.java @@ -1,4 +1,4 @@ -package com.indie.dto; +package com.krrishg.dto; import jakarta.validation.constraints.NotBlank; import lombok.Data; diff --git a/backend/src/main/java/com/indie/dto/RegisterRequest.java b/backend/src/main/java/com/krrishg/dto/RegisterRequest.java similarity index 95% rename from backend/src/main/java/com/indie/dto/RegisterRequest.java rename to backend/src/main/java/com/krrishg/dto/RegisterRequest.java index 2f77c51..99e72fc 100644 --- a/backend/src/main/java/com/indie/dto/RegisterRequest.java +++ b/backend/src/main/java/com/krrishg/dto/RegisterRequest.java @@ -1,4 +1,4 @@ -package com.indie.dto; +package com.krrishg.dto; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/backend/src/main/java/com/indie/dto/SiteRequest.java b/backend/src/main/java/com/krrishg/dto/SiteRequest.java similarity index 90% rename from backend/src/main/java/com/indie/dto/SiteRequest.java rename to backend/src/main/java/com/krrishg/dto/SiteRequest.java index e2809e5..e451e03 100644 --- a/backend/src/main/java/com/indie/dto/SiteRequest.java +++ b/backend/src/main/java/com/krrishg/dto/SiteRequest.java @@ -1,4 +1,4 @@ -package com.indie.dto; +package com.krrishg.dto; import jakarta.validation.constraints.NotBlank; import lombok.Data; diff --git a/backend/src/main/java/com/indie/dto/SiteResponse.java b/backend/src/main/java/com/krrishg/dto/SiteResponse.java similarity index 84% rename from backend/src/main/java/com/indie/dto/SiteResponse.java rename to backend/src/main/java/com/krrishg/dto/SiteResponse.java index 2e783fa..8bce05a 100644 --- a/backend/src/main/java/com/indie/dto/SiteResponse.java +++ b/backend/src/main/java/com/krrishg/dto/SiteResponse.java @@ -1,7 +1,7 @@ -package com.indie.dto; +package com.krrishg.dto; -import com.indie.model.Site; -import com.indie.model.SiteStatus; +import com.krrishg.model.Site; +import com.krrishg.model.SiteStatus; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -19,6 +19,7 @@ public class SiteResponse { private String name; private String description; private String subdomain; + private String publishedUrl; private SiteStatus status; private LocalDateTime publishedAt; private LocalDateTime createdAt; @@ -31,6 +32,7 @@ public class SiteResponse { .name(site.getName()) .description(site.getDescription()) .subdomain(site.getSubdomain()) + .publishedUrl(site.getPublishedUrl()) .status(site.getStatus()) .publishedAt(site.getPublishedAt()) .createdAt(site.getCreatedAt()) diff --git a/backend/src/main/java/com/indie/dto/SiteStructure.java b/backend/src/main/java/com/krrishg/dto/SiteStructure.java similarity index 97% rename from backend/src/main/java/com/indie/dto/SiteStructure.java rename to backend/src/main/java/com/krrishg/dto/SiteStructure.java index 792778f..804c10e 100644 --- a/backend/src/main/java/com/indie/dto/SiteStructure.java +++ b/backend/src/main/java/com/krrishg/dto/SiteStructure.java @@ -1,4 +1,4 @@ -package com.indie.dto; +package com.krrishg.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/backend/src/main/java/com/krrishg/engine/JsBundle.java b/backend/src/main/java/com/krrishg/engine/JsBundle.java new file mode 100644 index 0000000..2e0f7e5 --- /dev/null +++ b/backend/src/main/java/com/krrishg/engine/JsBundle.java @@ -0,0 +1,74 @@ +package com.krrishg.engine; + +import org.springframework.stereotype.Component; + +@Component +public class JsBundle { + + public String compile() { + return """ + document.addEventListener('DOMContentLoaded', function() { + // Nav toggle + var toggle = document.querySelector('.nav-toggle'); + var nav = document.querySelector('nav'); + if (toggle && nav) { + toggle.addEventListener('click', function() { + nav.classList.toggle('open'); + }); + } + + // Smooth scroll + document.querySelectorAll('a[href^="#"]').forEach(function(anchor) { + anchor.addEventListener('click', function(e) { + var target = document.querySelector(this.getAttribute('href')); + if (target) { + e.preventDefault(); + target.scrollIntoView({ behavior: 'smooth' }); + } + }); + }); + + // Lightbox + var lightboxOverlay = null; + document.querySelectorAll('.lightbox-trigger').forEach(function(img) { + img.addEventListener('click', function() { + if (!lightboxOverlay) { + lightboxOverlay = document.createElement('div'); + lightboxOverlay.className = 'lightbox-overlay'; + lightboxOverlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;align-items:center;justify-content:center;z-index:9999;cursor:pointer;'; + lightboxOverlay.addEventListener('click', function() { + lightboxOverlay.style.display = 'none'; + }); + document.body.appendChild(lightboxOverlay); + } + var fullImg = lightboxOverlay.querySelector('img'); + if (!fullImg) { + fullImg = document.createElement('img'); + fullImg.style.cssText = 'max-width:90%;max-height:90%;object-fit:contain;'; + lightboxOverlay.appendChild(fullImg); + } + fullImg.src = this.src; + lightboxOverlay.style.display = 'flex'; + }); + }); + + // Contact form handler + document.querySelectorAll('form[data-static]').forEach(function(form) { + form.addEventListener('submit', function(e) { + e.preventDefault(); + var btn = form.querySelector('.form-submit'); + if (btn) { + var original = btn.textContent; + btn.textContent = 'Message sent!'; + btn.disabled = true; + setTimeout(function() { + btn.textContent = original; + btn.disabled = false; + }, 3000); + } + }); + }); + }); + """; + } +} diff --git a/backend/src/main/java/com/krrishg/engine/StyleCompiler.java b/backend/src/main/java/com/krrishg/engine/StyleCompiler.java new file mode 100644 index 0000000..3c2e632 --- /dev/null +++ b/backend/src/main/java/com/krrishg/engine/StyleCompiler.java @@ -0,0 +1,31 @@ +package com.krrishg.engine; + +import org.springframework.stereotype.Component; + +@Component +public class StyleCompiler { + + public String compile() { + return """ + * { margin: 0; padding: 0; box-sizing: border-box; } + body { + font-family: var(--font-body, 'Inter', sans-serif); + color: var(--color-text, #1a1a1a); + background: var(--color-background, #ffffff); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + } + .container { max-width: var(--spacing-containerWidth, 1100px); margin: 0 auto; padding: 0 1.5rem; } + section { padding: var(--spacing-sectionPadding, 3rem) 0; } + a { color: inherit; text-decoration: none; } + img { max-width: 100%; height: auto; } + @media (max-width: 768px) { + .container { padding: 0 1rem; } + section { padding: 2rem 0; } + } + @media (max-width: 480px) { + section { padding: 1.5rem 0; } + } + """; + } +} diff --git a/backend/src/main/java/com/indie/model/AuthProvider.java b/backend/src/main/java/com/krrishg/model/AuthProvider.java similarity index 70% rename from backend/src/main/java/com/indie/model/AuthProvider.java rename to backend/src/main/java/com/krrishg/model/AuthProvider.java index 98ac036..1660a3c 100644 --- a/backend/src/main/java/com/indie/model/AuthProvider.java +++ b/backend/src/main/java/com/krrishg/model/AuthProvider.java @@ -1,4 +1,4 @@ -package com.indie.model; +package com.krrishg.model; public enum AuthProvider { LOCAL, GOOGLE, GITHUB, OPENID diff --git a/backend/src/main/java/com/indie/model/GenerationVersion.java b/backend/src/main/java/com/krrishg/model/GenerationVersion.java similarity index 97% rename from backend/src/main/java/com/indie/model/GenerationVersion.java rename to backend/src/main/java/com/krrishg/model/GenerationVersion.java index e4ada07..e3953dc 100644 --- a/backend/src/main/java/com/indie/model/GenerationVersion.java +++ b/backend/src/main/java/com/krrishg/model/GenerationVersion.java @@ -1,4 +1,4 @@ -package com.indie.model; +package com.krrishg.model; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/backend/src/main/java/com/krrishg/model/GitRemote.java b/backend/src/main/java/com/krrishg/model/GitRemote.java new file mode 100644 index 0000000..1e61e6c --- /dev/null +++ b/backend/src/main/java/com/krrishg/model/GitRemote.java @@ -0,0 +1,63 @@ +package com.krrishg.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "git_remotes") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class GitRemote { + + @Id + private UUID id; + + @Column(nullable = false) + private UUID siteId; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private GitProvider provider; + + @Column(nullable = false) + private String remoteUrl; + + @Column(nullable = false) + private String encryptedToken; + + @Column(nullable = false) + @Builder.Default + private String defaultBranch = "main"; + + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + void onCreate() { + if (id == null) { + id = UUID.randomUUID(); + } + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + void onUpdate() { + updatedAt = LocalDateTime.now(); + } + + public enum GitProvider { + GITHUB, GITLAB + } +} diff --git a/backend/src/main/java/com/indie/model/Site.java b/backend/src/main/java/com/krrishg/model/Site.java similarity index 95% rename from backend/src/main/java/com/indie/model/Site.java rename to backend/src/main/java/com/krrishg/model/Site.java index dc9b955..d1ef3a3 100644 --- a/backend/src/main/java/com/indie/model/Site.java +++ b/backend/src/main/java/com/krrishg/model/Site.java @@ -1,4 +1,4 @@ -package com.indie.model; +package com.krrishg.model; import jakarta.persistence.*; import lombok.AllArgsConstructor; @@ -32,6 +32,8 @@ public class Site { @Column(unique = true) private String subdomain; + private String publishedUrl; + @Enumerated(EnumType.STRING) @Column(nullable = false) @Builder.Default diff --git a/backend/src/main/java/com/indie/model/SiteStatus.java b/backend/src/main/java/com/krrishg/model/SiteStatus.java similarity index 64% rename from backend/src/main/java/com/indie/model/SiteStatus.java rename to backend/src/main/java/com/krrishg/model/SiteStatus.java index 3fabbdb..945ea44 100644 --- a/backend/src/main/java/com/indie/model/SiteStatus.java +++ b/backend/src/main/java/com/krrishg/model/SiteStatus.java @@ -1,4 +1,4 @@ -package com.indie.model; +package com.krrishg.model; public enum SiteStatus { DRAFT, PUBLISHED diff --git a/backend/src/main/java/com/indie/model/User.java b/backend/src/main/java/com/krrishg/model/User.java similarity index 97% rename from backend/src/main/java/com/indie/model/User.java rename to backend/src/main/java/com/krrishg/model/User.java index 6307b17..df7efb6 100644 --- a/backend/src/main/java/com/indie/model/User.java +++ b/backend/src/main/java/com/krrishg/model/User.java @@ -1,4 +1,4 @@ -package com.indie.model; +package com.krrishg.model; import jakarta.persistence.*; import lombok.AllArgsConstructor; diff --git a/backend/src/main/java/com/krrishg/repository/GitRemoteRepository.java b/backend/src/main/java/com/krrishg/repository/GitRemoteRepository.java new file mode 100644 index 0000000..e83cb1e --- /dev/null +++ b/backend/src/main/java/com/krrishg/repository/GitRemoteRepository.java @@ -0,0 +1,16 @@ +package com.krrishg.repository; + +import com.krrishg.model.GitRemote; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface GitRemoteRepository extends JpaRepository { + + Optional findBySiteId(UUID siteId); + + boolean existsBySiteId(UUID siteId); + + void deleteBySiteId(UUID siteId); +} diff --git a/backend/src/main/java/com/indie/repository/SiteRepository.java b/backend/src/main/java/com/krrishg/repository/SiteRepository.java similarity index 85% rename from backend/src/main/java/com/indie/repository/SiteRepository.java rename to backend/src/main/java/com/krrishg/repository/SiteRepository.java index 5906907..aa6b797 100644 --- a/backend/src/main/java/com/indie/repository/SiteRepository.java +++ b/backend/src/main/java/com/krrishg/repository/SiteRepository.java @@ -1,6 +1,6 @@ -package com.indie.repository; +package com.krrishg.repository; -import com.indie.model.Site; +import com.krrishg.model.Site; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; diff --git a/backend/src/main/java/com/indie/repository/UserRepository.java b/backend/src/main/java/com/krrishg/repository/UserRepository.java similarity index 78% rename from backend/src/main/java/com/indie/repository/UserRepository.java rename to backend/src/main/java/com/krrishg/repository/UserRepository.java index 9f6b1c3..47e5432 100644 --- a/backend/src/main/java/com/indie/repository/UserRepository.java +++ b/backend/src/main/java/com/krrishg/repository/UserRepository.java @@ -1,7 +1,7 @@ -package com.indie.repository; +package com.krrishg.repository; -import com.indie.model.AuthProvider; -import com.indie.model.User; +import com.krrishg.model.AuthProvider; +import com.krrishg.model.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; diff --git a/backend/src/main/java/com/indie/service/AuthService.java b/backend/src/main/java/com/krrishg/service/AuthService.java similarity index 90% rename from backend/src/main/java/com/indie/service/AuthService.java rename to backend/src/main/java/com/krrishg/service/AuthService.java index 52e1eb4..d7fe19a 100644 --- a/backend/src/main/java/com/indie/service/AuthService.java +++ b/backend/src/main/java/com/krrishg/service/AuthService.java @@ -1,13 +1,13 @@ -package com.indie.service; +package com.krrishg.service; -import com.indie.config.TokenService; -import com.indie.dto.AuthResponse; -import com.indie.dto.LoginRequest; -import com.indie.dto.RefreshTokenRequest; -import com.indie.dto.RegisterRequest; -import com.indie.model.AuthProvider; -import com.indie.model.User; -import com.indie.repository.UserRepository; +import com.krrishg.config.TokenService; +import com.krrishg.dto.AuthResponse; +import com.krrishg.dto.LoginRequest; +import com.krrishg.dto.RefreshTokenRequest; +import com.krrishg.dto.RegisterRequest; +import com.krrishg.model.AuthProvider; +import com.krrishg.model.User; +import com.krrishg.repository.UserRepository; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; diff --git a/backend/src/main/java/com/indie/service/CustomOAuth2User.java b/backend/src/main/java/com/krrishg/service/CustomOAuth2User.java similarity index 94% rename from backend/src/main/java/com/indie/service/CustomOAuth2User.java rename to backend/src/main/java/com/krrishg/service/CustomOAuth2User.java index 3aac90b..4bac12a 100644 --- a/backend/src/main/java/com/indie/service/CustomOAuth2User.java +++ b/backend/src/main/java/com/krrishg/service/CustomOAuth2User.java @@ -1,6 +1,6 @@ -package com.indie.service; +package com.krrishg.service; -import com.indie.model.User; +import com.krrishg.model.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.core.user.OAuth2User; diff --git a/backend/src/main/java/com/krrishg/service/DeploymentService.java b/backend/src/main/java/com/krrishg/service/DeploymentService.java new file mode 100644 index 0000000..c377f91 --- /dev/null +++ b/backend/src/main/java/com/krrishg/service/DeploymentService.java @@ -0,0 +1,85 @@ +package com.krrishg.service; + +import com.krrishg.dto.SiteStructure; +import com.krrishg.model.GitRemote; +import com.krrishg.model.Site; +import com.krrishg.model.SiteStatus; +import com.krrishg.repository.GitRemoteRepository; +import com.krrishg.repository.SiteRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +public class DeploymentService { + + private static final Logger log = LoggerFactory.getLogger(DeploymentService.class); + + private final RenderService renderService; + private final GitService gitService; + private final SiteRepository siteRepository; + private final LLMService llmService; + private final GitRemoteRepository gitRemoteRepository; + + public DeploymentService(RenderService renderService, + GitService gitService, + SiteRepository siteRepository, + LLMService llmService, + GitRemoteRepository gitRemoteRepository) { + this.renderService = renderService; + this.gitService = gitService; + this.siteRepository = siteRepository; + this.llmService = llmService; + this.gitRemoteRepository = gitRemoteRepository; + } + + public String publish(UUID siteId) { + Site site = siteRepository.findById(siteId) + .orElseThrow(() -> new IllegalArgumentException("Site not found")); + + SiteStructure structure = getLatestVersion(siteId); + if (structure == null) { + throw new IllegalStateException("No generation versions found for site " + siteId); + } + + GitRemote remote = gitRemoteRepository.findBySiteId(siteId).orElse(null); + if (remote == null) { + throw new IllegalStateException("No git remote configured. Connect a GitHub or GitLab remote first."); + } + + String url = gitService.deployToPages(siteId.toString(), remote, structure); + + site.setStatus(SiteStatus.PUBLISHED); + site.setPublishedAt(LocalDateTime.now()); + site.setPublishedUrl(url); + siteRepository.save(site); + + log.info("Published site {} to {} at {}", siteId, remote.getProvider(), url); + return url; + } + + public void unpublish(UUID siteId) { + Site site = siteRepository.findById(siteId) + .orElseThrow(() -> new IllegalArgumentException("Site not found")); + + gitService.removePages(siteId.toString()); + + site.setStatus(SiteStatus.DRAFT); + site.setPublishedAt(null); + site.setPublishedUrl(null); + siteRepository.save(site); + + log.info("Unpublished site {}", siteId); + } + + private SiteStructure getLatestVersion(UUID siteId) { + var versions = llmService.getVersions(siteId); + if (versions.isEmpty()) { + return null; + } + return llmService.restoreVersion(versions.get(0).getId()); + } +} diff --git a/backend/src/main/java/com/krrishg/service/DomainService.java b/backend/src/main/java/com/krrishg/service/DomainService.java new file mode 100644 index 0000000..2b2058d --- /dev/null +++ b/backend/src/main/java/com/krrishg/service/DomainService.java @@ -0,0 +1,60 @@ +package com.krrishg.service; + +import com.krrishg.model.Site; +import com.krrishg.repository.SiteRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class DomainService { + + private static final Logger log = LoggerFactory.getLogger(DomainService.class); + + private final SiteRepository siteRepository; + private final String siteDomain; + + public DomainService(SiteRepository siteRepository, + @Value("${indie.domain}") String siteDomain) { + this.siteRepository = siteRepository; + this.siteDomain = siteDomain; + } + + public Optional resolveSite(String hostname) { + String subdomain = extractSubdomain(hostname); + if (subdomain != null) { + return siteRepository.findBySubdomain(subdomain); + } + return Optional.empty(); + } + + private String extractSubdomain(String hostname) { + if (hostname == null || hostname.isBlank()) { + return null; + } + + String lower = hostname.toLowerCase(); + String suffix = "." + siteDomain; + if (lower.endsWith(suffix)) { + String subdomain = lower.substring(0, lower.indexOf(suffix)); + if (!subdomain.isEmpty() && !subdomain.contains(".")) { + return subdomain; + } + } + return null; + } + + public boolean isSubdomainAvailable(String subdomain) { + return !siteRepository.existsBySubdomain(subdomain); + } + + public String getSiteUrl(Site site) { + if (site.getSubdomain() != null) { + return "https://" + site.getSubdomain() + "." + siteDomain; + } + return null; + } +} diff --git a/backend/src/main/java/com/krrishg/service/ExportService.java b/backend/src/main/java/com/krrishg/service/ExportService.java new file mode 100644 index 0000000..2881b24 --- /dev/null +++ b/backend/src/main/java/com/krrishg/service/ExportService.java @@ -0,0 +1,44 @@ +package com.krrishg.service; + +import com.krrishg.dto.SiteStructure; +import com.krrishg.service.RenderService.SiteOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@Service +public class ExportService { + + private static final Logger log = LoggerFactory.getLogger(ExportService.class); + + private final RenderService renderService; + + public ExportService(RenderService renderService) { + this.renderService = renderService; + } + + public byte[] exportSite(SiteStructure structure) throws IOException { + SiteOutput output = renderService.renderSite(structure); + Map files = output.asFileMap(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos)) { + for (Map.Entry entry : files.entrySet()) { + ZipEntry zipEntry = new ZipEntry(entry.getKey()); + zipEntry.setTime(System.currentTimeMillis()); + zos.putNextEntry(zipEntry); + zos.write(entry.getValue()); + zos.closeEntry(); + } + } + + log.info("Exported site with {} files ({} bytes)", files.size(), baos.size()); + return baos.toByteArray(); + } +} diff --git a/backend/src/main/java/com/krrishg/service/GitService.java b/backend/src/main/java/com/krrishg/service/GitService.java new file mode 100644 index 0000000..b7dfb77 --- /dev/null +++ b/backend/src/main/java/com/krrishg/service/GitService.java @@ -0,0 +1,369 @@ +package com.krrishg.service; + +import com.krrishg.config.GitConfig; +import com.krrishg.dto.SiteStructure; +import com.krrishg.model.GitRemote; +import com.krrishg.repository.GitRemoteRepository; +import com.krrishg.service.RenderService.SiteOutput; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.Status; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@Service +public class GitService { + + private static final Logger log = LoggerFactory.getLogger(GitService.class); + + private final GitConfig gitConfig; + private final RenderService renderService; + private final GitRemoteRepository gitRemoteRepository; + private final KeyManager keyManager; + + public GitService(GitConfig gitConfig, + RenderService renderService, + GitRemoteRepository gitRemoteRepository, + KeyManager keyManager) { + this.gitConfig = gitConfig; + this.renderService = renderService; + this.gitRemoteRepository = gitRemoteRepository; + this.keyManager = keyManager; + } + + public void initRepo(String siteId) { + Path repoPath = gitConfig.getSiteRepoPath(siteId); + if (Files.exists(repoPath.resolve(".git"))) { + return; + } + try { + Files.createDirectories(repoPath); + Git.init().setDirectory(repoPath.toFile()).call(); + log.info("Initialized git repo for site {}", siteId); + } catch (GitAPIException | IOException e) { + throw new RuntimeException("Failed to init git repo for site " + siteId, e); + } + } + + public Git openRepo(String siteId) { + Path repoPath = gitConfig.getSiteRepoPath(siteId); + if (!Files.exists(repoPath.resolve(".git"))) { + initRepo(siteId); + } + try { + return Git.open(repoPath.toFile()); + } catch (IOException e) { + throw new RuntimeException("Failed to open git repo for site " + siteId, e); + } + } + + public void writeSiteFiles(String siteId, SiteStructure structure) { + Path repoPath = gitConfig.getSiteRepoPath(siteId); + + try { + SiteOutput output = renderService.renderSite(structure); + Map files = output.asFileMap(); + for (Map.Entry entry : files.entrySet()) { + Path filePath = repoPath.resolve(entry.getKey()); + Files.createDirectories(filePath.getParent()); + Files.write(filePath, entry.getValue()); + } + } catch (IOException e) { + throw new RuntimeException("Failed to write site files for site " + siteId, e); + } + } + + public RevCommit commit(String siteId, String message) { + try (Git git = openRepo(siteId)) { + git.add().addFilepattern(".").call(); + Status status = git.status().call(); + if (status.getUncommittedChanges().isEmpty() && status.getAdded().isEmpty()) { + log.info("No changes to commit for site {}", siteId); + return null; + } + + PersonIdent author = new PersonIdent( + gitConfig.getAuthorName(), gitConfig.getAuthorEmail()); + return git.commit() + .setAuthor(author) + .setCommitter(author) + .setMessage(message) + .call(); + } catch (Exception e) { + throw new RuntimeException("Failed to commit for site " + siteId, e); + } + } + + public void commitAndPush(String siteId, String message) { + RevCommit commit = commit(siteId, message); + if (commit == null) { + return; + } + + GitRemote remote = gitRemoteRepository.findBySiteId(UUID.fromString(siteId)) + .orElse(null); + if (remote == null) { + log.info("No remote configured for site {}, skipping push", siteId); + return; + } + + try (Git git = openRepo(siteId)) { + git.push() + .setRemote(remote.getRemoteUrl()) + .setCredentialsProvider( + new UsernamePasswordCredentialsProvider("token", remote.getEncryptedToken())) + .call(); + log.info("Pushed site {} to remote {}", siteId, remote.getProvider()); + } catch (Exception e) { + throw new RuntimeException("Failed to push for site " + siteId, e); + } + } + + public List getLog(String siteId, int maxCount) { + try (Git git = openRepo(siteId)) { + ObjectId head = git.getRepository().resolve("HEAD"); + if (head == null) { + return Collections.emptyList(); + } + List commits = new ArrayList<>(); + for (RevCommit rev : git.log().setMaxCount(maxCount).call()) { + commits.add(new CommitInfo( + rev.getName(), + rev.getShortMessage(), + rev.getAuthorIdent().getWhen().toInstant().toString(), + rev.getAuthorIdent().getName() + )); + } + return commits; + } catch (GitAPIException | IOException e) { + throw new RuntimeException("Failed to get log for site " + siteId, e); + } + } + + public void rollback(String siteId, String commitHash) { + try (Git git = openRepo(siteId)) { + ObjectId commitId = ObjectId.fromString(commitHash); + git.checkout().setName(commitHash).call(); + git.branchCreate().setName("restore-" + commitHash.substring(0, 7)).call(); + log.info("Rolled back site {} to commit {}", siteId, commitHash); + } catch (Exception e) { + throw new RuntimeException("Failed to rollback site " + siteId + " to " + commitHash, e); + } + } + + public byte[] exportRepo(String siteId) { + Path repoPath = gitConfig.getSiteRepoPath(siteId); + if (!Files.exists(repoPath)) { + throw new IllegalArgumentException("No repo found for site " + siteId); + } + + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos)) { + Files.walk(repoPath) + .filter(Files::isRegularFile) + .forEach(path -> { + String entryName = repoPath.relativize(path).toString(); + try { + zos.putNextEntry(new ZipEntry(entryName)); + zos.write(Files.readAllBytes(path)); + zos.closeEntry(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + return baos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Failed to export repo for site " + siteId, e); + } + } + + public record CommitInfo(String hash, String message, String timestamp, String author) {} + + private String normalizeHttpsUrl(String url) { + if (url.startsWith("https://")) { + return url; + } + String domain = null; + if (url.contains("github.com")) domain = "github.com"; + else if (url.contains("gitlab.com")) domain = "gitlab.com"; + if (domain != null && url.contains(":")) { + String path = url.substring(url.lastIndexOf(":") + 1); + if (path.endsWith(".git")) path = path.substring(0, path.length() - 4); + return "https://" + domain + "/" + path + ".git"; + } + return url; + } + + public String deployToPages(String siteId, GitRemote remote, SiteStructure structure) { + try { + SiteOutput output = renderService.renderSite(structure); + Map files = output.asFileMap(); + + String httpsUrl = normalizeHttpsUrl(remote.getRemoteUrl()); + String authUrl = httpsUrl.replace("https://", + "https://git:" + remote.getEncryptedToken() + "@"); + + boolean isGitLab = remote.getRemoteUrl().contains("gitlab.com"); + String branch = isGitLab ? "gl-pages" : "gh-pages"; + + Path tempDir = Files.createTempDirectory("indie-pages-" + siteId); + try { + Path contentRoot = isGitLab ? tempDir.resolve("public") : tempDir; + Files.createDirectories(contentRoot); + + for (Map.Entry entry : files.entrySet()) { + Path filePath = contentRoot.resolve(entry.getKey()); + Files.createDirectories(filePath.getParent()); + Files.write(filePath, entry.getValue()); + } + + if (isGitLab) { + Files.writeString(tempDir.resolve(".gitlab-ci.yml"), + "pages:\n" + + " stage: deploy\n" + + " script:\n" + + " - echo \"Deploying GitLab Pages\"\n" + + " artifacts:\n" + + " paths:\n" + + " - public\n" + + " rules:\n" + + " - if: '$CI_COMMIT_BRANCH == \"gl-pages\"'\n"); + } else { + Files.writeString(tempDir.resolve(".nojekyll"), ""); + } + pushOrphanBranch(tempDir, authUrl, branch); + + log.info("Pushed site {} to {} branch {}", siteId, remote.getRemoteUrl(), branch); + return buildPagesUrl(remote); + } finally { + deleteDirectory(tempDir); + } + } catch (Exception e) { + throw new RuntimeException("Failed to deploy site " + siteId + " to " + remote.getProvider() + " Pages", e); + } + } + + + + private void pushOrphanBranch(Path tempDir, String authUrl, String branch) throws Exception { + String authorConfig = "-c user.name='" + gitConfig.getAuthorName() + "' -c user.email='" + gitConfig.getAuthorEmail() + "'"; + runGit(tempDir, + "git init", + "git " + authorConfig + " commit --allow-empty -m 'Initial commit'", + "git push --force " + authUrl + " HEAD:main", + "git checkout --orphan " + branch, + "git add -A", + "git " + authorConfig + " commit -m 'Deploy site'", + "git push --force " + authUrl + " HEAD:" + branch + ); + } + + private void runGit(Path workDir, String... commands) throws Exception { + for (String cmd : commands) { + ProcessBuilder pb = new ProcessBuilder("sh", "-c", cmd); + pb.directory(workDir.toFile()); + pb.redirectErrorStream(true); + Process process = pb.start(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + String processOutput = new String(process.getInputStream().readAllBytes()); + throw new RuntimeException("git command failed (exit " + exitCode + "): " + cmd + "\n" + processOutput); + } + } + } + + private String buildPagesUrl(GitRemote remote) { + String url = normalizeHttpsUrl(remote.getRemoteUrl()); + String owner = ""; + String repo = ""; + + if (url.contains("github.com")) { + String path = url.substring(url.indexOf("github.com") + "github.com".length() + 1); + if (path.endsWith(".git")) path = path.substring(0, path.length() - 4); + if (path.endsWith("/")) path = path.substring(0, path.length() - 1); + String[] parts = path.split("/", 2); + if (parts.length == 2) { + owner = parts[0]; + repo = parts[1]; + } + return "https://" + owner + ".github.io/" + repo + "/"; + } else if (url.contains("gitlab.com")) { + String path = url.substring(url.indexOf("gitlab.com") + "gitlab.com".length() + 1); + if (path.endsWith(".git")) path = path.substring(0, path.length() - 4); + if (path.endsWith("/")) path = path.substring(0, path.length() - 1); + String[] parts = path.split("/", 2); + if (parts.length == 2) { + owner = parts[0]; + repo = parts[1]; + } + return "https://" + owner + ".gitlab.io/" + repo + "/"; + } + + return url; + } + + public void removePages(String siteId) { + GitRemote remote = gitRemoteRepository.findBySiteId(UUID.fromString(siteId)) + .orElse(null); + if (remote == null) { + log.info("No remote configured for site {}, skipping remove", siteId); + return; + } + + try { + String httpsUrl = normalizeHttpsUrl(remote.getRemoteUrl()); + String authUrl = httpsUrl.replace("https://", + "https://git:" + remote.getEncryptedToken() + "@"); + + boolean isGitLab = remote.getRemoteUrl().contains("gitlab.com"); + String branch = isGitLab ? "gl-pages" : "gh-pages"; + + Path tempDir = Files.createTempDirectory("indie-pages-remove-" + siteId); + try { + String authorConfig = "-c user.name='" + gitConfig.getAuthorName() + "' -c user.email='" + gitConfig.getAuthorEmail() + "'"; + runGit(tempDir, + "git init", + "git " + authorConfig + " commit --allow-empty -m 'Remove site'", + "git checkout --orphan " + branch, + "git " + authorConfig + " commit --allow-empty -m 'Remove site'", + "git push --force " + authUrl + " HEAD:" + branch + ); + log.info("Removed pages for site {} from {} branch {}", siteId, remote.getProvider(), branch); + } finally { + deleteDirectory(tempDir); + } + } catch (Exception e) { + throw new RuntimeException("Failed to remove pages for site " + siteId, e); + } + } + + private void deleteDirectory(Path path) throws IOException { + if (Files.exists(path)) { + try (var walk = Files.walk(path)) { + walk.sorted(java.util.Comparator.reverseOrder()) + .forEach(p -> { + try { + Files.deleteIfExists(p); + } catch (IOException e) { + log.warn("Failed to delete {}", p); + } + }); + } + } + } +} diff --git a/backend/src/main/java/com/krrishg/service/KeyManager.java b/backend/src/main/java/com/krrishg/service/KeyManager.java new file mode 100644 index 0000000..ed53bad --- /dev/null +++ b/backend/src/main/java/com/krrishg/service/KeyManager.java @@ -0,0 +1,91 @@ +package com.krrishg.service; + +import com.krrishg.config.GitConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; + +@Component +public class KeyManager { + + private static final Logger log = LoggerFactory.getLogger(KeyManager.class); + + private final GitConfig gitConfig; + + public KeyManager(GitConfig gitConfig) { + this.gitConfig = gitConfig; + } + + public Path getSshDir(String siteId) { + return gitConfig.getSiteRepoPath(siteId).resolve(".ssh"); + } + + public boolean hasKeyPair(String siteId) { + Path sshDir = getSshDir(siteId); + return Files.exists(sshDir.resolve("id_ed25519")); + } + + public void generateKeyPair(String siteId) { + Path sshDir = getSshDir(siteId); + try { + Files.createDirectories(sshDir); + + ProcessBuilder pb = new ProcessBuilder( + "ssh-keygen", "-t", "ed25519", + "-f", sshDir.resolve("id_ed25519").toString(), + "-N", "", "-q" + ); + pb.redirectErrorStream(true); + Process process = pb.start(); + int exitCode = process.waitFor(); + + if (exitCode != 0) { + throw new IOException("ssh-keygen failed with exit code " + exitCode); + } + + Files.setPosixFilePermissions( + sshDir.resolve("id_ed25519"), + PosixFilePermissions.fromString("rw-------")); + + log.info("Generated SSH key pair for site {}", siteId); + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Failed to generate SSH key pair for site " + siteId, e); + } + } + + public String getPublicKey(String siteId) { + Path pubKeyPath = getSshDir(siteId).resolve("id_ed25519.pub"); + if (!Files.exists(pubKeyPath)) { + generateKeyPair(siteId); + } + try { + return Files.readString(pubKeyPath).strip(); + } catch (IOException e) { + throw new RuntimeException("Failed to read public key for site " + siteId, e); + } + } + + public Path getPrivateKeyPath(String siteId) { + Path keyPath = getSshDir(siteId).resolve("id_ed25519"); + if (!Files.exists(keyPath)) { + generateKeyPair(siteId); + } + return keyPath; + } + + public void deleteKeys(String siteId) { + Path sshDir = getSshDir(siteId); + try { + Files.deleteIfExists(sshDir.resolve("id_ed25519")); + Files.deleteIfExists(sshDir.resolve("id_ed25519.pub")); + Files.deleteIfExists(sshDir); + } catch (IOException e) { + log.warn("Failed to delete SSH keys for site {}: {}", siteId, e.getMessage()); + } + } +} diff --git a/backend/src/main/java/com/indie/service/LLMResponseParser.java b/backend/src/main/java/com/krrishg/service/LLMResponseParser.java similarity index 98% rename from backend/src/main/java/com/indie/service/LLMResponseParser.java rename to backend/src/main/java/com/krrishg/service/LLMResponseParser.java index 8e4ca47..00c6966 100644 --- a/backend/src/main/java/com/indie/service/LLMResponseParser.java +++ b/backend/src/main/java/com/krrishg/service/LLMResponseParser.java @@ -1,10 +1,10 @@ -package com.indie.service; +package com.krrishg.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.indie.dto.SiteStructure; +import com.krrishg.dto.SiteStructure; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; diff --git a/backend/src/main/java/com/indie/service/LLMService.java b/backend/src/main/java/com/krrishg/service/LLMService.java similarity index 96% rename from backend/src/main/java/com/indie/service/LLMService.java rename to backend/src/main/java/com/krrishg/service/LLMService.java index e82686c..5c41656 100644 --- a/backend/src/main/java/com/indie/service/LLMService.java +++ b/backend/src/main/java/com/krrishg/service/LLMService.java @@ -1,12 +1,12 @@ -package com.indie.service; +package com.krrishg.service; -import com.indie.dto.LLMOptions; -import com.indie.dto.LLMResult; -import com.indie.dto.Reference; -import com.indie.dto.SiteStructure; -import com.indie.model.GenerationVersion; -import com.indie.service.llm.LLMFactory; -import com.indie.service.llm.LLMProvider; +import com.krrishg.dto.LLMOptions; +import com.krrishg.dto.LLMResult; +import com.krrishg.dto.Reference; +import com.krrishg.dto.SiteStructure; +import com.krrishg.model.GenerationVersion; +import com.krrishg.service.llm.LLMFactory; +import com.krrishg.service.llm.LLMProvider; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/backend/src/main/java/com/krrishg/service/MediaService.java b/backend/src/main/java/com/krrishg/service/MediaService.java new file mode 100644 index 0000000..5092ed3 --- /dev/null +++ b/backend/src/main/java/com/krrishg/service/MediaService.java @@ -0,0 +1,88 @@ +package com.krrishg.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +@Service +public class MediaService { + + private static final Logger log = LoggerFactory.getLogger(MediaService.class); + private static final Set ALLOWED_TYPES = Set.of( + "image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml", + "application/pdf" + ); + private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB + + private final S3Client s3Client; + + @Value("${indie.storage.bucket}") + private String bucket; + + @Value("${indie.storage.public-url-base}") + private String publicUrlBase; + + public MediaService(S3Client s3Client) { + this.s3Client = s3Client; + } + + public MediaUploadResult upload(MultipartFile file, UUID siteId) { + validateFile(file); + + String originalFilename = file.getOriginalFilename(); + String extension = ""; + if (originalFilename != null && originalFilename.contains(".")) { + extension = originalFilename.substring(originalFilename.lastIndexOf(".")); + } + String key = "media/" + siteId + "/" + UUID.randomUUID() + extension; + String contentType = file.getContentType(); + + try { + PutObjectRequest request = PutObjectRequest.builder() + .bucket(bucket) + .key(key) + .contentType(contentType) + .build(); + + s3Client.putObject(request, RequestBody.fromBytes(file.getBytes())); + + String publicUrl = publicUrlBase + "/" + key; + + log.info("Uploaded media {} ({}) for site {}", key, contentType, siteId); + + return new MediaUploadResult(key, publicUrl, originalFilename, contentType, file.getSize()); + } catch (IOException e) { + throw new RuntimeException("Failed to upload file", e); + } + } + + public String getPublicUrl(String key) { + return publicUrlBase + "/" + key; + } + + private void validateFile(MultipartFile file) { + if (file.isEmpty()) { + throw new IllegalArgumentException("File is empty"); + } + if (file.getSize() > MAX_FILE_SIZE) { + throw new IllegalArgumentException("File size exceeds maximum of 10 MB"); + } + String contentType = file.getContentType(); + if (contentType == null || !ALLOWED_TYPES.contains(contentType)) { + throw new IllegalArgumentException("File type not allowed: " + contentType); + } + } + + public record MediaUploadResult(String key, String url, String originalFilename, + String contentType, long size) {} +} diff --git a/backend/src/main/java/com/indie/service/OAuth2SuccessHandler.java b/backend/src/main/java/com/krrishg/service/OAuth2SuccessHandler.java similarity index 93% rename from backend/src/main/java/com/indie/service/OAuth2SuccessHandler.java rename to backend/src/main/java/com/krrishg/service/OAuth2SuccessHandler.java index 07255cb..7e61b5b 100644 --- a/backend/src/main/java/com/indie/service/OAuth2SuccessHandler.java +++ b/backend/src/main/java/com/krrishg/service/OAuth2SuccessHandler.java @@ -1,7 +1,7 @@ -package com.indie.service; +package com.krrishg.service; -import com.indie.config.JwtConfig; -import com.indie.model.User; +import com.krrishg.config.JwtConfig; +import com.krrishg.model.User; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/backend/src/main/java/com/indie/service/OAuth2UserService.java b/backend/src/main/java/com/krrishg/service/OAuth2UserService.java similarity index 96% rename from backend/src/main/java/com/indie/service/OAuth2UserService.java rename to backend/src/main/java/com/krrishg/service/OAuth2UserService.java index 249123c..416e38d 100644 --- a/backend/src/main/java/com/indie/service/OAuth2UserService.java +++ b/backend/src/main/java/com/krrishg/service/OAuth2UserService.java @@ -1,8 +1,8 @@ -package com.indie.service; +package com.krrishg.service; -import com.indie.model.AuthProvider; -import com.indie.model.User; -import com.indie.repository.UserRepository; +import com.krrishg.model.AuthProvider; +import com.krrishg.model.User; +import com.krrishg.repository.UserRepository; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; diff --git a/backend/src/main/java/com/indie/service/ReferenceService.java b/backend/src/main/java/com/krrishg/service/ReferenceService.java similarity index 97% rename from backend/src/main/java/com/indie/service/ReferenceService.java rename to backend/src/main/java/com/krrishg/service/ReferenceService.java index 7791810..8bf0d9d 100644 --- a/backend/src/main/java/com/indie/service/ReferenceService.java +++ b/backend/src/main/java/com/krrishg/service/ReferenceService.java @@ -1,6 +1,6 @@ -package com.indie.service; +package com.krrishg.service; -import com.indie.dto.Reference; +import com.krrishg.dto.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; diff --git a/backend/src/main/java/com/krrishg/service/RenderService.java b/backend/src/main/java/com/krrishg/service/RenderService.java new file mode 100644 index 0000000..6d79f5e --- /dev/null +++ b/backend/src/main/java/com/krrishg/service/RenderService.java @@ -0,0 +1,347 @@ +package com.krrishg.service; + +import com.krrishg.dto.SiteStructure; +import com.krrishg.engine.JsBundle; +import com.krrishg.engine.StyleCompiler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class RenderService { + + private static final Logger log = LoggerFactory.getLogger(RenderService.class); + + private final StyleCompiler styleCompiler; + private final JsBundle jsBundle; + + public RenderService(StyleCompiler styleCompiler, JsBundle jsBundle) { + this.styleCompiler = styleCompiler; + this.jsBundle = jsBundle; + } + + public SiteOutput renderSite(SiteStructure structure) { + String css = styleCompiler.compile(); + String js = jsBundle.compile(); + Map pages = new LinkedHashMap<>(); + + if (structure.getPages() != null) { + for (SiteStructure.PageStructure page : structure.getPages()) { + String html = renderPage(page, structure.getTheme()); + pages.put(page.getSlug(), html); + } + } + + return new SiteOutput(css, js, pages); + } + + private String renderPage(SiteStructure.PageStructure page, SiteStructure.ThemeConfig theme) { + StringBuilder cssVars = new StringBuilder(); + if (theme != null) { + if (theme.getColors() != null) { + for (Map.Entry e : theme.getColors().entrySet()) { + cssVars.append(" --color-").append(e.getKey()).append(": ").append(e.getValue()).append(";\n"); + } + } + if (theme.getFonts() != null) { + for (Map.Entry e : theme.getFonts().entrySet()) { + cssVars.append(" --font-").append(e.getKey()).append(": ").append(e.getValue()).append(";\n"); + } + } + if (theme.getSpacing() != null) { + for (Map.Entry e : theme.getSpacing().entrySet()) { + cssVars.append(" --spacing-").append(e.getKey()).append(": ").append(e.getValue()).append(";\n"); + } + } + } + + List
sections = buildSections(page.getComponents()); + Set lastIndices = sections.stream() + .map(s -> s.indices.get(s.indices.size() - 1)) + .collect(Collectors.toSet()); + + StringBuilder sectionsHtml = new StringBuilder(); + for (Section section : sections) { + String bgStyle = section.bg != null && !section.bg.isEmpty() + ? " style=\"background:" + section.bg + "\"" : ""; + sectionsHtml.append("\n
\n"); + for (int idx : section.indices) { + SiteStructure.ComponentStructure comp = page.getComponents().get(idx); + sectionsHtml.append(" ").append(renderComponent(comp, idx)).append("\n"); + } + sectionsHtml.append("
\n
\n"); + } + + StringBuilder compStyles = new StringBuilder(); + for (int i = 0; i < page.getComponents().size(); i++) { + SiteStructure.ComponentStructure c = page.getComponents().get(i); + if (c.getStyles() == null || c.getStyles().isEmpty()) continue; + List> entries = c.getStyles().entrySet().stream() + .filter(e -> !"backgroundColor".equals(e.getKey())) + .collect(Collectors.toList()); + if (lastIndices.contains(i)) { + entries = entries.stream() + .filter(e -> !"marginBottom".equals(e.getKey())) + .collect(Collectors.toList()); + } + if (entries.isEmpty()) continue; + compStyles.append(".comp-").append(i).append(" {\n"); + for (Map.Entry e : entries) { + compStyles.append(" ").append(cssProperty(e.getKey())).append(": ").append(e.getValue()).append(";\n"); + } + compStyles.append("}\n"); + } + + String title = page.getSeoTitle() != null && !page.getSeoTitle().isEmpty() + ? page.getSeoTitle() : page.getTitle(); + + return "\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " " + esc(title) + "\n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + sectionsHtml.toString() + + "\n" + + ""; + } + + private String renderComponent(SiteStructure.ComponentStructure c, int index) { + Map config = c.getConfig() != null ? c.getConfig() : new HashMap<>(); + String type = (c.getType() != null ? c.getType() : "").toUpperCase(); + String cls = "comp-" + index; + + return switch (type) { + case "HEADING" -> renderHeading(config, cls); + case "TEXT" -> renderText(config, cls); + case "IMAGE" -> renderImage(config, cls); + case "BUTTON" -> renderButton(config, cls); + case "DIVIDER" -> renderDivider(config, cls); + case "GALLERY" -> renderGallery(config, cls); + case "VIDEO" -> renderVideo(config, cls); + case "CONTACT_FORM" -> renderContactForm(config, cls); + case "MAP" -> renderMap(config, cls); + case "CUSTOM_HTML" -> renderCustomHtml(config, cls); + default -> "
[" + esc(type) + "]
"; + }; + } + + private String renderHeading(Map config, String cls) { + String level = str(config.get("level"), "h2"); + String text = str(config.get("text"), "Heading"); + String tag = List.of("h1", "h2", "h3", "h4", "h5", "h6").contains(level) ? level : "h2"; + return "<" + tag + " class=\"" + cls + "\">" + esc(text) + ""; + } + + private String renderText(Map config, String cls) { + String content = str(config.get("content"), ""); + return "
" + content + "
"; + } + + private String renderImage(Map config, String cls) { + String src = str(config.get("src"), ""); + String alt = str(config.get("alt"), ""); + String caption = str(config.get("caption"), ""); + String linkTo = str(config.get("linkTo"), ""); + String img = "\"""; + String imgHtml = linkTo.isEmpty() ? img : "" + img + ""; + if (!caption.isEmpty()) { + return "
" + imgHtml + + "
" + + esc(caption) + "
"; + } + return imgHtml; + } + + private String renderButton(Map config, String cls) { + String text = str(config.get("text"), "Button"); + String link = str(config.get("link"), "#"); + String variant = str(config.get("variant"), "primary"); + String btnStyle = "display:inline-block;text-decoration:none;font-weight:500;font-size:1rem;padding:0.75rem 2rem;border-radius:8px;transition:all 0.2s;cursor:pointer;border:2px solid transparent"; + btnStyle += switch (variant) { + case "outline" -> ";background:transparent;color:var(--color-primary,#4f46e5);border-color:var(--color-primary,#4f46e5)"; + case "secondary" -> ";background:var(--color-secondary,#6366f1);color:#ffffff"; + default -> ";background:var(--color-primary,#4f46e5);color:#ffffff"; + }; + return "" + esc(text) + ""; + } + + private String renderDivider(Map config, String cls) { + String style = str(config.get("style"), "solid"); + String color = str(config.get("color"), "var(--color-text, #d1d5db)"); + return "
"; + } + + @SuppressWarnings("unchecked") + private String renderGallery(Map config, String cls) { + List images = (List) config.getOrDefault("images", List.of()); + int columns = Math.min(Math.max(toInt(config.get("columns"), 3), 1), 6); + if (images.isEmpty()) { + return "
Gallery (no images)
"; + } + StringBuilder sb = new StringBuilder(); + sb.append("
\n"); + for (String src : images) { + sb.append(" \n"); + } + sb.append("
"); + return sb.toString(); + } + + private String renderVideo(Map config, String cls) { + String src = str(config.get("src"), ""); + boolean controls = config.get("controls") != Boolean.FALSE; + boolean autoplay = config.get("autoplay") == Boolean.TRUE; + StringBuilder sb = new StringBuilder(); + sb.append(""); + return sb.toString(); + } + + @SuppressWarnings("unchecked") + private String renderContactForm(Map config, String cls) { + List fields = (List) config.getOrDefault("fields", List.of("name", "email", "message")); + String submitText = str(config.get("submitText"), "Send"); + StringBuilder sb = new StringBuilder(); + sb.append("
\n"); + for (String field : fields) { + sb.append("
\n"); + sb.append(" \n"); + if ("message".equals(field)) { + sb.append(" \n"); + } else { + String inputType = "email".equals(field) ? "email" : "text"; + sb.append(" \n"); + } + sb.append("
\n"); + } + sb.append(" \n"); + sb.append("
"); + return sb.toString(); + } + + private String renderMap(Map config, String cls) { + String address = str(config.get("address"), "New York, NY"); + return "
\n" + + " Map: " + esc(address) + "\n" + + "
"; + } + + private String renderCustomHtml(Map config, String cls) { + String html = str(config.get("html"), ""); + return "
" + html + "
"; + } + + private List
buildSections(List components) { + List
sections = new ArrayList<>(); + if (components == null) return sections; + for (int i = 0; i < components.size(); i++) { + SiteStructure.ComponentStructure c = components.get(i); + String bg = ""; + if (c.getStyles() != null && c.getStyles().get("backgroundColor") != null) { + bg = String.valueOf(c.getStyles().get("backgroundColor")); + } + if ((bg != null && !bg.isEmpty()) || i == 0) { + sections.add(new Section(bg, new ArrayList<>())); + } + sections.get(sections.size() - 1).indices.add(i); + } + return sections; + } + + private String cssProperty(String key) { + return key.replaceAll("([a-z])([A-Z])", "$1-$2").toLowerCase(); + } + + private String esc(String s) { + if (s == null) return ""; + return s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + private String capitalize(String s) { + if (s == null || s.isEmpty()) return s; + return Character.toUpperCase(s.charAt(0)) + s.substring(1); + } + + private String str(Object value, String defaultValue) { + return value != null ? String.valueOf(value) : defaultValue; + } + + private int toInt(Object value, int defaultValue) { + if (value instanceof Number n) return n.intValue(); + if (value instanceof String s) { + try { return Integer.parseInt(s); } catch (NumberFormatException e) { return defaultValue; } + } + return defaultValue; + } + + private record Section(String bg, List indices) {} + + public record SiteOutput(String css, String js, Map pages) { + public Map asFileMap() { + Map files = new LinkedHashMap<>(); + files.put("style.css", css.getBytes()); + files.put("script.js", js.getBytes()); + + for (Map.Entry entry : pages.entrySet()) { + String slug = entry.getKey(); + String html = entry.getValue(); + if (slug == null || slug.isEmpty() || "home".equals(slug)) { + files.put("index.html", html.getBytes()); + } else { + files.put(slug + "/index.html", html.getBytes()); + } + } + + files.put("404.html", generate404().getBytes()); + return files; + } + + private String generate404() { + return "\n" + + "\n" + + "" + + "404 - Page Not Found" + + "\n" + + "\n" + + "

404

The page you're looking for doesn't exist.

" + + "Go Home
\n" + + ""; + } + } +} diff --git a/backend/src/main/java/com/indie/service/llm/GeminiProvider.java b/backend/src/main/java/com/krrishg/service/llm/GeminiProvider.java similarity index 97% rename from backend/src/main/java/com/indie/service/llm/GeminiProvider.java rename to backend/src/main/java/com/krrishg/service/llm/GeminiProvider.java index 5f0ef28..fd06c9c 100644 --- a/backend/src/main/java/com/indie/service/llm/GeminiProvider.java +++ b/backend/src/main/java/com/krrishg/service/llm/GeminiProvider.java @@ -1,8 +1,8 @@ -package com.indie.service.llm; +package com.krrishg.service.llm; -import com.indie.dto.LLMResult; -import com.indie.dto.LLMOptions; -import com.indie.dto.Reference; +import com.krrishg.dto.LLMResult; +import com.krrishg.dto.LLMOptions; +import com.krrishg.dto.Reference; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.*; diff --git a/backend/src/main/java/com/indie/service/llm/LLMFactory.java b/backend/src/main/java/com/krrishg/service/llm/LLMFactory.java similarity index 98% rename from backend/src/main/java/com/indie/service/llm/LLMFactory.java rename to backend/src/main/java/com/krrishg/service/llm/LLMFactory.java index 4bb8724..05089ab 100644 --- a/backend/src/main/java/com/indie/service/llm/LLMFactory.java +++ b/backend/src/main/java/com/krrishg/service/llm/LLMFactory.java @@ -1,4 +1,4 @@ -package com.indie.service.llm; +package com.krrishg.service.llm; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; diff --git a/backend/src/main/java/com/indie/service/llm/LLMProvider.java b/backend/src/main/java/com/krrishg/service/llm/LLMProvider.java similarity index 58% rename from backend/src/main/java/com/indie/service/llm/LLMProvider.java rename to backend/src/main/java/com/krrishg/service/llm/LLMProvider.java index 871c896..5d8bc44 100644 --- a/backend/src/main/java/com/indie/service/llm/LLMProvider.java +++ b/backend/src/main/java/com/krrishg/service/llm/LLMProvider.java @@ -1,8 +1,8 @@ -package com.indie.service.llm; +package com.krrishg.service.llm; -import com.indie.dto.LLMResult; -import com.indie.dto.LLMOptions; -import com.indie.dto.Reference; +import com.krrishg.dto.LLMResult; +import com.krrishg.dto.LLMOptions; +import com.krrishg.dto.Reference; import java.util.List; diff --git a/backend/src/main/java/com/indie/service/llm/OpenAIProvider.java b/backend/src/main/java/com/krrishg/service/llm/OpenAIProvider.java similarity index 96% rename from backend/src/main/java/com/indie/service/llm/OpenAIProvider.java rename to backend/src/main/java/com/krrishg/service/llm/OpenAIProvider.java index 053e3e6..8d294e2 100644 --- a/backend/src/main/java/com/indie/service/llm/OpenAIProvider.java +++ b/backend/src/main/java/com/krrishg/service/llm/OpenAIProvider.java @@ -1,8 +1,8 @@ -package com.indie.service.llm; +package com.krrishg.service.llm; -import com.indie.dto.LLMResult; -import com.indie.dto.LLMOptions; -import com.indie.dto.Reference; +import com.krrishg.dto.LLMResult; +import com.krrishg.dto.LLMOptions; +import com.krrishg.dto.Reference; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.*; diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 3573ba5..f846f10 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -43,9 +43,18 @@ indie: repos-path: ${INDIE_GIT_REPOS_PATH:/home/krrish/git/sites/} author-name: ${INDIE_GIT_AUTHOR_NAME:Indie Platform} author-email: ${INDIE_GIT_AUTHOR_EMAIL:git@indie.local} + domain: ${INDIE_SITE_DOMAIN:indie.com} cors: allowed-origins: ${INDIE_CORS_ORIGINS:http://localhost:4200,http://localhost:3000} + storage: + endpoint: ${INDIE_STORAGE_ENDPOINT:http://localhost:9000} + region: ${INDIE_STORAGE_REGION:auto} + access-key: ${INDIE_STORAGE_ACCESS_KEY:indie} + secret-key: ${INDIE_STORAGE_SECRET_KEY:indie_dev} + bucket: ${INDIE_STORAGE_BUCKET:indie-sites} + public-url-base: ${INDIE_STORAGE_PUBLIC_URL:http://localhost:9000/indie-sites} + llm: active-provider: ${INDIE_LLM_ACTIVE_PROVIDER:gemini} fallback-provider: ${INDIE_LLM_FALLBACK_PROVIDER:openai} diff --git a/backend/src/test/java/com/indie/config/GlobalExceptionHandlerTest.java b/backend/src/test/java/com/krrishg/config/GlobalExceptionHandlerTest.java similarity index 98% rename from backend/src/test/java/com/indie/config/GlobalExceptionHandlerTest.java rename to backend/src/test/java/com/krrishg/config/GlobalExceptionHandlerTest.java index ddb4c5e..3ca343b 100644 --- a/backend/src/test/java/com/indie/config/GlobalExceptionHandlerTest.java +++ b/backend/src/test/java/com/krrishg/config/GlobalExceptionHandlerTest.java @@ -1,4 +1,4 @@ -package com.indie.config; +package com.krrishg.config; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; diff --git a/backend/src/test/java/com/indie/config/RateLimitConfigTest.java b/backend/src/test/java/com/krrishg/config/RateLimitConfigTest.java similarity index 98% rename from backend/src/test/java/com/indie/config/RateLimitConfigTest.java rename to backend/src/test/java/com/krrishg/config/RateLimitConfigTest.java index df7091a..8ab2fd9 100644 --- a/backend/src/test/java/com/indie/config/RateLimitConfigTest.java +++ b/backend/src/test/java/com/krrishg/config/RateLimitConfigTest.java @@ -1,4 +1,4 @@ -package com.indie.config; +package com.krrishg.config; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/indie/config/UserPrincipalTest.java b/backend/src/test/java/com/krrishg/config/UserPrincipalTest.java similarity index 97% rename from backend/src/test/java/com/indie/config/UserPrincipalTest.java rename to backend/src/test/java/com/krrishg/config/UserPrincipalTest.java index 5c453b4..118f4f6 100644 --- a/backend/src/test/java/com/indie/config/UserPrincipalTest.java +++ b/backend/src/test/java/com/krrishg/config/UserPrincipalTest.java @@ -1,4 +1,4 @@ -package com.indie.config; +package com.krrishg.config; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/indie/controller/AuthControllerTest.java b/backend/src/test/java/com/krrishg/controller/AuthControllerTest.java similarity index 92% rename from backend/src/test/java/com/indie/controller/AuthControllerTest.java rename to backend/src/test/java/com/krrishg/controller/AuthControllerTest.java index 4ec9f18..4d6c5d9 100644 --- a/backend/src/test/java/com/indie/controller/AuthControllerTest.java +++ b/backend/src/test/java/com/krrishg/controller/AuthControllerTest.java @@ -1,14 +1,14 @@ -package com.indie.controller; +package com.krrishg.controller; -import com.indie.config.GlobalExceptionHandler; -import com.indie.dto.AuthResponse; -import com.indie.dto.AuthResponse.UserInfo; -import com.indie.dto.LoginRequest; -import com.indie.dto.RefreshTokenRequest; -import com.indie.dto.RegisterRequest; -import com.indie.model.AuthProvider; -import com.indie.service.AuthService; -import com.indie.support.TestSecurityConfig; +import com.krrishg.config.GlobalExceptionHandler; +import com.krrishg.dto.AuthResponse; +import com.krrishg.dto.AuthResponse.UserInfo; +import com.krrishg.dto.LoginRequest; +import com.krrishg.dto.RefreshTokenRequest; +import com.krrishg.dto.RegisterRequest; +import com.krrishg.model.AuthProvider; +import com.krrishg.service.AuthService; +import com.krrishg.support.TestSecurityConfig; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/backend/src/test/java/com/indie/controller/LLMControllerTest.java b/backend/src/test/java/com/krrishg/controller/LLMControllerTest.java similarity index 97% rename from backend/src/test/java/com/indie/controller/LLMControllerTest.java rename to backend/src/test/java/com/krrishg/controller/LLMControllerTest.java index 533c721..e5a01e6 100644 --- a/backend/src/test/java/com/indie/controller/LLMControllerTest.java +++ b/backend/src/test/java/com/krrishg/controller/LLMControllerTest.java @@ -1,14 +1,14 @@ -package com.indie.controller; +package com.krrishg.controller; -import com.indie.config.GlobalExceptionHandler; -import com.indie.config.JwtAuthenticationToken; -import com.indie.config.RateLimitConfig; -import com.indie.config.UserPrincipal; -import com.indie.dto.Reference; -import com.indie.dto.SiteStructure; -import com.indie.model.GenerationVersion; -import com.indie.service.LLMService; -import com.indie.support.TestSecurityConfig; +import com.krrishg.config.GlobalExceptionHandler; +import com.krrishg.config.JwtAuthenticationToken; +import com.krrishg.config.RateLimitConfig; +import com.krrishg.config.UserPrincipal; +import com.krrishg.dto.Reference; +import com.krrishg.dto.SiteStructure; +import com.krrishg.model.GenerationVersion; +import com.krrishg.service.LLMService; +import com.krrishg.support.TestSecurityConfig; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/indie/controller/SiteControllerTest.java b/backend/src/test/java/com/krrishg/controller/SiteControllerTest.java similarity index 95% rename from backend/src/test/java/com/indie/controller/SiteControllerTest.java rename to backend/src/test/java/com/krrishg/controller/SiteControllerTest.java index b345091..0c8f7e7 100644 --- a/backend/src/test/java/com/indie/controller/SiteControllerTest.java +++ b/backend/src/test/java/com/krrishg/controller/SiteControllerTest.java @@ -1,13 +1,13 @@ -package com.indie.controller; +package com.krrishg.controller; -import com.indie.config.GlobalExceptionHandler; -import com.indie.config.JwtAuthenticationToken; -import com.indie.config.UserPrincipal; -import com.indie.dto.SiteRequest; -import com.indie.model.Site; -import com.indie.model.SiteStatus; -import com.indie.repository.SiteRepository; -import com.indie.support.TestSecurityConfig; +import com.krrishg.config.GlobalExceptionHandler; +import com.krrishg.config.JwtAuthenticationToken; +import com.krrishg.config.UserPrincipal; +import com.krrishg.dto.SiteRequest; +import com.krrishg.model.Site; +import com.krrishg.model.SiteStatus; +import com.krrishg.repository.SiteRepository; +import com.krrishg.support.TestSecurityConfig; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/indie/dto/SiteResponseTest.java b/backend/src/test/java/com/krrishg/dto/SiteResponseTest.java similarity index 94% rename from backend/src/test/java/com/indie/dto/SiteResponseTest.java rename to backend/src/test/java/com/krrishg/dto/SiteResponseTest.java index 5225662..9aee402 100644 --- a/backend/src/test/java/com/indie/dto/SiteResponseTest.java +++ b/backend/src/test/java/com/krrishg/dto/SiteResponseTest.java @@ -1,7 +1,7 @@ -package com.indie.dto; +package com.krrishg.dto; -import com.indie.model.Site; -import com.indie.model.SiteStatus; +import com.krrishg.model.Site; +import com.krrishg.model.SiteStatus; import org.junit.jupiter.api.Test; import java.time.LocalDateTime; diff --git a/backend/src/test/java/com/indie/model/SiteTest.java b/backend/src/test/java/com/krrishg/model/SiteTest.java similarity index 98% rename from backend/src/test/java/com/indie/model/SiteTest.java rename to backend/src/test/java/com/krrishg/model/SiteTest.java index 698ac82..fcc1dd9 100644 --- a/backend/src/test/java/com/indie/model/SiteTest.java +++ b/backend/src/test/java/com/krrishg/model/SiteTest.java @@ -1,4 +1,4 @@ -package com.indie.model; +package com.krrishg.model; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/indie/model/UserTest.java b/backend/src/test/java/com/krrishg/model/UserTest.java similarity index 98% rename from backend/src/test/java/com/indie/model/UserTest.java rename to backend/src/test/java/com/krrishg/model/UserTest.java index c441e40..efea788 100644 --- a/backend/src/test/java/com/indie/model/UserTest.java +++ b/backend/src/test/java/com/krrishg/model/UserTest.java @@ -1,4 +1,4 @@ -package com.indie.model; +package com.krrishg.model; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/indie/repository/SiteRepositoryTest.java b/backend/src/test/java/com/krrishg/repository/SiteRepositoryTest.java similarity index 96% rename from backend/src/test/java/com/indie/repository/SiteRepositoryTest.java rename to backend/src/test/java/com/krrishg/repository/SiteRepositoryTest.java index 29548e6..903ab33 100644 --- a/backend/src/test/java/com/indie/repository/SiteRepositoryTest.java +++ b/backend/src/test/java/com/krrishg/repository/SiteRepositoryTest.java @@ -1,7 +1,7 @@ -package com.indie.repository; +package com.krrishg.repository; -import com.indie.model.Site; -import com.indie.model.SiteStatus; +import com.krrishg.model.Site; +import com.krrishg.model.SiteStatus; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; diff --git a/backend/src/test/java/com/indie/repository/UserRepositoryTest.java b/backend/src/test/java/com/krrishg/repository/UserRepositoryTest.java similarity index 96% rename from backend/src/test/java/com/indie/repository/UserRepositoryTest.java rename to backend/src/test/java/com/krrishg/repository/UserRepositoryTest.java index 87c9107..af3a129 100644 --- a/backend/src/test/java/com/indie/repository/UserRepositoryTest.java +++ b/backend/src/test/java/com/krrishg/repository/UserRepositoryTest.java @@ -1,7 +1,7 @@ -package com.indie.repository; +package com.krrishg.repository; -import com.indie.model.AuthProvider; -import com.indie.model.User; +import com.krrishg.model.AuthProvider; +import com.krrishg.model.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; diff --git a/backend/src/test/java/com/indie/service/AuthServiceTest.java b/backend/src/test/java/com/krrishg/service/AuthServiceTest.java similarity index 94% rename from backend/src/test/java/com/indie/service/AuthServiceTest.java rename to backend/src/test/java/com/krrishg/service/AuthServiceTest.java index 454a35a..97f4089 100644 --- a/backend/src/test/java/com/indie/service/AuthServiceTest.java +++ b/backend/src/test/java/com/krrishg/service/AuthServiceTest.java @@ -1,14 +1,14 @@ -package com.indie.service; +package com.krrishg.service; -import com.indie.dto.AuthResponse; -import com.indie.dto.LoginRequest; -import com.indie.dto.RefreshTokenRequest; -import com.indie.dto.RegisterRequest; -import com.indie.model.AuthProvider; -import com.indie.model.User; -import com.indie.support.FakeJwtConfig; -import com.indie.support.FakeUserRepository; -import com.indie.support.StubPasswordEncoder; +import com.krrishg.dto.AuthResponse; +import com.krrishg.dto.LoginRequest; +import com.krrishg.dto.RefreshTokenRequest; +import com.krrishg.dto.RegisterRequest; +import com.krrishg.model.AuthProvider; +import com.krrishg.model.User; +import com.krrishg.support.FakeJwtConfig; +import com.krrishg.support.FakeUserRepository; +import com.krrishg.support.StubPasswordEncoder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/indie/service/FakeReferenceService.java b/backend/src/test/java/com/krrishg/service/FakeReferenceService.java similarity index 94% rename from backend/src/test/java/com/indie/service/FakeReferenceService.java rename to backend/src/test/java/com/krrishg/service/FakeReferenceService.java index fa88d54..950564a 100644 --- a/backend/src/test/java/com/indie/service/FakeReferenceService.java +++ b/backend/src/test/java/com/krrishg/service/FakeReferenceService.java @@ -1,6 +1,6 @@ -package com.indie.service; +package com.krrishg.service; -import com.indie.dto.Reference; +import com.krrishg.dto.Reference; import java.util.List; diff --git a/backend/src/test/java/com/indie/service/LLMResponseParserTest.java b/backend/src/test/java/com/krrishg/service/LLMResponseParserTest.java similarity index 99% rename from backend/src/test/java/com/indie/service/LLMResponseParserTest.java rename to backend/src/test/java/com/krrishg/service/LLMResponseParserTest.java index 9db6a12..74104e6 100644 --- a/backend/src/test/java/com/indie/service/LLMResponseParserTest.java +++ b/backend/src/test/java/com/krrishg/service/LLMResponseParserTest.java @@ -1,7 +1,7 @@ -package com.indie.service; +package com.krrishg.service; import com.fasterxml.jackson.databind.ObjectMapper; -import com.indie.dto.SiteStructure; +import com.krrishg.dto.SiteStructure; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; diff --git a/backend/src/test/java/com/indie/service/LLMServiceTest.java b/backend/src/test/java/com/krrishg/service/LLMServiceTest.java similarity index 97% rename from backend/src/test/java/com/indie/service/LLMServiceTest.java rename to backend/src/test/java/com/krrishg/service/LLMServiceTest.java index e7e8439..f60b277 100644 --- a/backend/src/test/java/com/indie/service/LLMServiceTest.java +++ b/backend/src/test/java/com/krrishg/service/LLMServiceTest.java @@ -1,10 +1,10 @@ -package com.indie.service; +package com.krrishg.service; -import com.indie.dto.Reference; -import com.indie.dto.SiteStructure; -import com.indie.model.GenerationVersion; -import com.indie.service.llm.LLMFactory; -import com.indie.support.FakeLLMProvider; +import com.krrishg.dto.Reference; +import com.krrishg.dto.SiteStructure; +import com.krrishg.model.GenerationVersion; +import com.krrishg.service.llm.LLMFactory; +import com.krrishg.support.FakeLLMProvider; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -69,7 +69,8 @@ class LLMServiceTest { when(resourceLoader.getResource(anyString())).thenReturn(mockResource); llmService = new LLMService(llmFactory, responseParser, referenceService, - mongoTemplate, resourceLoader, "classpath:prompts/site-generator.txt"); + mongoTemplate, resourceLoader, "classpath:prompts/site-generator.txt", + "classpath:prompts/site-refiner.txt"); } @Test diff --git a/backend/src/test/java/com/indie/service/OAuth2UserServiceTest.java b/backend/src/test/java/com/krrishg/service/OAuth2UserServiceTest.java similarity index 95% rename from backend/src/test/java/com/indie/service/OAuth2UserServiceTest.java rename to backend/src/test/java/com/krrishg/service/OAuth2UserServiceTest.java index b429e8c..947f077 100644 --- a/backend/src/test/java/com/indie/service/OAuth2UserServiceTest.java +++ b/backend/src/test/java/com/krrishg/service/OAuth2UserServiceTest.java @@ -1,9 +1,9 @@ -package com.indie.service; +package com.krrishg.service; -import com.indie.model.AuthProvider; -import com.indie.model.User; -import com.indie.support.FakeUserRepository; -import com.indie.support.TestOAuth2User; +import com.krrishg.model.AuthProvider; +import com.krrishg.model.User; +import com.krrishg.support.FakeUserRepository; +import com.krrishg.support.TestOAuth2User; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/indie/service/ReferenceServiceTest.java b/backend/src/test/java/com/krrishg/service/ReferenceServiceTest.java similarity index 97% rename from backend/src/test/java/com/indie/service/ReferenceServiceTest.java rename to backend/src/test/java/com/krrishg/service/ReferenceServiceTest.java index 1f8bf9a..224918e 100644 --- a/backend/src/test/java/com/indie/service/ReferenceServiceTest.java +++ b/backend/src/test/java/com/krrishg/service/ReferenceServiceTest.java @@ -1,6 +1,6 @@ -package com.indie.service; +package com.krrishg.service; -import com.indie.dto.Reference; +import com.krrishg.dto.Reference; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/indie/service/llm/LLMFactoryTest.java b/backend/src/test/java/com/krrishg/service/llm/LLMFactoryTest.java similarity index 97% rename from backend/src/test/java/com/indie/service/llm/LLMFactoryTest.java rename to backend/src/test/java/com/krrishg/service/llm/LLMFactoryTest.java index 29206e3..b2221a5 100644 --- a/backend/src/test/java/com/indie/service/llm/LLMFactoryTest.java +++ b/backend/src/test/java/com/krrishg/service/llm/LLMFactoryTest.java @@ -1,6 +1,6 @@ -package com.indie.service.llm; +package com.krrishg.service.llm; -import com.indie.support.FakeLLMProvider; +import com.krrishg.support.FakeLLMProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/com/indie/support/FakeJwtConfig.java b/backend/src/test/java/com/krrishg/support/FakeJwtConfig.java similarity index 93% rename from backend/src/test/java/com/indie/support/FakeJwtConfig.java rename to backend/src/test/java/com/krrishg/support/FakeJwtConfig.java index ac526d6..926b7ec 100644 --- a/backend/src/test/java/com/indie/support/FakeJwtConfig.java +++ b/backend/src/test/java/com/krrishg/support/FakeJwtConfig.java @@ -1,7 +1,7 @@ -package com.indie.support; +package com.krrishg.support; -import com.indie.config.TokenService; -import com.indie.model.User; +import com.krrishg.config.TokenService; +import com.krrishg.model.User; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; diff --git a/backend/src/test/java/com/indie/support/FakeLLMProvider.java b/backend/src/test/java/com/krrishg/support/FakeLLMProvider.java similarity index 86% rename from backend/src/test/java/com/indie/support/FakeLLMProvider.java rename to backend/src/test/java/com/krrishg/support/FakeLLMProvider.java index d9915ed..84e2fcb 100644 --- a/backend/src/test/java/com/indie/support/FakeLLMProvider.java +++ b/backend/src/test/java/com/krrishg/support/FakeLLMProvider.java @@ -1,9 +1,9 @@ -package com.indie.support; +package com.krrishg.support; -import com.indie.dto.LLMResult; -import com.indie.dto.LLMOptions; -import com.indie.dto.Reference; -import com.indie.service.llm.LLMProvider; +import com.krrishg.dto.LLMResult; +import com.krrishg.dto.LLMOptions; +import com.krrishg.dto.Reference; +import com.krrishg.service.llm.LLMProvider; import java.util.List; diff --git a/backend/src/test/java/com/indie/support/FakeSiteRepository.java b/backend/src/test/java/com/krrishg/support/FakeSiteRepository.java similarity index 91% rename from backend/src/test/java/com/indie/support/FakeSiteRepository.java rename to backend/src/test/java/com/krrishg/support/FakeSiteRepository.java index a4ed6c3..24d8f99 100644 --- a/backend/src/test/java/com/indie/support/FakeSiteRepository.java +++ b/backend/src/test/java/com/krrishg/support/FakeSiteRepository.java @@ -1,7 +1,7 @@ -package com.indie.support; +package com.krrishg.support; -import com.indie.model.Site; -import com.indie.repository.SiteRepository; +import com.krrishg.model.Site; +import com.krrishg.repository.SiteRepository; import java.util.Comparator; import java.util.List; diff --git a/backend/src/test/java/com/indie/support/FakeUserRepository.java b/backend/src/test/java/com/krrishg/support/FakeUserRepository.java similarity index 88% rename from backend/src/test/java/com/indie/support/FakeUserRepository.java rename to backend/src/test/java/com/krrishg/support/FakeUserRepository.java index 2f9b4c1..893399c 100644 --- a/backend/src/test/java/com/indie/support/FakeUserRepository.java +++ b/backend/src/test/java/com/krrishg/support/FakeUserRepository.java @@ -1,8 +1,8 @@ -package com.indie.support; +package com.krrishg.support; -import com.indie.model.AuthProvider; -import com.indie.model.User; -import com.indie.repository.UserRepository; +import com.krrishg.model.AuthProvider; +import com.krrishg.model.User; +import com.krrishg.repository.UserRepository; import java.util.Optional; import java.util.UUID; diff --git a/backend/src/test/java/com/indie/support/InMemoryRepository.java b/backend/src/test/java/com/krrishg/support/InMemoryRepository.java similarity index 99% rename from backend/src/test/java/com/indie/support/InMemoryRepository.java rename to backend/src/test/java/com/krrishg/support/InMemoryRepository.java index bae026b..76c17ad 100644 --- a/backend/src/test/java/com/indie/support/InMemoryRepository.java +++ b/backend/src/test/java/com/krrishg/support/InMemoryRepository.java @@ -1,4 +1,4 @@ -package com.indie.support; +package com.krrishg.support; import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; diff --git a/backend/src/test/java/com/indie/support/StubPasswordEncoder.java b/backend/src/test/java/com/krrishg/support/StubPasswordEncoder.java similarity index 94% rename from backend/src/test/java/com/indie/support/StubPasswordEncoder.java rename to backend/src/test/java/com/krrishg/support/StubPasswordEncoder.java index 011575c..185ebbe 100644 --- a/backend/src/test/java/com/indie/support/StubPasswordEncoder.java +++ b/backend/src/test/java/com/krrishg/support/StubPasswordEncoder.java @@ -1,4 +1,4 @@ -package com.indie.support; +package com.krrishg.support; import org.springframework.security.crypto.password.PasswordEncoder; diff --git a/backend/src/test/java/com/indie/support/TestOAuth2User.java b/backend/src/test/java/com/krrishg/support/TestOAuth2User.java similarity index 97% rename from backend/src/test/java/com/indie/support/TestOAuth2User.java rename to backend/src/test/java/com/krrishg/support/TestOAuth2User.java index 774a2d4..93ac29b 100644 --- a/backend/src/test/java/com/indie/support/TestOAuth2User.java +++ b/backend/src/test/java/com/krrishg/support/TestOAuth2User.java @@ -1,4 +1,4 @@ -package com.indie.support; +package com.krrishg.support; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; diff --git a/backend/src/test/java/com/indie/support/TestSecurityConfig.java b/backend/src/test/java/com/krrishg/support/TestSecurityConfig.java similarity index 96% rename from backend/src/test/java/com/indie/support/TestSecurityConfig.java rename to backend/src/test/java/com/krrishg/support/TestSecurityConfig.java index 68f1b6a..7f3134f 100644 --- a/backend/src/test/java/com/indie/support/TestSecurityConfig.java +++ b/backend/src/test/java/com/krrishg/support/TestSecurityConfig.java @@ -1,4 +1,4 @@ -package com.indie.support; +package com.krrishg.support; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index e014752..3365c73 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -25,5 +25,15 @@ export const routes: Routes = [ loadComponent: () => import('./llm-workspace/llm-workspace.component').then(m => m.LlmWorkspaceComponent), canActivate: [AuthGuard], }, + { + path: 'llm/:siteId/settings', + loadComponent: () => import('./llm-workspace/settings/settings.component').then(m => m.SettingsComponent), + canActivate: [AuthGuard], + }, + { + path: 'llm/:siteId/git', + loadComponent: () => import('./llm-workspace/git-settings/git-settings.component').then(m => m.GitSettingsComponent), + canActivate: [AuthGuard], + }, { path: '**', redirectTo: '/dashboard' }, ]; diff --git a/frontend/src/app/core/models/deployment.ts b/frontend/src/app/core/models/deployment.ts new file mode 100644 index 0000000..91e9381 --- /dev/null +++ b/frontend/src/app/core/models/deployment.ts @@ -0,0 +1,25 @@ +export interface GitConnectRequest { + provider: 'GITHUB' | 'GITLAB'; + remoteUrl: string; + token: string; +} + +export interface GitStatusResponse { + connected: boolean; + provider: string | null; + remoteUrl: string | null; + hasKeys: boolean; + commits: CommitInfo[]; +} + +export interface CommitInfo { + hash: string; + message: string; + timestamp: string; + author: string; +} + +export interface PublishResponse { + message: string; + url: string; +} diff --git a/frontend/src/app/core/models/site.ts b/frontend/src/app/core/models/site.ts index d70a3cb..05e3178 100644 --- a/frontend/src/app/core/models/site.ts +++ b/frontend/src/app/core/models/site.ts @@ -4,6 +4,7 @@ export interface SiteResponse { name: string; description: string; subdomain: string; + publishedUrl: string | null; themeConfig: Record; status: 'DRAFT' | 'PUBLISHED'; publishedAt: string | null; diff --git a/frontend/src/app/core/services/deployment.service.ts b/frontend/src/app/core/services/deployment.service.ts new file mode 100644 index 0000000..c345d3d --- /dev/null +++ b/frontend/src/app/core/services/deployment.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { GitConnectRequest, GitStatusResponse, PublishResponse } from '../models/deployment'; + +@Injectable({ providedIn: 'root' }) +export class DeploymentService { + constructor(private http: HttpClient) {} + + exportSite(siteId: string): Observable { + return this.http.get(`/api/sites/${siteId}/export`, { responseType: 'blob' }); + } + + getGitStatus(siteId: string): Observable { + return this.http.get(`/api/sites/${siteId}/git/status`); + } + + connectGit(siteId: string, request: GitConnectRequest): Observable> { + return this.http.post>(`/api/sites/${siteId}/git/connect`, request); + } + + disconnectGit(siteId: string): Observable> { + return this.http.post>(`/api/sites/${siteId}/git/disconnect`, {}); + } + + publish(siteId: string): Observable { + return this.http.post(`/api/sites/${siteId}/publish`, {}); + } + + unpublish(siteId: string): Observable> { + return this.http.post>(`/api/sites/${siteId}/unpublish`, {}); + } +} diff --git a/frontend/src/app/dashboard/create-site-dialog/create-site-dialog.ts b/frontend/src/app/dashboard/create-site-dialog/create-site-dialog.ts index e05a68f..af9546a 100644 --- a/frontend/src/app/dashboard/create-site-dialog/create-site-dialog.ts +++ b/frontend/src/app/dashboard/create-site-dialog/create-site-dialog.ts @@ -23,7 +23,6 @@ import { SiteService } from '../../core/services/site.service'; Subdomain - .indie.com Subdomain is required diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index b33a28f..6ff3dfe 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -61,7 +61,7 @@ import { DeleteSiteDialog } from './delete-site-dialog/delete-site-dialog'; {{ site.name }} - {{ site.subdomain }}.indie.com + {{ site.subdomain }}

{{ site.description || 'No description' }}

diff --git a/frontend/src/app/llm-workspace/git-settings/git-settings.component.ts b/frontend/src/app/llm-workspace/git-settings/git-settings.component.ts new file mode 100644 index 0000000..600b59c --- /dev/null +++ b/frontend/src/app/llm-workspace/git-settings/git-settings.component.ts @@ -0,0 +1,236 @@ +import { Component, OnInit, OnDestroy, inject } from '@angular/core'; +import { NgIf, NgFor, DatePipe, SlicePipe } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatCardModule } from '@angular/material/card'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { DeploymentService } from '../../core/services/deployment.service'; +import { GitStatusResponse, CommitInfo } from '../../core/models/deployment'; + +@Component({ + selector: 'app-git-settings', + standalone: true, + imports: [ + NgIf, NgFor, DatePipe, SlicePipe, FormsModule, + MatToolbarModule, MatButtonModule, MatIconModule, + MatInputModule, MatFormFieldModule, MatCardModule, + MatDividerModule, MatSnackBarModule, MatProgressSpinnerModule, + MatSelectModule, + ], + template: ` +
+ +
+ + Git Settings +
+
+ +
+
+ +
+ +
+ + + + + +
+

Connection

+ + {{ status?.connected ? 'Connected' : 'Not Connected' }} + +
+ +
+

Connect a remote Git repository to automatically push changes when you publish.

+ + Provider + + GitHub + GitLab + + + + Remote URL + + + + Access Token + + +
+ +
+
+ +
+
+
+ Provider: + {{ status?.provider }} +
+
+ Remote: + {{ status?.remoteUrl }} +
+
+ vpn_key + SSH keys configured +
+
+
+ +
+
+
+
+ + + +

Commit History

+
+
+ commit +
+

{{ c.message }}

+
+ {{ c.hash | slice:0:8 }} + {{ c.author }} + {{ c.timestamp | date:'short' }} +
+
+
+
+
+
+ + + +

Commit History

+

No commits yet. Publish your site to create the first commit.

+
+
+ +
+
+
+
+ `, +}) +export class GitSettingsComponent implements OnInit, OnDestroy { + private route = inject(ActivatedRoute); + private router = inject(Router); + private deploymentService = inject(DeploymentService); + private snackBar = inject(MatSnackBar); + + siteId: string | null = null; + status: GitStatusResponse | null = null; + loading = true; + + connectProvider: 'GITHUB' | 'GITLAB' = 'GITHUB'; + connectRemoteUrl = ''; + connectToken = ''; + connecting = false; + disconnecting = false; + private destroy$ = new Subject(); + + ngOnInit(): void { + this.route.paramMap + .pipe(takeUntil(this.destroy$)) + .subscribe(params => { + this.siteId = params.get('siteId'); + if (this.siteId) this.loadStatus(); + }); + } + + private loadStatus(): void { + if (!this.siteId) return; + this.loading = true; + this.deploymentService.getGitStatus(this.siteId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (s) => { + this.status = s; + this.loading = false; + }, + error: () => { + this.loading = false; + this.snackBar.open('Failed to load git status', 'Close', { duration: 3000 }); + }, + }); + } + + connect(): void { + if (!this.siteId || !this.connectRemoteUrl.trim() || !this.connectToken.trim()) return; + this.connecting = true; + this.deploymentService.connectGit(this.siteId, { + provider: this.connectProvider, + remoteUrl: this.connectRemoteUrl.trim(), + token: this.connectToken.trim(), + }).pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (res) => { + this.connecting = false; + this.snackBar.open(res['message'] || 'Connected', 'Close', { duration: 3000 }); + this.loadStatus(); + }, + error: (err) => { + this.connecting = false; + const msg = err.error?.error || 'Failed to connect'; + this.snackBar.open(msg, 'Close', { duration: 5000 }); + }, + }); + } + + disconnect(): void { + if (!this.siteId) return; + this.disconnecting = true; + this.deploymentService.disconnectGit(this.siteId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (res) => { + this.disconnecting = false; + this.snackBar.open(res['message'] || 'Disconnected', 'Close', { duration: 3000 }); + this.loadStatus(); + }, + error: () => { + this.disconnecting = false; + this.snackBar.open('Failed to disconnect', 'Close', { duration: 3000 }); + }, + }); + } + + goBack(): void { + if (this.siteId) this.router.navigate(['/llm', this.siteId, 'settings']); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/frontend/src/app/llm-workspace/llm-workspace.component.ts b/frontend/src/app/llm-workspace/llm-workspace.component.ts index 636804f..239c185 100644 --- a/frontend/src/app/llm-workspace/llm-workspace.component.ts +++ b/frontend/src/app/llm-workspace/llm-workspace.component.ts @@ -41,6 +41,9 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial AI Site Generator +
@@ -439,6 +442,10 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy { this.router.navigate(['/dashboard']); } + goToSettings(): void { + if (this.siteId) this.router.navigate(['/llm', this.siteId, 'settings']); + } + totalComponents(structure: SiteStructure): number { return structure.pages.reduce((sum, p) => sum + p.components.length, 0); } diff --git a/frontend/src/app/llm-workspace/settings/settings.component.ts b/frontend/src/app/llm-workspace/settings/settings.component.ts new file mode 100644 index 0000000..591d239 --- /dev/null +++ b/frontend/src/app/llm-workspace/settings/settings.component.ts @@ -0,0 +1,278 @@ +import { Component, OnInit, OnDestroy, inject } from '@angular/core'; +import { NgIf, NgFor, DatePipe } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatCardModule } from '@angular/material/card'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { SiteService } from '../../core/services/site.service'; +import { DeploymentService } from '../../core/services/deployment.service'; +import { SiteResponse } from '../../core/models/site'; + +@Component({ + selector: 'app-settings', + standalone: true, + imports: [ + NgIf, NgFor, DatePipe, FormsModule, RouterModule, + MatToolbarModule, MatButtonModule, MatIconModule, + MatInputModule, MatFormFieldModule, MatCardModule, + MatDividerModule, MatSnackBarModule, MatProgressSpinnerModule, + ], + template: ` +
+ +
+ + Site Settings +
+
+ +
+
+ + + +

General

+
+ + Site Name + + + + Description + + + + Subdomain + + +
+ +
+
+
+
+ + + +

Export

+

+ Download your site as a ZIP file containing all HTML, CSS, JS, and assets, plus the full .git directory. +

+ +
+
+ + + +
+

Publishing

+ + {{ siteStatus === 'PUBLISHED' ? 'Published' : 'Draft' }} + +
+

+ Publish your site to make it live at your subdomain. The latest version will be rendered and deployed. +

+

+ Your site is live at {{ liveUrl }} +

+
+ + +
+
+ + {{ publishStatusText }} +
+
+
+ + + +
+

Git Repository

+ +
+

+ Connect a GitHub or GitLab repository to automatically push changes when you publish. +

+
+
+ +
+
+
+ `, +}) +export class SettingsComponent implements OnInit, OnDestroy { + private route = inject(ActivatedRoute); + private router = inject(Router); + private siteService = inject(SiteService); + private deploymentService = inject(DeploymentService); + private snackBar = inject(MatSnackBar); + + siteId: string | null = null; + siteName = ''; + siteDescription = ''; + siteSubdomain = ''; + siteStatus: 'DRAFT' | 'PUBLISHED' = 'DRAFT'; + liveUrl = ''; + saving = false; + exporting = false; + publishing = false; + publishStatusText = ''; + private destroy$ = new Subject(); + + ngOnInit(): void { + this.route.paramMap + .pipe(takeUntil(this.destroy$)) + .subscribe(params => { + this.siteId = params.get('siteId'); + if (this.siteId) this.loadSite(); + }); + } + + private loadSite(): void { + if (!this.siteId) return; + this.siteService.get(this.siteId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (site) => { + this.siteName = site.name; + this.siteDescription = site.description || ''; + this.siteSubdomain = site.subdomain || ''; + this.siteStatus = site.status; + this.liveUrl = site.publishedUrl || ''; + }, + error: () => this.snackBar.open('Failed to load site', 'Close', { duration: 3000 }), + }); + } + + saveSettings(): void { + if (!this.siteId || !this.siteName.trim()) return; + this.saving = true; + this.siteService.update(this.siteId, { + name: this.siteName, + description: this.siteDescription, + subdomain: this.siteSubdomain, + }).pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => { + this.saving = false; + this.snackBar.open('Settings saved', 'Close', { duration: 2000 }); + }, + error: () => { + this.saving = false; + this.snackBar.open('Failed to save settings', 'Close', { duration: 3000 }); + }, + }); + } + + exportSite(): void { + if (!this.siteId) return; + this.exporting = true; + this.deploymentService.exportSite(this.siteId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (blob) => { + this.exporting = false; + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + const name = this.siteSubdomain || this.siteId || 'site'; + a.download = `site-export-${name}.zip`; + a.click(); + window.URL.revokeObjectURL(url); + this.snackBar.open('Export downloaded', 'Close', { duration: 2000 }); + }, + error: () => { + this.exporting = false; + this.snackBar.open('Export failed', 'Close', { duration: 3000 }); + }, + }); + } + + publishSite(): void { + if (!this.siteId) return; + this.publishing = true; + this.publishStatusText = 'Publishing site...'; + this.deploymentService.publish(this.siteId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (res) => { + this.publishing = false; + this.siteStatus = 'PUBLISHED'; + this.liveUrl = res.url; + this.snackBar.open('Site published!', 'Close', { duration: 3000 }); + }, + error: (err) => { + this.publishing = false; + const msg = err.error?.error || 'Publish failed'; + this.snackBar.open(msg, 'Close', { duration: 5000 }); + }, + }); + } + + unpublishSite(): void { + if (!this.siteId) return; + this.publishing = true; + this.publishStatusText = 'Unpublishing site...'; + this.deploymentService.unpublish(this.siteId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => { + this.publishing = false; + this.siteStatus = 'DRAFT'; + this.liveUrl = ''; + this.snackBar.open('Site unpublished', 'Close', { duration: 3000 }); + }, + error: () => { + this.publishing = false; + this.snackBar.open('Unpublish failed', 'Close', { duration: 3000 }); + }, + }); + } + + goBack(): void { + if (this.siteId) this.router.navigate(['/llm', this.siteId]); + } + + goToGitSettings(): void { + if (this.siteId) this.router.navigate(['/llm', this.siteId, 'git']); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +}