publish to github or gitlab pages
This commit is contained in:
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id 'io.spring.dependency-management' version '1.1.4'
|
id 'io.spring.dependency-management' version '1.1.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'com.indie'
|
group = 'com.krrishg'
|
||||||
version = '0.1.0'
|
version = '0.1.0'
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie;
|
package com.krrishg;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.config;
|
package com.krrishg.config;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -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.HttpStatus;
|
||||||
import org.springframework.http.ProblemDetail;
|
import org.springframework.http.ProblemDetail;
|
||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.FieldError;
|
||||||
@@ -12,6 +14,8 @@ import java.util.stream.Collectors;
|
|||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
@ExceptionHandler(IllegalArgumentException.class)
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
public ProblemDetail handleBadRequest(IllegalArgumentException ex) {
|
public ProblemDetail handleBadRequest(IllegalArgumentException ex) {
|
||||||
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
|
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
|
||||||
@@ -27,6 +31,7 @@ public class GlobalExceptionHandler {
|
|||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ProblemDetail handleGeneral(Exception ex) {
|
public ProblemDetail handleGeneral(Exception ex) {
|
||||||
|
log.error("Unhandled exception", ex);
|
||||||
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR,
|
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
"An unexpected error occurred");
|
"An unexpected error occurred");
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.config;
|
package com.krrishg.config;
|
||||||
|
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
|
||||||
@@ -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.Claims;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.config;
|
package com.krrishg.config;
|
||||||
|
|
||||||
import io.github.bucket4j.Bandwidth;
|
import io.github.bucket4j.Bandwidth;
|
||||||
import io.github.bucket4j.Bucket;
|
import io.github.bucket4j.Bucket;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.indie.config;
|
package com.krrishg.config;
|
||||||
|
|
||||||
import com.indie.service.OAuth2SuccessHandler;
|
import com.krrishg.service.OAuth2SuccessHandler;
|
||||||
import com.indie.service.OAuth2UserService;
|
import com.krrishg.service.OAuth2UserService;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
41
backend/src/main/java/com/krrishg/config/StorageConfig.java
Normal file
41
backend/src/main/java/com/krrishg/config/StorageConfig.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Claims;
|
||||||
|
|
||||||
public interface TokenService {
|
public interface TokenService {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.config;
|
package com.krrishg.config;
|
||||||
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.indie.controller;
|
package com.krrishg.controller;
|
||||||
|
|
||||||
import com.indie.dto.AuthResponse;
|
import com.krrishg.dto.AuthResponse;
|
||||||
import com.indie.dto.LoginRequest;
|
import com.krrishg.dto.LoginRequest;
|
||||||
import com.indie.dto.RefreshTokenRequest;
|
import com.krrishg.dto.RefreshTokenRequest;
|
||||||
import com.indie.dto.RegisterRequest;
|
import com.krrishg.dto.RegisterRequest;
|
||||||
import com.indie.service.AuthService;
|
import com.krrishg.service.AuthService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<byte[]> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
102
backend/src/main/java/com/krrishg/controller/GitController.java
Normal file
102
backend/src/main/java/com/krrishg/controller/GitController.java
Normal file
@@ -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<String, String> 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<String, Object>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.indie.controller;
|
package com.krrishg.controller;
|
||||||
|
|
||||||
import com.indie.config.RateLimitConfig;
|
import com.krrishg.config.RateLimitConfig;
|
||||||
import com.indie.config.UserPrincipal;
|
import com.krrishg.config.UserPrincipal;
|
||||||
import com.indie.dto.Reference;
|
import com.krrishg.dto.Reference;
|
||||||
import com.indie.dto.SiteStructure;
|
import com.krrishg.dto.SiteStructure;
|
||||||
import com.indie.model.GenerationVersion;
|
import com.krrishg.model.GenerationVersion;
|
||||||
import com.indie.service.LLMService;
|
import com.krrishg.service.LLMService;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.indie.controller;
|
package com.krrishg.controller;
|
||||||
|
|
||||||
import com.indie.config.UserPrincipal;
|
import com.krrishg.config.UserPrincipal;
|
||||||
import com.indie.dto.SiteRequest;
|
import com.krrishg.dto.SiteRequest;
|
||||||
import com.indie.dto.SiteResponse;
|
import com.krrishg.dto.SiteResponse;
|
||||||
import com.indie.model.Site;
|
import com.krrishg.model.Site;
|
||||||
import com.indie.repository.SiteRepository;
|
import com.krrishg.repository.SiteRepository;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -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.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.dto;
|
package com.krrishg.dto;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.dto;
|
package com.krrishg.dto;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.dto;
|
package com.krrishg.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.Email;
|
import jakarta.validation.constraints.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.dto;
|
package com.krrishg.dto;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.dto;
|
package com.krrishg.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.dto;
|
package com.krrishg.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.Email;
|
import jakarta.validation.constraints.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.dto;
|
package com.krrishg.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.indie.dto;
|
package com.krrishg.dto;
|
||||||
|
|
||||||
import com.indie.model.Site;
|
import com.krrishg.model.Site;
|
||||||
import com.indie.model.SiteStatus;
|
import com.krrishg.model.SiteStatus;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -19,6 +19,7 @@ public class SiteResponse {
|
|||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
private String subdomain;
|
private String subdomain;
|
||||||
|
private String publishedUrl;
|
||||||
private SiteStatus status;
|
private SiteStatus status;
|
||||||
private LocalDateTime publishedAt;
|
private LocalDateTime publishedAt;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
@@ -31,6 +32,7 @@ public class SiteResponse {
|
|||||||
.name(site.getName())
|
.name(site.getName())
|
||||||
.description(site.getDescription())
|
.description(site.getDescription())
|
||||||
.subdomain(site.getSubdomain())
|
.subdomain(site.getSubdomain())
|
||||||
|
.publishedUrl(site.getPublishedUrl())
|
||||||
.status(site.getStatus())
|
.status(site.getStatus())
|
||||||
.publishedAt(site.getPublishedAt())
|
.publishedAt(site.getPublishedAt())
|
||||||
.createdAt(site.getCreatedAt())
|
.createdAt(site.getCreatedAt())
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.dto;
|
package com.krrishg.dto;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
74
backend/src/main/java/com/krrishg/engine/JsBundle.java
Normal file
74
backend/src/main/java/com/krrishg/engine/JsBundle.java
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
}
|
||||||
31
backend/src/main/java/com/krrishg/engine/StyleCompiler.java
Normal file
31
backend/src/main/java/com/krrishg/engine/StyleCompiler.java
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.model;
|
package com.krrishg.model;
|
||||||
|
|
||||||
public enum AuthProvider {
|
public enum AuthProvider {
|
||||||
LOCAL, GOOGLE, GITHUB, OPENID
|
LOCAL, GOOGLE, GITHUB, OPENID
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.model;
|
package com.krrishg.model;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
63
backend/src/main/java/com/krrishg/model/GitRemote.java
Normal file
63
backend/src/main/java/com/krrishg/model/GitRemote.java
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.model;
|
package com.krrishg.model;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
@@ -32,6 +32,8 @@ public class Site {
|
|||||||
@Column(unique = true)
|
@Column(unique = true)
|
||||||
private String subdomain;
|
private String subdomain;
|
||||||
|
|
||||||
|
private String publishedUrl;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.model;
|
package com.krrishg.model;
|
||||||
|
|
||||||
public enum SiteStatus {
|
public enum SiteStatus {
|
||||||
DRAFT, PUBLISHED
|
DRAFT, PUBLISHED
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.model;
|
package com.krrishg.model;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
@@ -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<GitRemote, UUID> {
|
||||||
|
|
||||||
|
Optional<GitRemote> findBySiteId(UUID siteId);
|
||||||
|
|
||||||
|
boolean existsBySiteId(UUID siteId);
|
||||||
|
|
||||||
|
void deleteBySiteId(UUID siteId);
|
||||||
|
}
|
||||||
@@ -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 org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.indie.repository;
|
package com.krrishg.repository;
|
||||||
|
|
||||||
import com.indie.model.AuthProvider;
|
import com.krrishg.model.AuthProvider;
|
||||||
import com.indie.model.User;
|
import com.krrishg.model.User;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.indie.service;
|
package com.krrishg.service;
|
||||||
|
|
||||||
import com.indie.config.TokenService;
|
import com.krrishg.config.TokenService;
|
||||||
import com.indie.dto.AuthResponse;
|
import com.krrishg.dto.AuthResponse;
|
||||||
import com.indie.dto.LoginRequest;
|
import com.krrishg.dto.LoginRequest;
|
||||||
import com.indie.dto.RefreshTokenRequest;
|
import com.krrishg.dto.RefreshTokenRequest;
|
||||||
import com.indie.dto.RegisterRequest;
|
import com.krrishg.dto.RegisterRequest;
|
||||||
import com.indie.model.AuthProvider;
|
import com.krrishg.model.AuthProvider;
|
||||||
import com.indie.model.User;
|
import com.krrishg.model.User;
|
||||||
import com.indie.repository.UserRepository;
|
import com.krrishg.repository.UserRepository;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
@@ -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.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/src/main/java/com/krrishg/service/DomainService.java
Normal file
60
backend/src/main/java/com/krrishg/service/DomainService.java
Normal file
@@ -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<Site> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
backend/src/main/java/com/krrishg/service/ExportService.java
Normal file
44
backend/src/main/java/com/krrishg/service/ExportService.java
Normal file
@@ -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<String, byte[]> files = output.asFileMap();
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
|
||||||
|
for (Map.Entry<String, byte[]> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
369
backend/src/main/java/com/krrishg/service/GitService.java
Normal file
369
backend/src/main/java/com/krrishg/service/GitService.java
Normal file
@@ -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<String, byte[]> files = output.asFileMap();
|
||||||
|
for (Map.Entry<String, byte[]> 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<CommitInfo> getLog(String siteId, int maxCount) {
|
||||||
|
try (Git git = openRepo(siteId)) {
|
||||||
|
ObjectId head = git.getRepository().resolve("HEAD");
|
||||||
|
if (head == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<CommitInfo> 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<String, byte[]> 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<String, byte[]> 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
backend/src/main/java/com/krrishg/service/KeyManager.java
Normal file
91
backend/src/main/java/com/krrishg/service/KeyManager.java
Normal file
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.indie.service;
|
package com.krrishg.service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.indie.dto.SiteStructure;
|
import com.krrishg.dto.SiteStructure;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.indie.service;
|
package com.krrishg.service;
|
||||||
|
|
||||||
import com.indie.dto.LLMOptions;
|
import com.krrishg.dto.LLMOptions;
|
||||||
import com.indie.dto.LLMResult;
|
import com.krrishg.dto.LLMResult;
|
||||||
import com.indie.dto.Reference;
|
import com.krrishg.dto.Reference;
|
||||||
import com.indie.dto.SiteStructure;
|
import com.krrishg.dto.SiteStructure;
|
||||||
import com.indie.model.GenerationVersion;
|
import com.krrishg.model.GenerationVersion;
|
||||||
import com.indie.service.llm.LLMFactory;
|
import com.krrishg.service.llm.LLMFactory;
|
||||||
import com.indie.service.llm.LLMProvider;
|
import com.krrishg.service.llm.LLMProvider;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
88
backend/src/main/java/com/krrishg/service/MediaService.java
Normal file
88
backend/src/main/java/com/krrishg/service/MediaService.java
Normal file
@@ -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<String> 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) {}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.indie.service;
|
package com.krrishg.service;
|
||||||
|
|
||||||
import com.indie.config.JwtConfig;
|
import com.krrishg.config.JwtConfig;
|
||||||
import com.indie.model.User;
|
import com.krrishg.model.User;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.indie.service;
|
package com.krrishg.service;
|
||||||
|
|
||||||
import com.indie.model.AuthProvider;
|
import com.krrishg.model.AuthProvider;
|
||||||
import com.indie.model.User;
|
import com.krrishg.model.User;
|
||||||
import com.indie.repository.UserRepository;
|
import com.krrishg.repository.UserRepository;
|
||||||
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
||||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
@@ -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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
347
backend/src/main/java/com/krrishg/service/RenderService.java
Normal file
347
backend/src/main/java/com/krrishg/service/RenderService.java
Normal file
@@ -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<String, String> 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<String, String> e : theme.getColors().entrySet()) {
|
||||||
|
cssVars.append(" --color-").append(e.getKey()).append(": ").append(e.getValue()).append(";\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (theme.getFonts() != null) {
|
||||||
|
for (Map.Entry<String, String> e : theme.getFonts().entrySet()) {
|
||||||
|
cssVars.append(" --font-").append(e.getKey()).append(": ").append(e.getValue()).append(";\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (theme.getSpacing() != null) {
|
||||||
|
for (Map.Entry<String, String> e : theme.getSpacing().entrySet()) {
|
||||||
|
cssVars.append(" --spacing-").append(e.getKey()).append(": ").append(e.getValue()).append(";\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Section> sections = buildSections(page.getComponents());
|
||||||
|
Set<Integer> 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("<section").append(bgStyle).append(">\n <div class=\"container\">\n");
|
||||||
|
for (int idx : section.indices) {
|
||||||
|
SiteStructure.ComponentStructure comp = page.getComponents().get(idx);
|
||||||
|
sectionsHtml.append(" ").append(renderComponent(comp, idx)).append("\n");
|
||||||
|
}
|
||||||
|
sectionsHtml.append(" </div>\n</section>\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<Map.Entry<String, Object>> 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<String, Object> 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 "<!DOCTYPE html>\n"
|
||||||
|
+ "<html lang=\"en\">\n"
|
||||||
|
+ "<head>\n"
|
||||||
|
+ " <meta charset=\"UTF-8\">\n"
|
||||||
|
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"
|
||||||
|
+ " <title>" + esc(title) + "</title>\n"
|
||||||
|
+ " <link rel=\"stylesheet\" href=\"style.css\">\n"
|
||||||
|
+ " <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@400;500;600;700&family=Merriweather:wght@300;400;700&family=Lato:wght@300;400;700&family=Open+Sans:wght@300;400;600;700&family=DM+Sans:wght@300;400;500;600;700&family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n"
|
||||||
|
+ " <style>\n"
|
||||||
|
+ " :root {\n"
|
||||||
|
+ cssVars.toString()
|
||||||
|
+ " }\n"
|
||||||
|
+ " * { margin: 0; padding: 0; box-sizing: border-box; }\n"
|
||||||
|
+ " body {\n"
|
||||||
|
+ " font-family: var(--font-body, 'Inter', sans-serif);\n"
|
||||||
|
+ " color: var(--color-text, #1a1a1a);\n"
|
||||||
|
+ " background: var(--color-background, #ffffff);\n"
|
||||||
|
+ " line-height: 1.6;\n"
|
||||||
|
+ " -webkit-font-smoothing: antialiased;\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " .container { max-width: var(--spacing-containerWidth, 1100px); margin: 0 auto; padding: 0 1.5rem; }\n"
|
||||||
|
+ " section { padding: var(--spacing-sectionPadding, 3rem) 0; }\n"
|
||||||
|
+ compStyles.toString()
|
||||||
|
+ " </style>\n"
|
||||||
|
+ "</head>\n"
|
||||||
|
+ "<body>\n"
|
||||||
|
+ sectionsHtml.toString()
|
||||||
|
+ "</body>\n"
|
||||||
|
+ "</html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderComponent(SiteStructure.ComponentStructure c, int index) {
|
||||||
|
Map<String, Object> 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 -> "<div class=\"" + cls + "\">[" + esc(type) + "]</div>";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderHeading(Map<String, Object> 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) + "</" + tag + ">";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderText(Map<String, Object> config, String cls) {
|
||||||
|
String content = str(config.get("content"), "");
|
||||||
|
return "<div class=\"" + cls + "\">" + content + "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderImage(Map<String, Object> 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 = "<img src=\"" + esc(src) + "\" alt=\"" + esc(alt) + "\" class=\"" + cls + "\" style=\"max-width:100%;height:auto\">";
|
||||||
|
String imgHtml = linkTo.isEmpty() ? img : "<a href=\"" + esc(linkTo) + "\">" + img + "</a>";
|
||||||
|
if (!caption.isEmpty()) {
|
||||||
|
return "<figure style=\"margin:0\">" + imgHtml
|
||||||
|
+ "<figcaption style=\"text-align:center;font-size:0.875rem;color:#6b7280;margin-top:0.5rem\">"
|
||||||
|
+ esc(caption) + "</figcaption></figure>";
|
||||||
|
}
|
||||||
|
return imgHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderButton(Map<String, Object> 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 "<a href=\"" + esc(link) + "\" class=\"" + cls + "\" style=\"" + btnStyle + "\">" + esc(text) + "</a>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderDivider(Map<String, Object> config, String cls) {
|
||||||
|
String style = str(config.get("style"), "solid");
|
||||||
|
String color = str(config.get("color"), "var(--color-text, #d1d5db)");
|
||||||
|
return "<hr class=\"" + cls + "\" style=\"border:none;border-top:1px " + style + " " + color + "\">";
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private String renderGallery(Map<String, Object> config, String cls) {
|
||||||
|
List<String> images = (List<String>) config.getOrDefault("images", List.of());
|
||||||
|
int columns = Math.min(Math.max(toInt(config.get("columns"), 3), 1), 6);
|
||||||
|
if (images.isEmpty()) {
|
||||||
|
return "<div class=\"" + cls + "\">Gallery (no images)</div>";
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("<div class=\"").append(cls).append("\" style=\"display:grid;grid-template-columns:repeat(")
|
||||||
|
.append(columns).append(",1fr);gap:1rem\">\n");
|
||||||
|
for (String src : images) {
|
||||||
|
sb.append(" <img src=\"").append(esc(src))
|
||||||
|
.append("\" style=\"width:100%;height:250px;object-fit:cover;border-radius:8px\">\n");
|
||||||
|
}
|
||||||
|
sb.append(" </div>");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderVideo(Map<String, Object> 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("<video src=\"").append(esc(src)).append("\" class=\"").append(cls)
|
||||||
|
.append("\"");
|
||||||
|
if (controls) sb.append(" controls");
|
||||||
|
if (autoplay) sb.append(" autoplay");
|
||||||
|
sb.append(" muted style=\"max-width:100%\"></video>");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private String renderContactForm(Map<String, Object> config, String cls) {
|
||||||
|
List<String> fields = (List<String>) config.getOrDefault("fields", List.of("name", "email", "message"));
|
||||||
|
String submitText = str(config.get("submitText"), "Send");
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("<form class=\"").append(cls).append("\">\n");
|
||||||
|
for (String field : fields) {
|
||||||
|
sb.append(" <div style=\"margin-bottom:1rem\">\n");
|
||||||
|
sb.append(" <label style=\"display:block;font-size:0.875rem;font-weight:600;margin-bottom:0.375rem;color:var(--color-text,#374151)\">")
|
||||||
|
.append(esc(capitalize(field))).append("</label>\n");
|
||||||
|
if ("message".equals(field)) {
|
||||||
|
sb.append(" <textarea rows=\"4\" style=\"width:100%;padding:0.75rem;border:1px solid #d1d5db;border-radius:8px;font-family:inherit;font-size:0.95rem;transition:border-color 0.2s;outline:none\" onfocus=\"this.style.borderColor='var(--color-primary,#4f46e5)'\" onblur=\"this.style.borderColor='#d1d5db'\"></textarea>\n");
|
||||||
|
} else {
|
||||||
|
String inputType = "email".equals(field) ? "email" : "text";
|
||||||
|
sb.append(" <input type=\"").append(inputType)
|
||||||
|
.append("\" style=\"width:100%;padding:0.75rem;border:1px solid #d1d5db;border-radius:8px;font-family:inherit;font-size:0.95rem;transition:border-color 0.2s;outline:none\" onfocus=\"this.style.borderColor='var(--color-primary,#4f46e5)'\" onblur=\"this.style.borderColor='#d1d5db'\">\n");
|
||||||
|
}
|
||||||
|
sb.append(" </div>\n");
|
||||||
|
}
|
||||||
|
sb.append(" <button type=\"submit\" style=\"padding:0.75rem 2rem;background:var(--color-primary,#4f46e5);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:1rem;font-weight:500;transition:opacity 0.2s\" onmouseover=\"this.style.opacity='0.9'\" onmouseout=\"this.style.opacity='1'\">")
|
||||||
|
.append(esc(submitText)).append("</button>\n");
|
||||||
|
sb.append(" </form>");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderMap(Map<String, Object> config, String cls) {
|
||||||
|
String address = str(config.get("address"), "New York, NY");
|
||||||
|
return "<div class=\"" + cls + "\" style=\"display:flex;align-items:center;justify-content:center;background:#f3f4f6\">\n"
|
||||||
|
+ " <span style=\"color:#9ca3af\">Map: " + esc(address) + "</span>\n"
|
||||||
|
+ " </div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderCustomHtml(Map<String, Object> config, String cls) {
|
||||||
|
String html = str(config.get("html"), "");
|
||||||
|
return "<div class=\"" + cls + "\">" + html + "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Section> buildSections(List<SiteStructure.ComponentStructure> components) {
|
||||||
|
List<Section> 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<Integer> indices) {}
|
||||||
|
|
||||||
|
public record SiteOutput(String css, String js, Map<String, String> pages) {
|
||||||
|
public Map<String, byte[]> asFileMap() {
|
||||||
|
Map<String, byte[]> files = new LinkedHashMap<>();
|
||||||
|
files.put("style.css", css.getBytes());
|
||||||
|
files.put("script.js", js.getBytes());
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> 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 "<!DOCTYPE html>\n"
|
||||||
|
+ "<html lang=\"en\">\n"
|
||||||
|
+ "<head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">"
|
||||||
|
+ "<title>404 - Page Not Found</title>"
|
||||||
|
+ "<link rel=\"stylesheet\" href=\"style.css\"></head>\n"
|
||||||
|
+ "<body style=\"display:flex;align-items:center;justify-content:center;min-height:100vh;text-align:center;padding:2rem;\">\n"
|
||||||
|
+ "<div><h1>404</h1><p>The page you're looking for doesn't exist.</p>"
|
||||||
|
+ "<a href=\"index.html\" style=\"display:inline-block;text-decoration:none;font-weight:500;font-size:1rem;padding:0.75rem 2rem;border-radius:8px;background:var(--color-primary,#4f46e5);color:#ffffff;margin-top:1rem\">Go Home</a></div>\n"
|
||||||
|
+ "</body></html>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.indie.service.llm;
|
package com.krrishg.service.llm;
|
||||||
|
|
||||||
import com.indie.dto.LLMResult;
|
import com.krrishg.dto.LLMResult;
|
||||||
import com.indie.dto.LLMOptions;
|
import com.krrishg.dto.LLMOptions;
|
||||||
import com.indie.dto.Reference;
|
import com.krrishg.dto.Reference;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.service.llm;
|
package com.krrishg.service.llm;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.indie.service.llm;
|
package com.krrishg.service.llm;
|
||||||
|
|
||||||
import com.indie.dto.LLMResult;
|
import com.krrishg.dto.LLMResult;
|
||||||
import com.indie.dto.LLMOptions;
|
import com.krrishg.dto.LLMOptions;
|
||||||
import com.indie.dto.Reference;
|
import com.krrishg.dto.Reference;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.indie.service.llm;
|
package com.krrishg.service.llm;
|
||||||
|
|
||||||
import com.indie.dto.LLMResult;
|
import com.krrishg.dto.LLMResult;
|
||||||
import com.indie.dto.LLMOptions;
|
import com.krrishg.dto.LLMOptions;
|
||||||
import com.indie.dto.Reference;
|
import com.krrishg.dto.Reference;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
@@ -43,9 +43,18 @@ indie:
|
|||||||
repos-path: ${INDIE_GIT_REPOS_PATH:/home/krrish/git/sites/}
|
repos-path: ${INDIE_GIT_REPOS_PATH:/home/krrish/git/sites/}
|
||||||
author-name: ${INDIE_GIT_AUTHOR_NAME:Indie Platform}
|
author-name: ${INDIE_GIT_AUTHOR_NAME:Indie Platform}
|
||||||
author-email: ${INDIE_GIT_AUTHOR_EMAIL:git@indie.local}
|
author-email: ${INDIE_GIT_AUTHOR_EMAIL:git@indie.local}
|
||||||
|
domain: ${INDIE_SITE_DOMAIN:indie.com}
|
||||||
cors:
|
cors:
|
||||||
allowed-origins: ${INDIE_CORS_ORIGINS:http://localhost:4200,http://localhost:3000}
|
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:
|
llm:
|
||||||
active-provider: ${INDIE_LLM_ACTIVE_PROVIDER:gemini}
|
active-provider: ${INDIE_LLM_ACTIVE_PROVIDER:gemini}
|
||||||
fallback-provider: ${INDIE_LLM_FALLBACK_PROVIDER:openai}
|
fallback-provider: ${INDIE_LLM_FALLBACK_PROVIDER:openai}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.config;
|
package com.krrishg.config;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.config;
|
package com.krrishg.config;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.config;
|
package com.krrishg.config;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.indie.controller;
|
package com.krrishg.controller;
|
||||||
|
|
||||||
import com.indie.config.GlobalExceptionHandler;
|
import com.krrishg.config.GlobalExceptionHandler;
|
||||||
import com.indie.dto.AuthResponse;
|
import com.krrishg.dto.AuthResponse;
|
||||||
import com.indie.dto.AuthResponse.UserInfo;
|
import com.krrishg.dto.AuthResponse.UserInfo;
|
||||||
import com.indie.dto.LoginRequest;
|
import com.krrishg.dto.LoginRequest;
|
||||||
import com.indie.dto.RefreshTokenRequest;
|
import com.krrishg.dto.RefreshTokenRequest;
|
||||||
import com.indie.dto.RegisterRequest;
|
import com.krrishg.dto.RegisterRequest;
|
||||||
import com.indie.model.AuthProvider;
|
import com.krrishg.model.AuthProvider;
|
||||||
import com.indie.service.AuthService;
|
import com.krrishg.service.AuthService;
|
||||||
import com.indie.support.TestSecurityConfig;
|
import com.krrishg.support.TestSecurityConfig;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.indie.controller;
|
package com.krrishg.controller;
|
||||||
|
|
||||||
import com.indie.config.GlobalExceptionHandler;
|
import com.krrishg.config.GlobalExceptionHandler;
|
||||||
import com.indie.config.JwtAuthenticationToken;
|
import com.krrishg.config.JwtAuthenticationToken;
|
||||||
import com.indie.config.RateLimitConfig;
|
import com.krrishg.config.RateLimitConfig;
|
||||||
import com.indie.config.UserPrincipal;
|
import com.krrishg.config.UserPrincipal;
|
||||||
import com.indie.dto.Reference;
|
import com.krrishg.dto.Reference;
|
||||||
import com.indie.dto.SiteStructure;
|
import com.krrishg.dto.SiteStructure;
|
||||||
import com.indie.model.GenerationVersion;
|
import com.krrishg.model.GenerationVersion;
|
||||||
import com.indie.service.LLMService;
|
import com.krrishg.service.LLMService;
|
||||||
import com.indie.support.TestSecurityConfig;
|
import com.krrishg.support.TestSecurityConfig;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.indie.controller;
|
package com.krrishg.controller;
|
||||||
|
|
||||||
import com.indie.config.GlobalExceptionHandler;
|
import com.krrishg.config.GlobalExceptionHandler;
|
||||||
import com.indie.config.JwtAuthenticationToken;
|
import com.krrishg.config.JwtAuthenticationToken;
|
||||||
import com.indie.config.UserPrincipal;
|
import com.krrishg.config.UserPrincipal;
|
||||||
import com.indie.dto.SiteRequest;
|
import com.krrishg.dto.SiteRequest;
|
||||||
import com.indie.model.Site;
|
import com.krrishg.model.Site;
|
||||||
import com.indie.model.SiteStatus;
|
import com.krrishg.model.SiteStatus;
|
||||||
import com.indie.repository.SiteRepository;
|
import com.krrishg.repository.SiteRepository;
|
||||||
import com.indie.support.TestSecurityConfig;
|
import com.krrishg.support.TestSecurityConfig;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.indie.dto;
|
package com.krrishg.dto;
|
||||||
|
|
||||||
import com.indie.model.Site;
|
import com.krrishg.model.Site;
|
||||||
import com.indie.model.SiteStatus;
|
import com.krrishg.model.SiteStatus;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.model;
|
package com.krrishg.model;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.model;
|
package com.krrishg.model;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.indie.repository;
|
package com.krrishg.repository;
|
||||||
|
|
||||||
import com.indie.model.Site;
|
import com.krrishg.model.Site;
|
||||||
import com.indie.model.SiteStatus;
|
import com.krrishg.model.SiteStatus;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.indie.repository;
|
package com.krrishg.repository;
|
||||||
|
|
||||||
import com.indie.model.AuthProvider;
|
import com.krrishg.model.AuthProvider;
|
||||||
import com.indie.model.User;
|
import com.krrishg.model.User;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.indie.service;
|
package com.krrishg.service;
|
||||||
|
|
||||||
import com.indie.dto.AuthResponse;
|
import com.krrishg.dto.AuthResponse;
|
||||||
import com.indie.dto.LoginRequest;
|
import com.krrishg.dto.LoginRequest;
|
||||||
import com.indie.dto.RefreshTokenRequest;
|
import com.krrishg.dto.RefreshTokenRequest;
|
||||||
import com.indie.dto.RegisterRequest;
|
import com.krrishg.dto.RegisterRequest;
|
||||||
import com.indie.model.AuthProvider;
|
import com.krrishg.model.AuthProvider;
|
||||||
import com.indie.model.User;
|
import com.krrishg.model.User;
|
||||||
import com.indie.support.FakeJwtConfig;
|
import com.krrishg.support.FakeJwtConfig;
|
||||||
import com.indie.support.FakeUserRepository;
|
import com.krrishg.support.FakeUserRepository;
|
||||||
import com.indie.support.StubPasswordEncoder;
|
import com.krrishg.support.StubPasswordEncoder;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -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;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.indie.service;
|
package com.krrishg.service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.indie.service;
|
package com.krrishg.service;
|
||||||
|
|
||||||
import com.indie.dto.Reference;
|
import com.krrishg.dto.Reference;
|
||||||
import com.indie.dto.SiteStructure;
|
import com.krrishg.dto.SiteStructure;
|
||||||
import com.indie.model.GenerationVersion;
|
import com.krrishg.model.GenerationVersion;
|
||||||
import com.indie.service.llm.LLMFactory;
|
import com.krrishg.service.llm.LLMFactory;
|
||||||
import com.indie.support.FakeLLMProvider;
|
import com.krrishg.support.FakeLLMProvider;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -69,7 +69,8 @@ class LLMServiceTest {
|
|||||||
when(resourceLoader.getResource(anyString())).thenReturn(mockResource);
|
when(resourceLoader.getResource(anyString())).thenReturn(mockResource);
|
||||||
|
|
||||||
llmService = new LLMService(llmFactory, responseParser, referenceService,
|
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
|
@Test
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.indie.service;
|
package com.krrishg.service;
|
||||||
|
|
||||||
import com.indie.model.AuthProvider;
|
import com.krrishg.model.AuthProvider;
|
||||||
import com.indie.model.User;
|
import com.krrishg.model.User;
|
||||||
import com.indie.support.FakeUserRepository;
|
import com.krrishg.support.FakeUserRepository;
|
||||||
import com.indie.support.TestOAuth2User;
|
import com.krrishg.support.TestOAuth2User;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -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.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -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.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.indie.support;
|
package com.krrishg.support;
|
||||||
|
|
||||||
import com.indie.config.TokenService;
|
import com.krrishg.config.TokenService;
|
||||||
import com.indie.model.User;
|
import com.krrishg.model.User;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.indie.support;
|
package com.krrishg.support;
|
||||||
|
|
||||||
import com.indie.dto.LLMResult;
|
import com.krrishg.dto.LLMResult;
|
||||||
import com.indie.dto.LLMOptions;
|
import com.krrishg.dto.LLMOptions;
|
||||||
import com.indie.dto.Reference;
|
import com.krrishg.dto.Reference;
|
||||||
import com.indie.service.llm.LLMProvider;
|
import com.krrishg.service.llm.LLMProvider;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.indie.support;
|
package com.krrishg.support;
|
||||||
|
|
||||||
import com.indie.model.Site;
|
import com.krrishg.model.Site;
|
||||||
import com.indie.repository.SiteRepository;
|
import com.krrishg.repository.SiteRepository;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.indie.support;
|
package com.krrishg.support;
|
||||||
|
|
||||||
import com.indie.model.AuthProvider;
|
import com.krrishg.model.AuthProvider;
|
||||||
import com.indie.model.User;
|
import com.krrishg.model.User;
|
||||||
import com.indie.repository.UserRepository;
|
import com.krrishg.repository.UserRepository;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.support;
|
package com.krrishg.support;
|
||||||
|
|
||||||
import org.springframework.data.domain.Example;
|
import org.springframework.data.domain.Example;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.support;
|
package com.krrishg.support;
|
||||||
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.support;
|
package com.krrishg.support;
|
||||||
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.indie.support;
|
package com.krrishg.support;
|
||||||
|
|
||||||
import org.springframework.boot.test.context.TestConfiguration;
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -25,5 +25,15 @@ export const routes: Routes = [
|
|||||||
loadComponent: () => import('./llm-workspace/llm-workspace.component').then(m => m.LlmWorkspaceComponent),
|
loadComponent: () => import('./llm-workspace/llm-workspace.component').then(m => m.LlmWorkspaceComponent),
|
||||||
canActivate: [AuthGuard],
|
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' },
|
{ path: '**', redirectTo: '/dashboard' },
|
||||||
];
|
];
|
||||||
|
|||||||
25
frontend/src/app/core/models/deployment.ts
Normal file
25
frontend/src/app/core/models/deployment.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ export interface SiteResponse {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
subdomain: string;
|
subdomain: string;
|
||||||
|
publishedUrl: string | null;
|
||||||
themeConfig: Record<string, unknown>;
|
themeConfig: Record<string, unknown>;
|
||||||
status: 'DRAFT' | 'PUBLISHED';
|
status: 'DRAFT' | 'PUBLISHED';
|
||||||
publishedAt: string | null;
|
publishedAt: string | null;
|
||||||
|
|||||||
33
frontend/src/app/core/services/deployment.service.ts
Normal file
33
frontend/src/app/core/services/deployment.service.ts
Normal file
@@ -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<Blob> {
|
||||||
|
return this.http.get(`/api/sites/${siteId}/export`, { responseType: 'blob' });
|
||||||
|
}
|
||||||
|
|
||||||
|
getGitStatus(siteId: string): Observable<GitStatusResponse> {
|
||||||
|
return this.http.get<GitStatusResponse>(`/api/sites/${siteId}/git/status`);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectGit(siteId: string, request: GitConnectRequest): Observable<Record<string, string>> {
|
||||||
|
return this.http.post<Record<string, string>>(`/api/sites/${siteId}/git/connect`, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectGit(siteId: string): Observable<Record<string, string>> {
|
||||||
|
return this.http.post<Record<string, string>>(`/api/sites/${siteId}/git/disconnect`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
publish(siteId: string): Observable<PublishResponse> {
|
||||||
|
return this.http.post<PublishResponse>(`/api/sites/${siteId}/publish`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
unpublish(siteId: string): Observable<Record<string, string>> {
|
||||||
|
return this.http.post<Record<string, string>>(`/api/sites/${siteId}/unpublish`, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,6 @@ import { SiteService } from '../../core/services/site.service';
|
|||||||
|
|
||||||
<mat-form-field appearance="outline" class="w-full">
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
<mat-label>Subdomain</mat-label>
|
<mat-label>Subdomain</mat-label>
|
||||||
<span matTextSuffix>.indie.com</span>
|
|
||||||
<input matInput formControlName="subdomain" placeholder="my-site" />
|
<input matInput formControlName="subdomain" placeholder="my-site" />
|
||||||
<mat-error *ngIf="form.get('subdomain')?.hasError('required')">Subdomain is required</mat-error>
|
<mat-error *ngIf="form.get('subdomain')?.hasError('required')">Subdomain is required</mat-error>
|
||||||
<mat-error *ngIf="form.get('subdomain')?.hasError('pattern')">
|
<mat-error *ngIf="form.get('subdomain')?.hasError('pattern')">
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ import { DeleteSiteDialog } from './delete-site-dialog/delete-site-dialog';
|
|||||||
<mat-card *ngFor="let site of sites" class="cursor-pointer hover:shadow-lg transition-shadow" (click)="openBuilder(site)">
|
<mat-card *ngFor="let site of sites" class="cursor-pointer hover:shadow-lg transition-shadow" (click)="openBuilder(site)">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>{{ site.name }}</mat-card-title>
|
<mat-card-title>{{ site.name }}</mat-card-title>
|
||||||
<mat-card-subtitle *ngIf="site.subdomain">{{ site.subdomain }}.indie.com</mat-card-subtitle>
|
<mat-card-subtitle *ngIf="site.subdomain">{{ site.subdomain }}</mat-card-subtitle>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<p class="text-gray-600 text-sm mt-2">{{ site.description || 'No description' }}</p>
|
<p class="text-gray-600 text-sm mt-2">{{ site.description || 'No description' }}</p>
|
||||||
|
|||||||
@@ -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: `
|
||||||
|
<div class="h-screen flex flex-col bg-gray-50">
|
||||||
|
<mat-toolbar color="primary" class="h-14 flex items-center justify-between px-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button mat-icon-button (click)="goBack()">
|
||||||
|
<mat-icon>arrow_back</mat-icon>
|
||||||
|
</button>
|
||||||
|
<span class="text-lg font-bold">Git Settings</span>
|
||||||
|
</div>
|
||||||
|
</mat-toolbar>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
<div class="max-w-2xl mx-auto px-6 py-6 space-y-6">
|
||||||
|
|
||||||
|
<div *ngIf="loading" class="flex justify-center py-12">
|
||||||
|
<mat-spinner diameter="40"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!loading">
|
||||||
|
|
||||||
|
<mat-card class="p-4" appearance="outlined">
|
||||||
|
<mat-card-content class="!p-0">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-500 uppercase tracking-wide">Connection</h4>
|
||||||
|
<span class="text-xs px-2 py-0.5 rounded-full"
|
||||||
|
[class.bg-green-100]="status?.connected"
|
||||||
|
[class.text-green-700]="status?.connected"
|
||||||
|
[class.bg-gray-100]="!status?.connected"
|
||||||
|
[class.text-gray-600]="!status?.connected">
|
||||||
|
{{ status?.connected ? 'Connected' : 'Not Connected' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!status?.connected" class="space-y-3">
|
||||||
|
<p class="text-sm text-gray-600">Connect a remote Git repository to automatically push changes when you publish.</p>
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Provider</mat-label>
|
||||||
|
<mat-select [(ngModel)]="connectProvider">
|
||||||
|
<mat-option value="GITHUB">GitHub</mat-option>
|
||||||
|
<mat-option value="GITLAB">GitLab</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Remote URL</mat-label>
|
||||||
|
<input matInput placeholder="git@github.com:user/repo.git" [(ngModel)]="connectRemoteUrl" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Access Token</mat-label>
|
||||||
|
<input matInput type="password" placeholder="GitHub/GitLab personal access token" [(ngModel)]="connectToken" />
|
||||||
|
</mat-form-field>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button mat-flat-button color="primary" [disabled]="connecting || !connectRemoteUrl.trim() || !connectToken.trim()" (click)="connect()">
|
||||||
|
<mat-icon>link</mat-icon>
|
||||||
|
{{ connecting ? 'Connecting...' : 'Connect' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="status?.connected" class="space-y-3">
|
||||||
|
<div class="bg-gray-50 rounded-lg p-3 space-y-2">
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<span class="text-gray-500 font-medium">Provider:</span>
|
||||||
|
<span class="text-gray-800">{{ status?.provider }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<span class="text-gray-500 font-medium">Remote:</span>
|
||||||
|
<span class="text-gray-800 font-mono text-xs">{{ status?.remoteUrl }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-sm" *ngIf="status?.hasKeys">
|
||||||
|
<mat-icon class="text-green-600 text-lg">vpn_key</mat-icon>
|
||||||
|
<span class="text-green-700">SSH keys configured</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button mat-stroked-button color="warn" [disabled]="disconnecting" (click)="disconnect()">
|
||||||
|
<mat-icon>link_off</mat-icon>
|
||||||
|
{{ disconnecting ? 'Disconnecting...' : 'Disconnect' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card class="p-4" appearance="outlined" *ngIf="status?.connected && status?.commits && status!.commits.length > 0">
|
||||||
|
<mat-card-content class="!p-0">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">Commit History</h4>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div *ngFor="let c of status!.commits" class="flex items-start gap-3 p-2 rounded-lg hover:bg-gray-50">
|
||||||
|
<mat-icon class="text-gray-400 text-lg mt-0.5">commit</mat-icon>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm text-gray-800 truncate">{{ c.message }}</p>
|
||||||
|
<div class="flex items-center gap-2 text-xs text-gray-400 mt-0.5">
|
||||||
|
<span class="font-mono">{{ c.hash | slice:0:8 }}</span>
|
||||||
|
<span>{{ c.author }}</span>
|
||||||
|
<span>{{ c.timestamp | date:'short' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card class="p-4" appearance="outlined" *ngIf="status?.connected && (!status?.commits || status!.commits.length === 0)">
|
||||||
|
<mat-card-content class="!p-0">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">Commit History</h4>
|
||||||
|
<p class="text-sm text-gray-500">No commits yet. Publish your site to create the first commit.</p>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
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<void>();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,9 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial
|
|||||||
</button>
|
</button>
|
||||||
<span class="text-lg font-bold">AI Site Generator</span>
|
<span class="text-lg font-bold">AI Site Generator</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button mat-icon-button (click)="goToSettings()" title="Settings">
|
||||||
|
<mat-icon>settings</mat-icon>
|
||||||
|
</button>
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
@@ -439,6 +442,10 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate(['/dashboard']);
|
this.router.navigate(['/dashboard']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goToSettings(): void {
|
||||||
|
if (this.siteId) this.router.navigate(['/llm', this.siteId, 'settings']);
|
||||||
|
}
|
||||||
|
|
||||||
totalComponents(structure: SiteStructure): number {
|
totalComponents(structure: SiteStructure): number {
|
||||||
return structure.pages.reduce((sum, p) => sum + p.components.length, 0);
|
return structure.pages.reduce((sum, p) => sum + p.components.length, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
278
frontend/src/app/llm-workspace/settings/settings.component.ts
Normal file
278
frontend/src/app/llm-workspace/settings/settings.component.ts
Normal file
@@ -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: `
|
||||||
|
<div class="h-screen flex flex-col bg-gray-50">
|
||||||
|
<mat-toolbar color="primary" class="h-14 flex items-center justify-between px-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button mat-icon-button (click)="goBack()">
|
||||||
|
<mat-icon>arrow_back</mat-icon>
|
||||||
|
</button>
|
||||||
|
<span class="text-lg font-bold">Site Settings</span>
|
||||||
|
</div>
|
||||||
|
</mat-toolbar>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
<div class="max-w-2xl mx-auto px-6 py-6 space-y-6">
|
||||||
|
|
||||||
|
<mat-card class="p-4" appearance="outlined">
|
||||||
|
<mat-card-content class="!p-0">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">General</h4>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Site Name</mat-label>
|
||||||
|
<input matInput [(ngModel)]="siteName" [disabled]="saving" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Description</mat-label>
|
||||||
|
<textarea matInput rows="2" [(ngModel)]="siteDescription" [disabled]="saving"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Subdomain</mat-label>
|
||||||
|
<input matInput [(ngModel)]="siteSubdomain" [disabled]="saving" />
|
||||||
|
</mat-form-field>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button mat-flat-button color="primary" [disabled]="saving || !siteName.trim()" (click)="saveSettings()">
|
||||||
|
<mat-icon>save</mat-icon>
|
||||||
|
{{ saving ? 'Saving...' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card class="p-4" appearance="outlined">
|
||||||
|
<mat-card-content class="!p-0">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">Export</h4>
|
||||||
|
<p class="text-sm text-gray-600 mb-3">
|
||||||
|
Download your site as a ZIP file containing all HTML, CSS, JS, and assets, plus the full .git directory.
|
||||||
|
</p>
|
||||||
|
<button mat-stroked-button color="primary" [disabled]="exporting" (click)="exportSite()">
|
||||||
|
<mat-icon>download</mat-icon>
|
||||||
|
{{ exporting ? 'Exporting...' : 'Download ZIP' }}
|
||||||
|
</button>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card class="p-4" appearance="outlined">
|
||||||
|
<mat-card-content class="!p-0">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-500 uppercase tracking-wide">Publishing</h4>
|
||||||
|
<span class="text-xs px-2 py-0.5 rounded-full"
|
||||||
|
[class.bg-green-100]="siteStatus === 'PUBLISHED'"
|
||||||
|
[class.text-green-700]="siteStatus === 'PUBLISHED'"
|
||||||
|
[class.bg-gray-100]="siteStatus === 'DRAFT'"
|
||||||
|
[class.text-gray-600]="siteStatus === 'DRAFT'">
|
||||||
|
{{ siteStatus === 'PUBLISHED' ? 'Published' : 'Draft' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-600 mb-3" *ngIf="siteStatus === 'DRAFT'">
|
||||||
|
Publish your site to make it live at your subdomain. The latest version will be rendered and deployed.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-600 mb-3" *ngIf="siteStatus === 'PUBLISHED' && liveUrl">
|
||||||
|
Your site is live at <a [href]="liveUrl" target="_blank" class="text-blue-600 underline">{{ liveUrl }}</a>
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-3" *ngIf="!publishing">
|
||||||
|
<button mat-flat-button color="primary" *ngIf="siteStatus === 'DRAFT'" [disabled]="publishing" (click)="publishSite()">
|
||||||
|
<mat-icon>publish</mat-icon>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
|
<button mat-stroked-button color="warn" *ngIf="siteStatus === 'PUBLISHED'" [disabled]="publishing" (click)="unpublishSite()">
|
||||||
|
<mat-icon>unpublish</mat-icon>
|
||||||
|
Unpublish
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="publishing" class="flex items-center gap-3">
|
||||||
|
<mat-spinner diameter="20"></mat-spinner>
|
||||||
|
<span class="text-sm text-gray-500">{{ publishStatusText }}</span>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card class="p-4" appearance="outlined">
|
||||||
|
<mat-card-content class="!p-0">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-500 uppercase tracking-wide">Git Repository</h4>
|
||||||
|
<button mat-stroked-button (click)="goToGitSettings()">
|
||||||
|
<mat-icon>settings</mat-icon>
|
||||||
|
Manage
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-600 mt-2">
|
||||||
|
Connect a GitHub or GitLab repository to automatically push changes when you publish.
|
||||||
|
</p>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
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<void>();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user