2026-04-04 17:53:10 +02:00
|
|
|
package net.siegeln.cameleer.saas.app;
|
|
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
import net.siegeln.cameleer.saas.app.dto.AppResponse;
|
|
|
|
|
import net.siegeln.cameleer.saas.app.dto.CreateAppRequest;
|
2026-04-04 20:57:37 +02:00
|
|
|
import net.siegeln.cameleer.saas.environment.EnvironmentService;
|
|
|
|
|
import net.siegeln.cameleer.saas.runtime.RuntimeConfig;
|
|
|
|
|
import net.siegeln.cameleer.saas.tenant.TenantRepository;
|
2026-04-04 17:53:10 +02:00
|
|
|
import org.springframework.http.HttpStatus;
|
|
|
|
|
import org.springframework.http.ResponseEntity;
|
|
|
|
|
import org.springframework.security.core.Authentication;
|
|
|
|
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
|
|
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
2026-04-04 20:57:37 +02:00
|
|
|
import org.springframework.web.bind.annotation.PatchMapping;
|
2026-04-04 17:53:10 +02:00
|
|
|
import org.springframework.web.bind.annotation.PathVariable;
|
|
|
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
|
|
|
import org.springframework.web.bind.annotation.PutMapping;
|
2026-04-04 20:57:37 +02:00
|
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
2026-04-04 17:53:10 +02:00
|
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
|
|
import org.springframework.web.bind.annotation.RequestPart;
|
|
|
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
|
|
|
|
|
@RestController
|
|
|
|
|
@RequestMapping("/api/environments/{environmentId}/apps")
|
|
|
|
|
public class AppController {
|
|
|
|
|
|
|
|
|
|
private final AppService appService;
|
|
|
|
|
private final ObjectMapper objectMapper;
|
2026-04-04 20:57:37 +02:00
|
|
|
private final EnvironmentService environmentService;
|
|
|
|
|
private final RuntimeConfig runtimeConfig;
|
|
|
|
|
private final TenantRepository tenantRepository;
|
2026-04-04 17:53:10 +02:00
|
|
|
|
2026-04-04 20:57:37 +02:00
|
|
|
public AppController(AppService appService, ObjectMapper objectMapper,
|
|
|
|
|
EnvironmentService environmentService,
|
|
|
|
|
RuntimeConfig runtimeConfig,
|
|
|
|
|
TenantRepository tenantRepository) {
|
2026-04-04 17:53:10 +02:00
|
|
|
this.appService = appService;
|
|
|
|
|
this.objectMapper = objectMapper;
|
2026-04-04 20:57:37 +02:00
|
|
|
this.environmentService = environmentService;
|
|
|
|
|
this.runtimeConfig = runtimeConfig;
|
|
|
|
|
this.tenantRepository = tenantRepository;
|
2026-04-04 17:53:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping(consumes = "multipart/form-data")
|
|
|
|
|
public ResponseEntity<AppResponse> create(
|
|
|
|
|
@PathVariable UUID environmentId,
|
|
|
|
|
@RequestPart("metadata") String metadataJson,
|
|
|
|
|
@RequestPart("file") MultipartFile file,
|
|
|
|
|
Authentication authentication) {
|
|
|
|
|
try {
|
|
|
|
|
var request = objectMapper.readValue(metadataJson, CreateAppRequest.class);
|
|
|
|
|
UUID actorId = resolveActorId(authentication);
|
|
|
|
|
var entity = appService.create(environmentId, request.slug(), request.displayName(), file, actorId);
|
|
|
|
|
return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(entity));
|
|
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
|
|
var msg = e.getMessage();
|
|
|
|
|
if (msg != null && (msg.contains("already exists") || msg.contains("slug"))) {
|
|
|
|
|
return ResponseEntity.status(HttpStatus.CONFLICT).build();
|
|
|
|
|
}
|
|
|
|
|
return ResponseEntity.badRequest().build();
|
|
|
|
|
} catch (IllegalStateException e) {
|
|
|
|
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
return ResponseEntity.badRequest().build();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping
|
|
|
|
|
public ResponseEntity<List<AppResponse>> list(@PathVariable UUID environmentId) {
|
|
|
|
|
var apps = appService.listByEnvironmentId(environmentId)
|
|
|
|
|
.stream()
|
|
|
|
|
.map(this::toResponse)
|
|
|
|
|
.toList();
|
|
|
|
|
return ResponseEntity.ok(apps);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping("/{appId}")
|
|
|
|
|
public ResponseEntity<AppResponse> getById(
|
|
|
|
|
@PathVariable UUID environmentId,
|
|
|
|
|
@PathVariable UUID appId) {
|
|
|
|
|
return appService.getById(appId)
|
|
|
|
|
.map(entity -> ResponseEntity.ok(toResponse(entity)))
|
|
|
|
|
.orElse(ResponseEntity.notFound().build());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PutMapping(value = "/{appId}/jar", consumes = "multipart/form-data")
|
|
|
|
|
public ResponseEntity<AppResponse> reuploadJar(
|
|
|
|
|
@PathVariable UUID environmentId,
|
|
|
|
|
@PathVariable UUID appId,
|
|
|
|
|
@RequestPart("file") MultipartFile file,
|
|
|
|
|
Authentication authentication) {
|
|
|
|
|
try {
|
|
|
|
|
UUID actorId = resolveActorId(authentication);
|
|
|
|
|
var entity = appService.reuploadJar(appId, file, actorId);
|
|
|
|
|
return ResponseEntity.ok(toResponse(entity));
|
|
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
|
|
return ResponseEntity.notFound().build();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@DeleteMapping("/{appId}")
|
|
|
|
|
public ResponseEntity<Void> delete(
|
|
|
|
|
@PathVariable UUID environmentId,
|
|
|
|
|
@PathVariable UUID appId,
|
|
|
|
|
Authentication authentication) {
|
|
|
|
|
try {
|
|
|
|
|
UUID actorId = resolveActorId(authentication);
|
|
|
|
|
appService.delete(appId, actorId);
|
|
|
|
|
return ResponseEntity.noContent().build();
|
|
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
|
|
return ResponseEntity.notFound().build();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 20:57:37 +02:00
|
|
|
@PatchMapping("/{appId}/routing")
|
|
|
|
|
public ResponseEntity<AppResponse> updateRouting(
|
|
|
|
|
@PathVariable UUID environmentId,
|
|
|
|
|
@PathVariable UUID appId,
|
|
|
|
|
@RequestBody net.siegeln.cameleer.saas.observability.dto.UpdateRoutingRequest request,
|
|
|
|
|
Authentication authentication) {
|
|
|
|
|
try {
|
|
|
|
|
var actorId = resolveActorId(authentication);
|
|
|
|
|
var app = appService.updateRouting(appId, request.exposedPort(), actorId);
|
|
|
|
|
return ResponseEntity.ok(toResponse(app));
|
|
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
|
|
return ResponseEntity.notFound().build();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 17:53:10 +02:00
|
|
|
private UUID resolveActorId(Authentication authentication) {
|
|
|
|
|
String sub = authentication.getName();
|
|
|
|
|
try {
|
|
|
|
|
return UUID.fromString(sub);
|
|
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
|
|
return UUID.nameUUIDFromBytes(sub.getBytes());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 20:57:37 +02:00
|
|
|
private AppResponse toResponse(AppEntity app) {
|
|
|
|
|
String routeUrl = null;
|
|
|
|
|
if (app.getExposedPort() != null) {
|
|
|
|
|
var env = environmentService.getById(app.getEnvironmentId()).orElse(null);
|
|
|
|
|
if (env != null) {
|
|
|
|
|
var tenant = tenantRepository.findById(env.getTenantId()).orElse(null);
|
|
|
|
|
if (tenant != null) {
|
|
|
|
|
routeUrl = "http://" + app.getSlug() + "." + env.getSlug() + "."
|
|
|
|
|
+ tenant.getSlug() + "." + runtimeConfig.getDomain();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-04 17:53:10 +02:00
|
|
|
return new AppResponse(
|
2026-04-04 20:57:37 +02:00
|
|
|
app.getId(), app.getEnvironmentId(), app.getSlug(), app.getDisplayName(),
|
|
|
|
|
app.getJarOriginalFilename(), app.getJarSizeBytes(), app.getJarChecksum(),
|
|
|
|
|
app.getExposedPort(), routeUrl,
|
|
|
|
|
app.getCurrentDeploymentId(), app.getPreviousDeploymentId(),
|
|
|
|
|
app.getCreatedAt(), app.getUpdatedAt());
|
2026-04-04 17:53:10 +02:00
|
|
|
}
|
|
|
|
|
}
|