Contract-first API with DTOs, validation, and server-side OpenAPI post-processing
Add dedicated request/response DTOs for all controllers, replacing raw JsonNode parameters with validated types. Move OpenAPI path-prefix stripping and ProcessorNode children injection into OpenApiCustomizer beans so the spec served at /api/v1/api-docs is already clean — eliminating the need for the ui/scripts/process-openapi.mjs post-processing script. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,10 @@
|
||||
<artifactId>org.eclipse.xtext.xbase.lib</artifactId>
|
||||
<version>2.37.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.cameleer3.server.app.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.Paths;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.media.ArraySchema;
|
||||
import io.swagger.v3.oas.models.media.Schema;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Configuration
|
||||
@SecurityScheme(name = "bearer", type = SecuritySchemeType.HTTP,
|
||||
scheme = "bearer", bearerFormat = "JWT")
|
||||
public class OpenApiConfig {
|
||||
|
||||
/**
|
||||
* Core domain models that always have all fields populated.
|
||||
* Mark all their properties as required so the generated TypeScript
|
||||
* types are non-optional.
|
||||
*/
|
||||
private static final Set<String> ALL_FIELDS_REQUIRED = Set.of(
|
||||
"ExecutionSummary", "ExecutionDetail", "ExecutionStats",
|
||||
"StatsTimeseries", "TimeseriesBucket",
|
||||
"SearchResultExecutionSummary", "UserInfo",
|
||||
"ProcessorNode"
|
||||
);
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info().title("Cameleer3 Server API").version("1.0"))
|
||||
.addSecurityItem(new SecurityRequirement().addList("bearer"))
|
||||
.servers(List.of());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenApiCustomizer pathPrefixStripper() {
|
||||
return openApi -> {
|
||||
var original = openApi.getPaths();
|
||||
if (original == null) return;
|
||||
String prefix = "/api/v1";
|
||||
var stripped = new Paths();
|
||||
for (var entry : original.entrySet()) {
|
||||
String path = entry.getKey();
|
||||
stripped.addPathItem(
|
||||
path.startsWith(prefix) ? path.substring(prefix.length()) : path,
|
||||
entry.getValue());
|
||||
}
|
||||
openApi.setPaths(stripped);
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("unchecked")
|
||||
public OpenApiCustomizer schemaCustomizer() {
|
||||
return openApi -> {
|
||||
var schemas = openApi.getComponents().getSchemas();
|
||||
if (schemas == null) return;
|
||||
|
||||
// Add children to ProcessorNode if missing (recursive self-reference)
|
||||
if (schemas.containsKey("ProcessorNode")) {
|
||||
Schema<Object> processorNode = schemas.get("ProcessorNode");
|
||||
if (processorNode.getProperties() != null
|
||||
&& !processorNode.getProperties().containsKey("children")) {
|
||||
Schema<?> selfRef = new Schema<>().$ref("#/components/schemas/ProcessorNode");
|
||||
ArraySchema childrenArray = new ArraySchema().items(selfRef);
|
||||
processorNode.addProperty("children", childrenArray);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all fields as required for core domain models
|
||||
for (String schemaName : ALL_FIELDS_REQUIRED) {
|
||||
if (schemas.containsKey(schemaName)) {
|
||||
Schema<Object> schema = schemas.get(schemaName);
|
||||
if (schema.getProperties() != null) {
|
||||
schema.setRequired(new ArrayList<>(schema.getProperties().keySet()));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.app.agent.SseConnectionManager;
|
||||
import com.cameleer3.server.app.dto.CommandBroadcastResponse;
|
||||
import com.cameleer3.server.app.dto.CommandRequest;
|
||||
import com.cameleer3.server.app.dto.CommandSingleResponse;
|
||||
import com.cameleer3.server.core.agent.AgentCommand;
|
||||
import com.cameleer3.server.core.agent.AgentInfo;
|
||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer3.server.core.agent.AgentState;
|
||||
import com.cameleer3.server.core.agent.CommandType;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
@@ -24,9 +26,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Command push endpoints for sending commands to agents via SSE.
|
||||
@@ -63,24 +63,21 @@ public class AgentCommandController {
|
||||
@ApiResponse(responseCode = "202", description = "Command accepted")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid command payload")
|
||||
@ApiResponse(responseCode = "404", description = "Agent not registered")
|
||||
public ResponseEntity<String> sendCommand(@PathVariable String id,
|
||||
@RequestBody String body) throws JsonProcessingException {
|
||||
public ResponseEntity<CommandSingleResponse> sendCommand(@PathVariable String id,
|
||||
@RequestBody CommandRequest request) throws JsonProcessingException {
|
||||
AgentInfo agent = registryService.findById(id);
|
||||
if (agent == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Agent not found: " + id);
|
||||
}
|
||||
|
||||
CommandRequest request = parseCommandRequest(body);
|
||||
AgentCommand command = registryService.addCommand(id, request.type, request.payloadJson);
|
||||
CommandType type = mapCommandType(request.type());
|
||||
String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";
|
||||
AgentCommand command = registryService.addCommand(id, type, payloadJson);
|
||||
|
||||
String status = connectionManager.isConnected(id) ? "DELIVERED" : "PENDING";
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("commandId", command.id());
|
||||
response.put("status", status);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED)
|
||||
.body(objectMapper.writeValueAsString(response));
|
||||
.body(new CommandSingleResponse(command.id(), status));
|
||||
}
|
||||
|
||||
@PostMapping("/groups/{group}/commands")
|
||||
@@ -88,9 +85,10 @@ public class AgentCommandController {
|
||||
description = "Sends a command to all LIVE agents in the specified group")
|
||||
@ApiResponse(responseCode = "202", description = "Commands accepted")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid command payload")
|
||||
public ResponseEntity<String> sendGroupCommand(@PathVariable String group,
|
||||
@RequestBody String body) throws JsonProcessingException {
|
||||
CommandRequest request = parseCommandRequest(body);
|
||||
public ResponseEntity<CommandBroadcastResponse> sendGroupCommand(@PathVariable String group,
|
||||
@RequestBody CommandRequest request) throws JsonProcessingException {
|
||||
CommandType type = mapCommandType(request.type());
|
||||
String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";
|
||||
|
||||
List<AgentInfo> agents = registryService.findAll().stream()
|
||||
.filter(a -> a.state() == AgentState.LIVE)
|
||||
@@ -99,16 +97,12 @@ public class AgentCommandController {
|
||||
|
||||
List<String> commandIds = new ArrayList<>();
|
||||
for (AgentInfo agent : agents) {
|
||||
AgentCommand command = registryService.addCommand(agent.id(), request.type, request.payloadJson);
|
||||
AgentCommand command = registryService.addCommand(agent.id(), type, payloadJson);
|
||||
commandIds.add(command.id());
|
||||
}
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("commandIds", commandIds);
|
||||
response.put("targetCount", agents.size());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED)
|
||||
.body(objectMapper.writeValueAsString(response));
|
||||
.body(new CommandBroadcastResponse(commandIds, agents.size()));
|
||||
}
|
||||
|
||||
@PostMapping("/commands")
|
||||
@@ -116,23 +110,20 @@ public class AgentCommandController {
|
||||
description = "Sends a command to all agents currently in LIVE state")
|
||||
@ApiResponse(responseCode = "202", description = "Commands accepted")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid command payload")
|
||||
public ResponseEntity<String> broadcastCommand(@RequestBody String body) throws JsonProcessingException {
|
||||
CommandRequest request = parseCommandRequest(body);
|
||||
public ResponseEntity<CommandBroadcastResponse> broadcastCommand(@RequestBody CommandRequest request) throws JsonProcessingException {
|
||||
CommandType type = mapCommandType(request.type());
|
||||
String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";
|
||||
|
||||
List<AgentInfo> liveAgents = registryService.findByState(AgentState.LIVE);
|
||||
|
||||
List<String> commandIds = new ArrayList<>();
|
||||
for (AgentInfo agent : liveAgents) {
|
||||
AgentCommand command = registryService.addCommand(agent.id(), request.type, request.payloadJson);
|
||||
AgentCommand command = registryService.addCommand(agent.id(), type, payloadJson);
|
||||
commandIds.add(command.id());
|
||||
}
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("commandIds", commandIds);
|
||||
response.put("targetCount", liveAgents.size());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED)
|
||||
.body(objectMapper.writeValueAsString(response));
|
||||
.body(new CommandBroadcastResponse(commandIds, liveAgents.size()));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/commands/{commandId}/ack")
|
||||
@@ -149,24 +140,6 @@ public class AgentCommandController {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private CommandRequest parseCommandRequest(String body) throws JsonProcessingException {
|
||||
JsonNode node = objectMapper.readTree(body);
|
||||
|
||||
if (!node.has("type")) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing 'type' field");
|
||||
}
|
||||
|
||||
String typeStr = node.get("type").asText();
|
||||
CommandType type = mapCommandType(typeStr);
|
||||
|
||||
String payloadJson = "{}";
|
||||
if (node.has("payload")) {
|
||||
payloadJson = node.get("payload").toString();
|
||||
}
|
||||
|
||||
return new CommandRequest(type, payloadJson);
|
||||
}
|
||||
|
||||
private CommandType mapCommandType(String typeStr) {
|
||||
return switch (typeStr) {
|
||||
case "config-update" -> CommandType.CONFIG_UPDATE;
|
||||
@@ -176,6 +149,4 @@ public class AgentCommandController {
|
||||
"Invalid command type: " + typeStr + ". Valid: config-update, deep-trace, replay");
|
||||
};
|
||||
}
|
||||
|
||||
private record CommandRequest(CommandType type, String payloadJson) {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.app.config.AgentRegistryConfig;
|
||||
import com.cameleer3.server.app.dto.AgentInstanceResponse;
|
||||
import com.cameleer3.server.app.dto.AgentRefreshRequest;
|
||||
import com.cameleer3.server.app.dto.AgentRefreshResponse;
|
||||
import com.cameleer3.server.app.dto.AgentRegistrationRequest;
|
||||
import com.cameleer3.server.app.dto.AgentRegistrationResponse;
|
||||
import com.cameleer3.server.app.dto.ErrorResponse;
|
||||
import com.cameleer3.server.app.security.BootstrapTokenValidator;
|
||||
import com.cameleer3.server.core.agent.AgentInfo;
|
||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||
@@ -8,10 +14,9 @@ import com.cameleer3.server.core.agent.AgentState;
|
||||
import com.cameleer3.server.core.security.Ed25519SigningService;
|
||||
import com.cameleer3.server.core.security.InvalidTokenException;
|
||||
import com.cameleer3.server.core.security.JwtService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -26,12 +31,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent registration, heartbeat, listing, and token refresh endpoints.
|
||||
@@ -46,20 +47,17 @@ public class AgentRegistrationController {
|
||||
|
||||
private final AgentRegistryService registryService;
|
||||
private final AgentRegistryConfig config;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final BootstrapTokenValidator bootstrapTokenValidator;
|
||||
private final JwtService jwtService;
|
||||
private final Ed25519SigningService ed25519SigningService;
|
||||
|
||||
public AgentRegistrationController(AgentRegistryService registryService,
|
||||
AgentRegistryConfig config,
|
||||
ObjectMapper objectMapper,
|
||||
BootstrapTokenValidator bootstrapTokenValidator,
|
||||
JwtService jwtService,
|
||||
Ed25519SigningService ed25519SigningService) {
|
||||
this.registryService = registryService;
|
||||
this.config = config;
|
||||
this.objectMapper = objectMapper;
|
||||
this.bootstrapTokenValidator = bootstrapTokenValidator;
|
||||
this.jwtService = jwtService;
|
||||
this.ed25519SigningService = ed25519SigningService;
|
||||
@@ -70,12 +68,14 @@ public class AgentRegistrationController {
|
||||
description = "Registers a new agent or re-registers an existing one. "
|
||||
+ "Requires bootstrap token in Authorization header.")
|
||||
@ApiResponse(responseCode = "200", description = "Agent registered successfully")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid registration payload")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid registration payload",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@ApiResponse(responseCode = "401", description = "Missing or invalid bootstrap token")
|
||||
public ResponseEntity<String> register(@RequestBody String body,
|
||||
HttpServletRequest request) throws JsonProcessingException {
|
||||
public ResponseEntity<AgentRegistrationResponse> register(
|
||||
@RequestBody AgentRegistrationRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
// Validate bootstrap token
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
String authHeader = httpRequest.getHeader("Authorization");
|
||||
String bootstrapToken = null;
|
||||
if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
|
||||
bootstrapToken = authHeader.substring(BEARER_PREFIX.length());
|
||||
@@ -84,52 +84,32 @@ public class AgentRegistrationController {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
|
||||
JsonNode node = objectMapper.readTree(body);
|
||||
|
||||
String agentId = getRequiredField(node, "agentId");
|
||||
String name = getRequiredField(node, "name");
|
||||
if (agentId == null || name == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("{\"error\":\"agentId and name are required\"}");
|
||||
if (request.agentId() == null || request.agentId().isBlank()
|
||||
|| request.name() == null || request.name().isBlank()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
String group = node.has("group") ? node.get("group").asText() : "default";
|
||||
String version = node.has("version") ? node.get("version").asText() : null;
|
||||
String group = request.group() != null ? request.group() : "default";
|
||||
List<String> routeIds = request.routeIds() != null ? request.routeIds() : List.of();
|
||||
var capabilities = request.capabilities() != null ? request.capabilities() : Collections.<String, Object>emptyMap();
|
||||
|
||||
List<String> routeIds = new ArrayList<>();
|
||||
if (node.has("routeIds") && node.get("routeIds").isArray()) {
|
||||
for (JsonNode rid : node.get("routeIds")) {
|
||||
routeIds.add(rid.asText());
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> capabilities = Collections.emptyMap();
|
||||
if (node.has("capabilities") && node.get("capabilities").isObject()) {
|
||||
capabilities = new LinkedHashMap<>();
|
||||
Iterator<Map.Entry<String, JsonNode>> fields = node.get("capabilities").fields();
|
||||
while (fields.hasNext()) {
|
||||
Map.Entry<String, JsonNode> field = fields.next();
|
||||
capabilities.put(field.getKey(), parseJsonValue(field.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
AgentInfo agent = registryService.register(agentId, name, group, version, routeIds, capabilities);
|
||||
log.info("Agent registered: {} (name={}, group={})", agentId, name, group);
|
||||
AgentInfo agent = registryService.register(
|
||||
request.agentId(), request.name(), group, request.version(), routeIds, capabilities);
|
||||
log.info("Agent registered: {} (name={}, group={})", request.agentId(), request.name(), group);
|
||||
|
||||
// Issue JWT tokens with AGENT role
|
||||
java.util.List<String> roles = java.util.List.of("AGENT");
|
||||
String accessToken = jwtService.createAccessToken(agentId, group, roles);
|
||||
String refreshToken = jwtService.createRefreshToken(agentId, group, roles);
|
||||
List<String> roles = List.of("AGENT");
|
||||
String accessToken = jwtService.createAccessToken(request.agentId(), group, roles);
|
||||
String refreshToken = jwtService.createRefreshToken(request.agentId(), group, roles);
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("agentId", agent.id());
|
||||
response.put("sseEndpoint", "/api/v1/agents/" + agentId + "/events");
|
||||
response.put("heartbeatIntervalMs", config.getHeartbeatIntervalMs());
|
||||
response.put("serverPublicKey", ed25519SigningService.getPublicKeyBase64());
|
||||
response.put("accessToken", accessToken);
|
||||
response.put("refreshToken", refreshToken);
|
||||
|
||||
return ResponseEntity.ok(objectMapper.writeValueAsString(response));
|
||||
return ResponseEntity.ok(new AgentRegistrationResponse(
|
||||
agent.id(),
|
||||
"/api/v1/agents/" + agent.id() + "/events",
|
||||
config.getHeartbeatIntervalMs(),
|
||||
ed25519SigningService.getPublicKeyBase64(),
|
||||
accessToken,
|
||||
refreshToken
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/refresh")
|
||||
@@ -138,19 +118,16 @@ public class AgentRegistrationController {
|
||||
@ApiResponse(responseCode = "200", description = "New access token issued")
|
||||
@ApiResponse(responseCode = "401", description = "Invalid or expired refresh token")
|
||||
@ApiResponse(responseCode = "404", description = "Agent not found")
|
||||
public ResponseEntity<String> refresh(@PathVariable String id,
|
||||
@RequestBody String body) throws JsonProcessingException {
|
||||
JsonNode node = objectMapper.readTree(body);
|
||||
String refreshToken = node.has("refreshToken") ? node.get("refreshToken").asText() : null;
|
||||
|
||||
if (refreshToken == null || refreshToken.isBlank()) {
|
||||
public ResponseEntity<AgentRefreshResponse> refresh(@PathVariable String id,
|
||||
@RequestBody AgentRefreshRequest request) {
|
||||
if (request.refreshToken() == null || request.refreshToken().isBlank()) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
|
||||
// Validate refresh token
|
||||
com.cameleer3.server.core.security.JwtService.JwtValidationResult result;
|
||||
JwtService.JwtValidationResult result;
|
||||
try {
|
||||
result = jwtService.validateRefreshToken(refreshToken);
|
||||
result = jwtService.validateRefreshToken(request.refreshToken());
|
||||
} catch (InvalidTokenException e) {
|
||||
log.debug("Refresh token validation failed: {}", e.getMessage());
|
||||
return ResponseEntity.status(401).build();
|
||||
@@ -171,14 +148,11 @@ public class AgentRegistrationController {
|
||||
}
|
||||
|
||||
// Preserve roles from refresh token
|
||||
java.util.List<String> roles = result.roles().isEmpty()
|
||||
? java.util.List.of("AGENT") : result.roles();
|
||||
List<String> roles = result.roles().isEmpty()
|
||||
? List.of("AGENT") : result.roles();
|
||||
String newAccessToken = jwtService.createAccessToken(agentId, agent.group(), roles);
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("accessToken", newAccessToken);
|
||||
|
||||
return ResponseEntity.ok(objectMapper.writeValueAsString(response));
|
||||
return ResponseEntity.ok(new AgentRefreshResponse(newAccessToken));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/heartbeat")
|
||||
@@ -198,9 +172,10 @@ public class AgentRegistrationController {
|
||||
@Operation(summary = "List all agents",
|
||||
description = "Returns all registered agents, optionally filtered by status")
|
||||
@ApiResponse(responseCode = "200", description = "Agent list returned")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid status filter")
|
||||
public ResponseEntity<String> listAgents(
|
||||
@RequestParam(required = false) String status) throws JsonProcessingException {
|
||||
@ApiResponse(responseCode = "400", description = "Invalid status filter",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public ResponseEntity<List<AgentInstanceResponse>> listAgents(
|
||||
@RequestParam(required = false) String status) {
|
||||
List<AgentInfo> agents;
|
||||
|
||||
if (status != null) {
|
||||
@@ -208,29 +183,15 @@ public class AgentRegistrationController {
|
||||
AgentState stateFilter = AgentState.valueOf(status.toUpperCase());
|
||||
agents = registryService.findByState(stateFilter);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("{\"error\":\"Invalid status: " + status + ". Valid values: LIVE, STALE, DEAD\"}");
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
} else {
|
||||
agents = registryService.findAll();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(objectMapper.writeValueAsString(agents));
|
||||
}
|
||||
|
||||
private String getRequiredField(JsonNode node, String fieldName) {
|
||||
if (!node.has(fieldName) || node.get(fieldName).isNull() || node.get(fieldName).asText().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return node.get(fieldName).asText();
|
||||
}
|
||||
|
||||
private Object parseJsonValue(JsonNode node) {
|
||||
if (node.isBoolean()) return node.asBoolean();
|
||||
if (node.isInt()) return node.asInt();
|
||||
if (node.isLong()) return node.asLong();
|
||||
if (node.isDouble()) return node.asDouble();
|
||||
if (node.isTextual()) return node.asText();
|
||||
return node.toString();
|
||||
List<AgentInstanceResponse> response = agents.stream()
|
||||
.map(AgentInstanceResponse::from)
|
||||
.toList();
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.app.dto.ErrorResponse;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
/**
|
||||
* Global exception handler that ensures error responses use the typed {@link ErrorResponse} schema.
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class ApiExceptionHandler {
|
||||
|
||||
@ExceptionHandler(ResponseStatusException.class)
|
||||
public ResponseEntity<ErrorResponse> handleResponseStatus(ResponseStatusException ex) {
|
||||
return ResponseEntity.status(ex.getStatusCode())
|
||||
.body(new ErrorResponse(ex.getReason() != null ? ex.getReason() : "Unknown error"));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.cameleer3.server.app.storage.ClickHouseExecutionRepository;
|
||||
import com.cameleer3.server.core.detail.DetailService;
|
||||
import com.cameleer3.server.core.detail.ExecutionDetail;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -36,6 +37,8 @@ public class DetailController {
|
||||
|
||||
@GetMapping("/{executionId}")
|
||||
@Operation(summary = "Get execution detail with nested processor tree")
|
||||
@ApiResponse(responseCode = "200", description = "Execution detail found")
|
||||
@ApiResponse(responseCode = "404", description = "Execution not found")
|
||||
public ResponseEntity<ExecutionDetail> getDetail(@PathVariable String executionId) {
|
||||
return detailService.getDetail(executionId)
|
||||
.map(ResponseEntity::ok)
|
||||
@@ -44,6 +47,8 @@ public class DetailController {
|
||||
|
||||
@GetMapping("/{executionId}/processors/{index}/snapshot")
|
||||
@Operation(summary = "Get exchange snapshot for a specific processor")
|
||||
@ApiResponse(responseCode = "200", description = "Snapshot data")
|
||||
@ApiResponse(responseCode = "404", description = "Snapshot not found")
|
||||
public ResponseEntity<Map<String, String>> getProcessorSnapshot(
|
||||
@PathVariable String executionId,
|
||||
@PathVariable int index) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.cameleer3.server.core.diagram.DiagramLayout;
|
||||
import com.cameleer3.server.core.diagram.DiagramRenderer;
|
||||
import com.cameleer3.server.core.storage.DiagramRepository;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -45,7 +47,11 @@ public class DiagramRenderController {
|
||||
@GetMapping("/{contentHash}/render")
|
||||
@Operation(summary = "Render a route diagram",
|
||||
description = "Returns SVG (default) or JSON layout based on Accept header")
|
||||
@ApiResponse(responseCode = "200", description = "Diagram rendered successfully")
|
||||
@ApiResponse(responseCode = "200", description = "Diagram rendered successfully",
|
||||
content = {
|
||||
@Content(mediaType = "image/svg+xml", schema = @Schema(type = "string")),
|
||||
@Content(mediaType = "application/json", schema = @Schema(implementation = DiagramLayout.class))
|
||||
})
|
||||
@ApiResponse(responseCode = "404", description = "Diagram not found")
|
||||
public ResponseEntity<?> renderDiagram(
|
||||
@PathVariable String contentHash,
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.app.dto.ErrorResponse;
|
||||
import com.cameleer3.server.app.dto.OidcAdminConfigRequest;
|
||||
import com.cameleer3.server.app.dto.OidcAdminConfigResponse;
|
||||
import com.cameleer3.server.app.dto.OidcTestResult;
|
||||
import com.cameleer3.server.app.security.OidcTokenExchanger;
|
||||
import com.cameleer3.server.core.security.OidcConfig;
|
||||
import com.cameleer3.server.core.security.OidcConfigRepository;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -16,10 +23,9 @@ import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -45,19 +51,20 @@ public class OidcConfigAdminController {
|
||||
@GetMapping
|
||||
@Operation(summary = "Get OIDC configuration")
|
||||
@ApiResponse(responseCode = "200", description = "Current OIDC configuration (client_secret masked)")
|
||||
public ResponseEntity<?> getConfig() {
|
||||
public ResponseEntity<OidcAdminConfigResponse> getConfig() {
|
||||
Optional<OidcConfig> config = configRepository.find();
|
||||
if (config.isEmpty()) {
|
||||
return ResponseEntity.ok(Map.of("configured", false));
|
||||
return ResponseEntity.ok(OidcAdminConfigResponse.unconfigured());
|
||||
}
|
||||
return ResponseEntity.ok(toResponse(config.get()));
|
||||
return ResponseEntity.ok(OidcAdminConfigResponse.from(config.get()));
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "Save OIDC configuration")
|
||||
@ApiResponse(responseCode = "200", description = "Configuration saved")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid configuration")
|
||||
public ResponseEntity<?> saveConfig(@RequestBody OidcConfigRequest request) {
|
||||
@ApiResponse(responseCode = "400", description = "Invalid configuration",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public ResponseEntity<OidcAdminConfigResponse> saveConfig(@RequestBody OidcAdminConfigRequest request) {
|
||||
// Resolve client_secret: if masked or empty, preserve existing
|
||||
String clientSecret = request.clientSecret();
|
||||
if (clientSecret == null || clientSecret.isBlank() || clientSecret.equals("********")) {
|
||||
@@ -66,12 +73,12 @@ public class OidcConfigAdminController {
|
||||
}
|
||||
|
||||
if (request.enabled() && (request.issuerUri() == null || request.issuerUri().isBlank())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "issuerUri is required when OIDC is enabled"));
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"issuerUri is required when OIDC is enabled");
|
||||
}
|
||||
if (request.enabled() && (request.clientId() == null || request.clientId().isBlank())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "clientId is required when OIDC is enabled"));
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"clientId is required when OIDC is enabled");
|
||||
}
|
||||
|
||||
OidcConfig config = new OidcConfig(
|
||||
@@ -88,31 +95,29 @@ public class OidcConfigAdminController {
|
||||
tokenExchanger.invalidateCache();
|
||||
|
||||
log.info("OIDC configuration updated: enabled={}, issuer={}", config.enabled(), config.issuerUri());
|
||||
return ResponseEntity.ok(toResponse(config));
|
||||
return ResponseEntity.ok(OidcAdminConfigResponse.from(config));
|
||||
}
|
||||
|
||||
@PostMapping("/test")
|
||||
@Operation(summary = "Test OIDC provider connectivity")
|
||||
@ApiResponse(responseCode = "200", description = "Provider reachable")
|
||||
@ApiResponse(responseCode = "400", description = "Provider unreachable or misconfigured")
|
||||
public ResponseEntity<?> testConnection() {
|
||||
@ApiResponse(responseCode = "400", description = "Provider unreachable or misconfigured",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public ResponseEntity<OidcTestResult> testConnection() {
|
||||
Optional<OidcConfig> config = configRepository.find();
|
||||
if (config.isEmpty() || !config.get().enabled()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "OIDC is not configured or disabled"));
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"OIDC is not configured or disabled");
|
||||
}
|
||||
|
||||
try {
|
||||
tokenExchanger.invalidateCache();
|
||||
String authEndpoint = tokenExchanger.getAuthorizationEndpoint();
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"status", "ok",
|
||||
"authorizationEndpoint", authEndpoint
|
||||
));
|
||||
return ResponseEntity.ok(new OidcTestResult("ok", authEndpoint));
|
||||
} catch (Exception e) {
|
||||
log.warn("OIDC connectivity test failed: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "Failed to reach OIDC provider: " + e.getMessage()));
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Failed to reach OIDC provider: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,27 +130,4 @@ public class OidcConfigAdminController {
|
||||
log.info("OIDC configuration deleted");
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
private Map<String, Object> toResponse(OidcConfig config) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("configured", true);
|
||||
map.put("enabled", config.enabled());
|
||||
map.put("issuerUri", config.issuerUri());
|
||||
map.put("clientId", config.clientId());
|
||||
map.put("clientSecretSet", !config.clientSecret().isBlank());
|
||||
map.put("rolesClaim", config.rolesClaim());
|
||||
map.put("defaultRoles", config.defaultRoles());
|
||||
map.put("autoSignup", config.autoSignup());
|
||||
return map;
|
||||
}
|
||||
|
||||
public record OidcConfigRequest(
|
||||
boolean enabled,
|
||||
String issuerUri,
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
String rolesClaim,
|
||||
List<String> defaultRoles,
|
||||
boolean autoSignup
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import com.cameleer3.server.core.agent.AgentInfo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "Agent instance summary")
|
||||
public record AgentInstanceResponse(
|
||||
@NotNull String id,
|
||||
@NotNull String name,
|
||||
@NotNull String group,
|
||||
@NotNull String status,
|
||||
@NotNull List<String> routeIds,
|
||||
@NotNull Instant registeredAt,
|
||||
@NotNull Instant lastHeartbeat
|
||||
) {
|
||||
public static AgentInstanceResponse from(AgentInfo info) {
|
||||
return new AgentInstanceResponse(
|
||||
info.id(), info.name(), info.group(),
|
||||
info.state().name(), info.routeIds(),
|
||||
info.registeredAt(), info.lastHeartbeat()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "Agent token refresh request")
|
||||
public record AgentRefreshRequest(@NotNull String refreshToken) {}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "Refreshed access token")
|
||||
public record AgentRefreshResponse(@NotNull String accessToken) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "Agent registration payload")
|
||||
public record AgentRegistrationRequest(
|
||||
@NotNull String agentId,
|
||||
@NotNull String name,
|
||||
@Schema(defaultValue = "default") String group,
|
||||
String version,
|
||||
List<String> routeIds,
|
||||
Map<String, Object> capabilities
|
||||
) {}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "Agent registration result with JWT tokens and SSE endpoint")
|
||||
public record AgentRegistrationResponse(
|
||||
@NotNull String agentId,
|
||||
@NotNull String sseEndpoint,
|
||||
long heartbeatIntervalMs,
|
||||
@NotNull String serverPublicKey,
|
||||
@NotNull String accessToken,
|
||||
@NotNull String refreshToken
|
||||
) {}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "JWT token pair")
|
||||
public record AuthTokenResponse(
|
||||
@NotNull String accessToken,
|
||||
@NotNull String refreshToken
|
||||
) {}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "Result of broadcasting a command to multiple agents")
|
||||
public record CommandBroadcastResponse(
|
||||
@NotNull List<String> commandIds,
|
||||
int targetCount
|
||||
) {}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "Command to send to agent(s)")
|
||||
public record CommandRequest(
|
||||
@NotNull @Schema(description = "Command type: config-update, deep-trace, or replay")
|
||||
String type,
|
||||
@Schema(description = "Command payload JSON")
|
||||
Object payload
|
||||
) {}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "Result of sending a command to a single agent")
|
||||
public record CommandSingleResponse(
|
||||
@NotNull String commandId,
|
||||
@NotNull String status
|
||||
) {}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "Error response")
|
||||
public record ErrorResponse(@NotNull String message) {}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "OIDC configuration update request")
|
||||
public record OidcAdminConfigRequest(
|
||||
boolean enabled,
|
||||
String issuerUri,
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
String rolesClaim,
|
||||
List<String> defaultRoles,
|
||||
boolean autoSignup
|
||||
) {}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import com.cameleer3.server.core.security.OidcConfig;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "OIDC configuration for admin management")
|
||||
public record OidcAdminConfigResponse(
|
||||
boolean configured,
|
||||
boolean enabled,
|
||||
String issuerUri,
|
||||
String clientId,
|
||||
boolean clientSecretSet,
|
||||
String rolesClaim,
|
||||
List<String> defaultRoles,
|
||||
boolean autoSignup
|
||||
) {
|
||||
public static OidcAdminConfigResponse unconfigured() {
|
||||
return new OidcAdminConfigResponse(false, false, null, null, false, null, null, false);
|
||||
}
|
||||
|
||||
public static OidcAdminConfigResponse from(OidcConfig config) {
|
||||
return new OidcAdminConfigResponse(
|
||||
true, config.enabled(), config.issuerUri(), config.clientId(),
|
||||
!config.clientSecret().isBlank(), config.rolesClaim(),
|
||||
config.defaultRoles(), config.autoSignup()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "OIDC configuration for SPA login flow")
|
||||
public record OidcPublicConfigResponse(
|
||||
@NotNull String issuer,
|
||||
@NotNull String clientId,
|
||||
@NotNull String authorizationEndpoint,
|
||||
@Schema(description = "Present if the provider supports RP-initiated logout")
|
||||
String endSessionEndpoint
|
||||
) {}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "OIDC provider connectivity test result")
|
||||
public record OidcTestResult(
|
||||
@NotNull String status,
|
||||
@NotNull String authorizationEndpoint
|
||||
) {}
|
||||
@@ -1,24 +1,32 @@
|
||||
package com.cameleer3.server.app.security;
|
||||
|
||||
import com.cameleer3.server.app.dto.AuthTokenResponse;
|
||||
import com.cameleer3.server.app.dto.ErrorResponse;
|
||||
import com.cameleer3.server.app.dto.OidcPublicConfigResponse;
|
||||
import com.cameleer3.server.core.security.JwtService;
|
||||
import com.cameleer3.server.core.security.OidcConfig;
|
||||
import com.cameleer3.server.core.security.OidcConfigRepository;
|
||||
import com.cameleer3.server.core.security.UserInfo;
|
||||
import com.cameleer3.server.core.security.UserRepository;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -29,6 +37,7 @@ import java.util.Optional;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth/oidc")
|
||||
@Tag(name = "Authentication", description = "Login and token refresh endpoints")
|
||||
public class OidcAuthController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OidcAuthController.class);
|
||||
@@ -53,7 +62,12 @@ public class OidcAuthController {
|
||||
* Returns 404 if OIDC is not configured or disabled.
|
||||
*/
|
||||
@GetMapping("/config")
|
||||
public ResponseEntity<?> getConfig() {
|
||||
@Operation(summary = "Get OIDC config for SPA login flow")
|
||||
@ApiResponse(responseCode = "200", description = "OIDC configuration")
|
||||
@ApiResponse(responseCode = "404", description = "OIDC not configured or disabled")
|
||||
@ApiResponse(responseCode = "500", description = "Failed to retrieve OIDC provider metadata",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public ResponseEntity<OidcPublicConfigResponse> getConfig() {
|
||||
Optional<OidcConfig> config = configRepository.find();
|
||||
if (config.isEmpty() || !config.get().enabled()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -61,19 +75,17 @@ public class OidcAuthController {
|
||||
|
||||
try {
|
||||
OidcConfig oidc = config.get();
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("issuer", oidc.issuerUri());
|
||||
response.put("clientId", oidc.clientId());
|
||||
response.put("authorizationEndpoint", tokenExchanger.getAuthorizationEndpoint());
|
||||
String endSessionEndpoint = tokenExchanger.getEndSessionEndpoint();
|
||||
if (endSessionEndpoint != null) {
|
||||
response.put("endSessionEndpoint", endSessionEndpoint);
|
||||
}
|
||||
return ResponseEntity.ok(response);
|
||||
return ResponseEntity.ok(new OidcPublicConfigResponse(
|
||||
oidc.issuerUri(),
|
||||
oidc.clientId(),
|
||||
tokenExchanger.getAuthorizationEndpoint(),
|
||||
endSessionEndpoint
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to retrieve OIDC provider metadata: {}", e.getMessage());
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("message", "Failed to retrieve OIDC provider metadata"));
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"Failed to retrieve OIDC provider metadata");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +93,14 @@ public class OidcAuthController {
|
||||
* Exchanges an OIDC authorization code for internal Cameleer JWTs.
|
||||
*/
|
||||
@PostMapping("/callback")
|
||||
public ResponseEntity<?> callback(@RequestBody CallbackRequest request) {
|
||||
@Operation(summary = "Exchange OIDC authorization code for JWTs")
|
||||
@ApiResponse(responseCode = "200", description = "Authentication successful")
|
||||
@ApiResponse(responseCode = "401", description = "OIDC authentication failed",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@ApiResponse(responseCode = "403", description = "Account not provisioned",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
@ApiResponse(responseCode = "404", description = "OIDC not configured or disabled")
|
||||
public ResponseEntity<AuthTokenResponse> callback(@RequestBody CallbackRequest request) {
|
||||
Optional<OidcConfig> config = configRepository.find();
|
||||
if (config.isEmpty() || !config.get().enabled()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -98,8 +117,8 @@ public class OidcAuthController {
|
||||
// Check auto-signup gate: if disabled, user must already exist
|
||||
Optional<UserInfo> existingUser = userRepository.findById(userId);
|
||||
if (!config.get().autoSignup() && existingUser.isEmpty()) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(Map.of("message", "Account not provisioned. Contact your administrator."));
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN,
|
||||
"Account not provisioned. Contact your administrator.");
|
||||
}
|
||||
|
||||
// Resolve roles: DB override > OIDC claim > default
|
||||
@@ -111,14 +130,13 @@ public class OidcAuthController {
|
||||
String accessToken = jwtService.createAccessToken(userId, "ui", roles);
|
||||
String refreshToken = jwtService.createRefreshToken(userId, "ui", roles);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"accessToken", accessToken,
|
||||
"refreshToken", refreshToken
|
||||
));
|
||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken));
|
||||
} catch (ResponseStatusException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("OIDC callback failed: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(401)
|
||||
.body(Map.of("message", "OIDC authentication failed: " + e.getMessage()));
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
|
||||
"OIDC authentication failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
package com.cameleer3.server.app.security;
|
||||
|
||||
import com.cameleer3.server.app.dto.AuthTokenResponse;
|
||||
import com.cameleer3.server.app.dto.ErrorResponse;
|
||||
import com.cameleer3.server.core.security.JwtService;
|
||||
import com.cameleer3.server.core.security.JwtService.JwtValidationResult;
|
||||
import com.cameleer3.server.core.security.UserInfo;
|
||||
import com.cameleer3.server.core.security.UserRepository;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Authentication endpoints for the UI (local credentials).
|
||||
@@ -25,6 +33,7 @@ import java.util.Map;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
@Tag(name = "Authentication", description = "Login and token refresh endpoints")
|
||||
public class UiAuthController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(UiAuthController.class);
|
||||
@@ -41,20 +50,24 @@ public class UiAuthController {
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
|
||||
@Operation(summary = "Login with local credentials")
|
||||
@ApiResponse(responseCode = "200", description = "Login successful")
|
||||
@ApiResponse(responseCode = "401", description = "Invalid credentials",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public ResponseEntity<AuthTokenResponse> login(@RequestBody LoginRequest request) {
|
||||
String configuredUser = properties.getUiUser();
|
||||
String configuredPassword = properties.getUiPassword();
|
||||
|
||||
if (configuredUser == null || configuredUser.isBlank()
|
||||
|| configuredPassword == null || configuredPassword.isBlank()) {
|
||||
log.warn("UI authentication attempted but CAMELEER_UI_USER / CAMELEER_UI_PASSWORD not configured");
|
||||
return ResponseEntity.status(401).body(Map.of("message", "UI authentication not configured"));
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "UI authentication not configured");
|
||||
}
|
||||
|
||||
if (!configuredUser.equals(request.username())
|
||||
|| !configuredPassword.equals(request.password())) {
|
||||
log.debug("UI login failed for user: {}", request.username());
|
||||
return ResponseEntity.status(401).body(Map.of("message", "Invalid credentials"));
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials");
|
||||
}
|
||||
|
||||
String subject = "ui:" + request.username();
|
||||
@@ -72,18 +85,19 @@ public class UiAuthController {
|
||||
String refreshToken = jwtService.createRefreshToken(subject, "ui", roles);
|
||||
|
||||
log.info("UI user logged in: {}", request.username());
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"accessToken", accessToken,
|
||||
"refreshToken", refreshToken
|
||||
));
|
||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken));
|
||||
}
|
||||
|
||||
@PostMapping("/refresh")
|
||||
public ResponseEntity<?> refresh(@RequestBody RefreshRequest request) {
|
||||
@Operation(summary = "Refresh access token")
|
||||
@ApiResponse(responseCode = "200", description = "Token refreshed")
|
||||
@ApiResponse(responseCode = "401", description = "Invalid refresh token",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public ResponseEntity<AuthTokenResponse> refresh(@RequestBody RefreshRequest request) {
|
||||
try {
|
||||
JwtValidationResult result = jwtService.validateRefreshToken(request.refreshToken());
|
||||
if (!result.subject().startsWith("ui:")) {
|
||||
return ResponseEntity.status(401).body(Map.of("message", "Not a UI token"));
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Not a UI token");
|
||||
}
|
||||
|
||||
// Preserve roles from the refresh token
|
||||
@@ -91,13 +105,12 @@ public class UiAuthController {
|
||||
String accessToken = jwtService.createAccessToken(result.subject(), "ui", roles);
|
||||
String refreshToken = jwtService.createRefreshToken(result.subject(), "ui", roles);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"accessToken", accessToken,
|
||||
"refreshToken", refreshToken
|
||||
));
|
||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken));
|
||||
} catch (ResponseStatusException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.debug("UI token refresh failed: {}", e.getMessage());
|
||||
return ResponseEntity.status(401).body(Map.of("message", "Invalid refresh token"));
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid refresh token");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class AgentRegistrationControllerIT extends AbstractClickHouseIT {
|
||||
JsonNode body = objectMapper.readTree(response.getBody());
|
||||
assertThat(body.isArray()).isTrue();
|
||||
for (JsonNode agent : body) {
|
||||
assertThat(agent.get("state").asText()).isEqualTo("LIVE");
|
||||
assertThat(agent.get("status").asText()).isEqualTo("LIVE");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
73
ui/README.md
Normal file
73
ui/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
@@ -8,7 +8,8 @@
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"generate-api": "openapi-typescript http://localhost:8081/api/v1/api-docs -o src/api/schema.d.ts"
|
||||
"generate-api": "openapi-typescript src/api/openapi.json -o src/api/schema.d.ts",
|
||||
"generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "OpenAPI definition",
|
||||
"version": "v0"
|
||||
"title": "Cameleer3 Server API",
|
||||
"version": "1.0"
|
||||
},
|
||||
"servers": [],
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "Agent SSE",
|
||||
@@ -26,6 +31,10 @@
|
||||
"name": "Ingestion",
|
||||
"description": "Data ingestion endpoints"
|
||||
},
|
||||
{
|
||||
"name": "Authentication",
|
||||
"description": "Login and token refresh endpoints"
|
||||
},
|
||||
{
|
||||
"name": "OIDC Config Admin",
|
||||
"description": "OIDC provider configuration (ADMIN only)"
|
||||
@@ -94,7 +103,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/OidcAdminConfigResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,7 +120,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OidcConfigRequest"
|
||||
"$ref": "#/components/schemas/OidcAdminConfigRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -123,7 +132,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/OidcAdminConfigResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +142,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,8 +387,9 @@
|
||||
"/auth/refresh": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"ui-auth-controller"
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Refresh access token",
|
||||
"operationId": "refresh",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -393,11 +403,21 @@
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "Token refreshed",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/AuthTokenResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid refresh token",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,8 +428,9 @@
|
||||
"/auth/oidc/callback": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"oidc-auth-controller"
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Exchange OIDC authorization code for JWTs",
|
||||
"operationId": "callback",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -423,11 +444,41 @@
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "Authentication successful",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/AuthTokenResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "OIDC authentication failed",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Account not provisioned",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "OIDC not configured or disabled",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AuthTokenResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -438,8 +489,9 @@
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"ui-auth-controller"
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Login with local credentials",
|
||||
"operationId": "login",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -453,11 +505,21 @@
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "Login successful",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/AuthTokenResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid credentials",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -487,7 +549,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/AgentRefreshRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -499,7 +561,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/AgentRefreshResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -509,7 +571,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/AgentRefreshResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,7 +581,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/AgentRefreshResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -577,7 +639,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/CommandRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -589,7 +651,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/CommandSingleResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -599,7 +661,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/CommandSingleResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -609,7 +671,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/CommandSingleResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -665,7 +727,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/AgentRegistrationRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -677,7 +739,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/AgentRegistrationResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -687,7 +749,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -697,7 +759,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/AgentRegistrationResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -727,7 +789,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/CommandRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -739,7 +801,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/CommandBroadcastResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -749,7 +811,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/CommandBroadcastResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -769,7 +831,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/CommandRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -781,7 +843,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/CommandBroadcastResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -791,7 +853,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/CommandBroadcastResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -812,7 +874,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/OidcTestResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -822,7 +884,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -941,7 +1003,17 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "Execution detail found",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ExecutionDetail"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Execution not found",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
@@ -981,7 +1053,20 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "Snapshot data",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Snapshot not found",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
@@ -1018,9 +1103,14 @@
|
||||
"200": {
|
||||
"description": "Diagram rendered successfully",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"image/svg+xml": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DiagramLayout"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1041,16 +1131,37 @@
|
||||
"/auth/oidc/config": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"oidc-auth-controller"
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Get OIDC config for SPA login flow",
|
||||
"operationId": "getConfig_1",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "OIDC configuration",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/OidcPublicConfigResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "OIDC not configured or disabled",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OidcPublicConfigResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Failed to retrieve OIDC provider metadata",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1082,7 +1193,10 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AgentInstanceResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1092,7 +1206,7 @@
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1252,8 +1366,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"OidcConfigRequest": {
|
||||
"OidcAdminConfigRequest": {
|
||||
"type": "object",
|
||||
"description": "OIDC configuration update request",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
@@ -1281,6 +1396,51 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ErrorResponse": {
|
||||
"type": "object",
|
||||
"description": "Error response",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
]
|
||||
},
|
||||
"OidcAdminConfigResponse": {
|
||||
"type": "object",
|
||||
"description": "OIDC configuration for admin management",
|
||||
"properties": {
|
||||
"configured": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"issuerUri": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecretSet": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rolesClaim": {
|
||||
"type": "string"
|
||||
},
|
||||
"defaultRoles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"autoSignup": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SearchRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1373,7 +1533,19 @@
|
||||
"diagramContentHash": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"correlationId",
|
||||
"diagramContentHash",
|
||||
"durationMs",
|
||||
"endTime",
|
||||
"errorMessage",
|
||||
"executionId",
|
||||
"routeId",
|
||||
"startTime",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"SearchResultExecutionSummary": {
|
||||
"type": "object",
|
||||
@@ -1396,7 +1568,13 @@
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data",
|
||||
"limit",
|
||||
"offset",
|
||||
"total"
|
||||
]
|
||||
},
|
||||
"RefreshRequest": {
|
||||
"type": "object",
|
||||
@@ -1406,6 +1584,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"AuthTokenResponse": {
|
||||
"type": "object",
|
||||
"description": "JWT token pair",
|
||||
"properties": {
|
||||
"accessToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"refreshToken": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accessToken",
|
||||
"refreshToken"
|
||||
]
|
||||
},
|
||||
"CallbackRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1428,6 +1622,165 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"AgentRefreshRequest": {
|
||||
"type": "object",
|
||||
"description": "Agent token refresh request",
|
||||
"properties": {
|
||||
"refreshToken": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"refreshToken"
|
||||
]
|
||||
},
|
||||
"AgentRefreshResponse": {
|
||||
"type": "object",
|
||||
"description": "Refreshed access token",
|
||||
"properties": {
|
||||
"accessToken": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accessToken"
|
||||
]
|
||||
},
|
||||
"CommandRequest": {
|
||||
"type": "object",
|
||||
"description": "Command to send to agent(s)",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Command type: config-update, deep-trace, or replay"
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"description": "Command payload JSON"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"CommandSingleResponse": {
|
||||
"type": "object",
|
||||
"description": "Result of sending a command to a single agent",
|
||||
"properties": {
|
||||
"commandId": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"commandId",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"AgentRegistrationRequest": {
|
||||
"type": "object",
|
||||
"description": "Agent registration payload",
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"group": {
|
||||
"type": "string",
|
||||
"default": "default"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"routeIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"AgentRegistrationResponse": {
|
||||
"type": "object",
|
||||
"description": "Agent registration result with JWT tokens and SSE endpoint",
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sseEndpoint": {
|
||||
"type": "string"
|
||||
},
|
||||
"heartbeatIntervalMs": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"serverPublicKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"accessToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"refreshToken": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accessToken",
|
||||
"agentId",
|
||||
"refreshToken",
|
||||
"serverPublicKey",
|
||||
"sseEndpoint"
|
||||
]
|
||||
},
|
||||
"CommandBroadcastResponse": {
|
||||
"type": "object",
|
||||
"description": "Result of broadcasting a command to multiple agents",
|
||||
"properties": {
|
||||
"commandIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"targetCount": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"commandIds"
|
||||
]
|
||||
},
|
||||
"OidcTestResult": {
|
||||
"type": "object",
|
||||
"description": "OIDC provider connectivity test result",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"authorizationEndpoint": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"authorizationEndpoint",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"ExecutionStats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1471,7 +1824,19 @@
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeCount",
|
||||
"avgDurationMs",
|
||||
"failedCount",
|
||||
"p99LatencyMs",
|
||||
"prevAvgDurationMs",
|
||||
"prevFailedCount",
|
||||
"prevP99LatencyMs",
|
||||
"prevTotalCount",
|
||||
"totalCount",
|
||||
"totalToday"
|
||||
]
|
||||
},
|
||||
"StatsTimeseries": {
|
||||
"type": "object",
|
||||
@@ -1482,7 +1847,10 @@
|
||||
"$ref": "#/components/schemas/TimeseriesBucket"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"buckets"
|
||||
]
|
||||
},
|
||||
"TimeseriesBucket": {
|
||||
"type": "object",
|
||||
@@ -1511,7 +1879,15 @@
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeCount",
|
||||
"avgDurationMs",
|
||||
"failedCount",
|
||||
"p99DurationMs",
|
||||
"time",
|
||||
"totalCount"
|
||||
]
|
||||
},
|
||||
"ExecutionDetail": {
|
||||
"type": "object",
|
||||
@@ -1561,7 +1937,22 @@
|
||||
"$ref": "#/components/schemas/ProcessorNode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"correlationId",
|
||||
"diagramContentHash",
|
||||
"durationMs",
|
||||
"endTime",
|
||||
"errorMessage",
|
||||
"errorStackTrace",
|
||||
"exchangeId",
|
||||
"executionId",
|
||||
"processors",
|
||||
"routeId",
|
||||
"startTime",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"ProcessorNode": {
|
||||
"type": "object",
|
||||
@@ -1595,9 +1986,171 @@
|
||||
},
|
||||
"errorStackTrace": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ProcessorNode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"children",
|
||||
"diagramNodeId",
|
||||
"durationMs",
|
||||
"endTime",
|
||||
"errorMessage",
|
||||
"errorStackTrace",
|
||||
"processorId",
|
||||
"processorType",
|
||||
"startTime",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"DiagramLayout": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"width": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"nodes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PositionedNode"
|
||||
}
|
||||
},
|
||||
"edges": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PositionedEdge"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PositionedEdge": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sourceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"targetId": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"points": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PositionedNode": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"x": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"OidcPublicConfigResponse": {
|
||||
"type": "object",
|
||||
"description": "OIDC configuration for SPA login flow",
|
||||
"properties": {
|
||||
"issuer": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"authorizationEndpoint": {
|
||||
"type": "string"
|
||||
},
|
||||
"endSessionEndpoint": {
|
||||
"type": "string",
|
||||
"description": "Present if the provider supports RP-initiated logout"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"authorizationEndpoint",
|
||||
"clientId",
|
||||
"issuer"
|
||||
]
|
||||
},
|
||||
"AgentInstanceResponse": {
|
||||
"type": "object",
|
||||
"description": "Agent instance summary",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"group": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"routeIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"registeredAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"lastHeartbeat": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"group",
|
||||
"id",
|
||||
"lastHeartbeat",
|
||||
"name",
|
||||
"registeredAt",
|
||||
"routeIds",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"SseEmitter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1632,7 +2185,22 @@
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"displayName",
|
||||
"email",
|
||||
"provider",
|
||||
"roles",
|
||||
"userId"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearer": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../client';
|
||||
import type {
|
||||
SearchRequest,
|
||||
ExecutionStats,
|
||||
ExecutionSummary,
|
||||
StatsTimeseries,
|
||||
ExecutionDetail,
|
||||
} from '../schema-types';
|
||||
import type { SearchRequest } from '../types';
|
||||
|
||||
export function useExecutionStats(timeFrom: string | undefined, timeTo: string | undefined) {
|
||||
return useQuery({
|
||||
@@ -21,7 +15,7 @@ export function useExecutionStats(timeFrom: string | undefined, timeTo: string |
|
||||
},
|
||||
});
|
||||
if (error) throw new Error('Failed to load stats');
|
||||
return data as unknown as ExecutionStats;
|
||||
return data!;
|
||||
},
|
||||
enabled: !!timeFrom,
|
||||
placeholderData: (prev) => prev,
|
||||
@@ -37,7 +31,7 @@ export function useSearchExecutions(filters: SearchRequest, live = false) {
|
||||
body: filters,
|
||||
});
|
||||
if (error) throw new Error('Search failed');
|
||||
return data as unknown as { data: ExecutionSummary[]; total: number; offset: number; limit: number };
|
||||
return data!;
|
||||
},
|
||||
placeholderData: (prev) => prev,
|
||||
refetchInterval: live ? 5_000 : false,
|
||||
@@ -58,7 +52,7 @@ export function useStatsTimeseries(timeFrom: string | undefined, timeTo: string
|
||||
},
|
||||
});
|
||||
if (error) throw new Error('Failed to load timeseries');
|
||||
return data as unknown as StatsTimeseries;
|
||||
return data!;
|
||||
},
|
||||
enabled: !!timeFrom,
|
||||
placeholderData: (prev) => prev,
|
||||
@@ -74,7 +68,7 @@ export function useExecutionDetail(executionId: string | null) {
|
||||
params: { path: { executionId: executionId! } },
|
||||
});
|
||||
if (error) throw new Error('Failed to load execution detail');
|
||||
return data as unknown as ExecutionDetail;
|
||||
return data!;
|
||||
},
|
||||
enabled: !!executionId,
|
||||
});
|
||||
@@ -96,7 +90,7 @@ export function useProcessorSnapshot(
|
||||
},
|
||||
);
|
||||
if (error) throw new Error('Failed to load snapshot');
|
||||
return data as unknown as Record<string, string>;
|
||||
return data!;
|
||||
},
|
||||
enabled: !!executionId && index !== null,
|
||||
});
|
||||
|
||||
@@ -1,80 +1,47 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { config } from '../../config';
|
||||
import { useAuthStore } from '../../auth/auth-store';
|
||||
|
||||
export interface OidcConfigResponse {
|
||||
configured: boolean;
|
||||
enabled?: boolean;
|
||||
issuerUri?: string;
|
||||
clientId?: string;
|
||||
clientSecretSet?: boolean;
|
||||
rolesClaim?: string;
|
||||
defaultRoles?: string[];
|
||||
autoSignup?: boolean;
|
||||
}
|
||||
|
||||
export interface OidcConfigRequest {
|
||||
enabled: boolean;
|
||||
issuerUri: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
rolesClaim: string;
|
||||
defaultRoles: string[];
|
||||
autoSignup: boolean;
|
||||
}
|
||||
|
||||
interface TestResult {
|
||||
status: string;
|
||||
authorizationEndpoint: string;
|
||||
}
|
||||
|
||||
async function adminFetch<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const token = useAuthStore.getState().accessToken;
|
||||
const res = await fetch(`${config.apiBaseUrl}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
if (res.status === 204) return undefined as T;
|
||||
const body = await res.json();
|
||||
if (!res.ok) throw new Error(body.message || `Request failed (${res.status})`);
|
||||
return body as T;
|
||||
}
|
||||
import { api } from '../client';
|
||||
import type { OidcAdminConfigRequest } from '../types';
|
||||
|
||||
export function useOidcConfig() {
|
||||
return useQuery<OidcConfigResponse>({
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'oidc'],
|
||||
queryFn: () => adminFetch('/admin/oidc'),
|
||||
queryFn: async () => {
|
||||
const { data, error } = await api.GET('/admin/oidc');
|
||||
if (error) throw new Error('Failed to load OIDC config');
|
||||
return data!;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveOidcConfig() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: OidcConfigRequest) =>
|
||||
adminFetch<OidcConfigResponse>('/admin/oidc', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
mutationFn: async (body: OidcAdminConfigRequest) => {
|
||||
const { data, error } = await api.PUT('/admin/oidc', { body });
|
||||
if (error) throw new Error('Failed to save OIDC config');
|
||||
return data!;
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'oidc'] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useTestOidcConnection() {
|
||||
return useMutation({
|
||||
mutationFn: () =>
|
||||
adminFetch<TestResult>('/admin/oidc/test', { method: 'POST' }),
|
||||
mutationFn: async () => {
|
||||
const { data, error } = await api.POST('/admin/oidc/test');
|
||||
if (error) throw new Error('OIDC test failed');
|
||||
return data!;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteOidcConfig() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () =>
|
||||
adminFetch<void>('/admin/oidc', { method: 'DELETE' }),
|
||||
mutationFn: async () => {
|
||||
const { error } = await api.DELETE('/admin/oidc');
|
||||
if (error) throw new Error('Failed to delete OIDC config');
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'oidc'] }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { components } from './schema';
|
||||
|
||||
type Require<T> = {
|
||||
[K in keyof T]-?: T[K] extends (infer U)[]
|
||||
? Require<U>[]
|
||||
: T[K] extends object | undefined
|
||||
? Require<NonNullable<T[K]>>
|
||||
: NonNullable<T[K]>;
|
||||
};
|
||||
|
||||
export type ExecutionSummary = Require<components['schemas']['ExecutionSummary']>;
|
||||
export type SearchRequest = components['schemas']['SearchRequest'];
|
||||
export type ExecutionDetail = Require<components['schemas']['ExecutionDetail']>;
|
||||
export type ExecutionStats = Require<components['schemas']['ExecutionStats']>;
|
||||
export type StatsTimeseries = Require<components['schemas']['StatsTimeseries']>;
|
||||
export type TimeseriesBucket = Require<components['schemas']['TimeseriesBucket']>;
|
||||
export type UserInfo = Require<components['schemas']['UserInfo']>;
|
||||
|
||||
export type ProcessorNode = Require<components['schemas']['ProcessorNode']> & {
|
||||
children?: ProcessorNode[];
|
||||
};
|
||||
|
||||
export interface AgentInstance {
|
||||
id: string;
|
||||
applicationName: string;
|
||||
group: string;
|
||||
status: string;
|
||||
routeIds: string[];
|
||||
registeredAt: string;
|
||||
lastHeartbeat: string;
|
||||
}
|
||||
404
ui/src/api/schema.d.ts
vendored
404
ui/src/api/schema.d.ts
vendored
@@ -127,6 +127,7 @@ export interface paths {
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Refresh access token */
|
||||
post: operations["refresh"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
@@ -143,6 +144,7 @@ export interface paths {
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Exchange OIDC authorization code for JWTs */
|
||||
post: operations["callback"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
@@ -159,6 +161,7 @@ export interface paths {
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Login with local credentials */
|
||||
post: operations["login"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
@@ -418,6 +421,7 @@ export interface paths {
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get OIDC config for SPA login flow */
|
||||
get: operations["getConfig_1"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
@@ -509,7 +513,8 @@ export interface components {
|
||||
RolesRequest: {
|
||||
roles?: string[];
|
||||
};
|
||||
OidcConfigRequest: {
|
||||
/** @description OIDC configuration update request */
|
||||
OidcAdminConfigRequest: {
|
||||
enabled?: boolean;
|
||||
issuerUri?: string;
|
||||
clientId?: string;
|
||||
@@ -518,6 +523,21 @@ export interface components {
|
||||
defaultRoles?: string[];
|
||||
autoSignup?: boolean;
|
||||
};
|
||||
/** @description Error response */
|
||||
ErrorResponse: {
|
||||
message: string;
|
||||
};
|
||||
/** @description OIDC configuration for admin management */
|
||||
OidcAdminConfigResponse: {
|
||||
configured?: boolean;
|
||||
enabled?: boolean;
|
||||
issuerUri?: string;
|
||||
clientId?: string;
|
||||
clientSecretSet?: boolean;
|
||||
rolesClaim?: string;
|
||||
defaultRoles?: string[];
|
||||
autoSignup?: boolean;
|
||||
};
|
||||
SearchRequest: {
|
||||
status?: string;
|
||||
/** Format: date-time */
|
||||
@@ -542,32 +562,37 @@ export interface components {
|
||||
limit?: number;
|
||||
};
|
||||
ExecutionSummary: {
|
||||
executionId?: string;
|
||||
routeId?: string;
|
||||
agentId?: string;
|
||||
status?: string;
|
||||
executionId: string;
|
||||
routeId: string;
|
||||
agentId: string;
|
||||
status: string;
|
||||
/** Format: date-time */
|
||||
startTime?: string;
|
||||
startTime: string;
|
||||
/** Format: date-time */
|
||||
endTime?: string;
|
||||
endTime: string;
|
||||
/** Format: int64 */
|
||||
durationMs?: number;
|
||||
correlationId?: string;
|
||||
errorMessage?: string;
|
||||
diagramContentHash?: string;
|
||||
durationMs: number;
|
||||
correlationId: string;
|
||||
errorMessage: string;
|
||||
diagramContentHash: string;
|
||||
};
|
||||
SearchResultExecutionSummary: {
|
||||
data?: components["schemas"]["ExecutionSummary"][];
|
||||
data: components["schemas"]["ExecutionSummary"][];
|
||||
/** Format: int64 */
|
||||
total?: number;
|
||||
total: number;
|
||||
/** Format: int32 */
|
||||
offset?: number;
|
||||
offset: number;
|
||||
/** Format: int32 */
|
||||
limit?: number;
|
||||
limit: number;
|
||||
};
|
||||
RefreshRequest: {
|
||||
refreshToken?: string;
|
||||
};
|
||||
/** @description JWT token pair */
|
||||
AuthTokenResponse: {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
CallbackRequest: {
|
||||
code?: string;
|
||||
redirectUri?: string;
|
||||
@@ -576,89 +601,190 @@ export interface components {
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
/** @description Agent token refresh request */
|
||||
AgentRefreshRequest: {
|
||||
refreshToken: string;
|
||||
};
|
||||
/** @description Refreshed access token */
|
||||
AgentRefreshResponse: {
|
||||
accessToken: string;
|
||||
};
|
||||
/** @description Command to send to agent(s) */
|
||||
CommandRequest: {
|
||||
/** @description Command type: config-update, deep-trace, or replay */
|
||||
type: string;
|
||||
/** @description Command payload JSON */
|
||||
payload?: Record<string, never>;
|
||||
};
|
||||
/** @description Result of sending a command to a single agent */
|
||||
CommandSingleResponse: {
|
||||
commandId: string;
|
||||
status: string;
|
||||
};
|
||||
/** @description Agent registration payload */
|
||||
AgentRegistrationRequest: {
|
||||
agentId: string;
|
||||
name: string;
|
||||
/** @default default */
|
||||
group: string;
|
||||
version?: string;
|
||||
routeIds?: string[];
|
||||
capabilities?: {
|
||||
[key: string]: Record<string, never>;
|
||||
};
|
||||
};
|
||||
/** @description Agent registration result with JWT tokens and SSE endpoint */
|
||||
AgentRegistrationResponse: {
|
||||
agentId: string;
|
||||
sseEndpoint: string;
|
||||
/** Format: int64 */
|
||||
heartbeatIntervalMs?: number;
|
||||
serverPublicKey: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
/** @description Result of broadcasting a command to multiple agents */
|
||||
CommandBroadcastResponse: {
|
||||
commandIds: string[];
|
||||
/** Format: int32 */
|
||||
targetCount?: number;
|
||||
};
|
||||
/** @description OIDC provider connectivity test result */
|
||||
OidcTestResult: {
|
||||
status: string;
|
||||
authorizationEndpoint: string;
|
||||
};
|
||||
ExecutionStats: {
|
||||
/** Format: int64 */
|
||||
totalCount?: number;
|
||||
totalCount: number;
|
||||
/** Format: int64 */
|
||||
failedCount?: number;
|
||||
failedCount: number;
|
||||
/** Format: int64 */
|
||||
avgDurationMs?: number;
|
||||
avgDurationMs: number;
|
||||
/** Format: int64 */
|
||||
p99LatencyMs?: number;
|
||||
p99LatencyMs: number;
|
||||
/** Format: int64 */
|
||||
activeCount?: number;
|
||||
activeCount: number;
|
||||
/** Format: int64 */
|
||||
totalToday?: number;
|
||||
totalToday: number;
|
||||
/** Format: int64 */
|
||||
prevTotalCount?: number;
|
||||
prevTotalCount: number;
|
||||
/** Format: int64 */
|
||||
prevFailedCount?: number;
|
||||
prevFailedCount: number;
|
||||
/** Format: int64 */
|
||||
prevAvgDurationMs?: number;
|
||||
prevAvgDurationMs: number;
|
||||
/** Format: int64 */
|
||||
prevP99LatencyMs?: number;
|
||||
prevP99LatencyMs: number;
|
||||
};
|
||||
StatsTimeseries: {
|
||||
buckets?: components["schemas"]["TimeseriesBucket"][];
|
||||
buckets: components["schemas"]["TimeseriesBucket"][];
|
||||
};
|
||||
TimeseriesBucket: {
|
||||
/** Format: date-time */
|
||||
time?: string;
|
||||
time: string;
|
||||
/** Format: int64 */
|
||||
totalCount?: number;
|
||||
totalCount: number;
|
||||
/** Format: int64 */
|
||||
failedCount?: number;
|
||||
failedCount: number;
|
||||
/** Format: int64 */
|
||||
avgDurationMs?: number;
|
||||
avgDurationMs: number;
|
||||
/** Format: int64 */
|
||||
p99DurationMs?: number;
|
||||
p99DurationMs: number;
|
||||
/** Format: int64 */
|
||||
activeCount?: number;
|
||||
activeCount: number;
|
||||
};
|
||||
ExecutionDetail: {
|
||||
executionId?: string;
|
||||
routeId?: string;
|
||||
agentId?: string;
|
||||
status?: string;
|
||||
executionId: string;
|
||||
routeId: string;
|
||||
agentId: string;
|
||||
status: string;
|
||||
/** Format: date-time */
|
||||
startTime?: string;
|
||||
startTime: string;
|
||||
/** Format: date-time */
|
||||
endTime?: string;
|
||||
endTime: string;
|
||||
/** Format: int64 */
|
||||
durationMs?: number;
|
||||
correlationId?: string;
|
||||
exchangeId?: string;
|
||||
errorMessage?: string;
|
||||
errorStackTrace?: string;
|
||||
diagramContentHash?: string;
|
||||
processors?: components["schemas"]["ProcessorNode"][];
|
||||
durationMs: number;
|
||||
correlationId: string;
|
||||
exchangeId: string;
|
||||
errorMessage: string;
|
||||
errorStackTrace: string;
|
||||
diagramContentHash: string;
|
||||
processors: components["schemas"]["ProcessorNode"][];
|
||||
};
|
||||
ProcessorNode: {
|
||||
processorId?: string;
|
||||
processorType?: string;
|
||||
status?: string;
|
||||
processorId: string;
|
||||
processorType: string;
|
||||
status: string;
|
||||
/** Format: date-time */
|
||||
startTime?: string;
|
||||
startTime: string;
|
||||
/** Format: date-time */
|
||||
endTime?: string;
|
||||
endTime: string;
|
||||
/** Format: int64 */
|
||||
durationMs?: number;
|
||||
diagramNodeId?: string;
|
||||
errorMessage?: string;
|
||||
errorStackTrace?: string;
|
||||
durationMs: number;
|
||||
diagramNodeId: string;
|
||||
errorMessage: string;
|
||||
errorStackTrace: string;
|
||||
children: components["schemas"]["ProcessorNode"][];
|
||||
};
|
||||
DiagramLayout: {
|
||||
/** Format: double */
|
||||
width?: number;
|
||||
/** Format: double */
|
||||
height?: number;
|
||||
nodes?: components["schemas"]["PositionedNode"][];
|
||||
edges?: components["schemas"]["PositionedEdge"][];
|
||||
};
|
||||
PositionedEdge: {
|
||||
sourceId?: string;
|
||||
targetId?: string;
|
||||
label?: string;
|
||||
points?: number[][];
|
||||
};
|
||||
PositionedNode: {
|
||||
id?: string;
|
||||
label?: string;
|
||||
type?: string;
|
||||
/** Format: double */
|
||||
x?: number;
|
||||
/** Format: double */
|
||||
y?: number;
|
||||
/** Format: double */
|
||||
width?: number;
|
||||
/** Format: double */
|
||||
height?: number;
|
||||
};
|
||||
/** @description OIDC configuration for SPA login flow */
|
||||
OidcPublicConfigResponse: {
|
||||
issuer: string;
|
||||
clientId: string;
|
||||
authorizationEndpoint: string;
|
||||
/** @description Present if the provider supports RP-initiated logout */
|
||||
endSessionEndpoint?: string;
|
||||
};
|
||||
/** @description Agent instance summary */
|
||||
AgentInstanceResponse: {
|
||||
id: string;
|
||||
name: string;
|
||||
group: string;
|
||||
status: string;
|
||||
routeIds: string[];
|
||||
/** Format: date-time */
|
||||
registeredAt: string;
|
||||
/** Format: date-time */
|
||||
lastHeartbeat: string;
|
||||
};
|
||||
SseEmitter: {
|
||||
/** Format: int64 */
|
||||
timeout?: number;
|
||||
};
|
||||
UserInfo: {
|
||||
userId?: string;
|
||||
provider?: string;
|
||||
email?: string;
|
||||
displayName?: string;
|
||||
roles?: string[];
|
||||
userId: string;
|
||||
provider: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
roles: string[];
|
||||
/** Format: date-time */
|
||||
createdAt?: string;
|
||||
createdAt: string;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
@@ -715,7 +841,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": Record<string, never>;
|
||||
"*/*": components["schemas"]["OidcAdminConfigResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -729,7 +855,7 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["OidcConfigRequest"];
|
||||
"application/json": components["schemas"]["OidcAdminConfigRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@@ -739,7 +865,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": Record<string, never>;
|
||||
"*/*": components["schemas"]["OidcAdminConfigResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid configuration */
|
||||
@@ -748,7 +874,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": Record<string, never>;
|
||||
"*/*": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -926,13 +1052,22 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
/** @description Token refreshed */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": Record<string, never>;
|
||||
"*/*": components["schemas"]["AuthTokenResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid refresh token */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -950,13 +1085,40 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
/** @description Authentication successful */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": Record<string, never>;
|
||||
"*/*": components["schemas"]["AuthTokenResponse"];
|
||||
};
|
||||
};
|
||||
/** @description OIDC authentication failed */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Account not provisioned */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description OIDC not configured or disabled */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["AuthTokenResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -974,13 +1136,22 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
/** @description Login successful */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": Record<string, never>;
|
||||
"*/*": components["schemas"]["AuthTokenResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid credentials */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -996,7 +1167,7 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": string;
|
||||
"application/json": components["schemas"]["AgentRefreshRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@@ -1006,7 +1177,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["AgentRefreshResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid or expired refresh token */
|
||||
@@ -1015,7 +1186,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["AgentRefreshResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Agent not found */
|
||||
@@ -1024,7 +1195,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["AgentRefreshResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1067,7 +1238,7 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": string;
|
||||
"application/json": components["schemas"]["CommandRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@@ -1077,7 +1248,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["CommandSingleResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid command payload */
|
||||
@@ -1086,7 +1257,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["CommandSingleResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Agent not registered */
|
||||
@@ -1095,7 +1266,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["CommandSingleResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1137,7 +1308,7 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": string;
|
||||
"application/json": components["schemas"]["AgentRegistrationRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@@ -1147,7 +1318,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["AgentRegistrationResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid registration payload */
|
||||
@@ -1156,7 +1327,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Missing or invalid bootstrap token */
|
||||
@@ -1165,7 +1336,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["AgentRegistrationResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1181,7 +1352,7 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": string;
|
||||
"application/json": components["schemas"]["CommandRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@@ -1191,7 +1362,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["CommandBroadcastResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid command payload */
|
||||
@@ -1200,7 +1371,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["CommandBroadcastResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1214,7 +1385,7 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": string;
|
||||
"application/json": components["schemas"]["CommandRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@@ -1224,7 +1395,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["CommandBroadcastResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid command payload */
|
||||
@@ -1233,7 +1404,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["CommandBroadcastResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1253,7 +1424,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": Record<string, never>;
|
||||
"*/*": components["schemas"]["OidcTestResult"];
|
||||
};
|
||||
};
|
||||
/** @description Provider unreachable or misconfigured */
|
||||
@@ -1262,7 +1433,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": Record<string, never>;
|
||||
"*/*": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1325,7 +1496,7 @@ export interface operations {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
/** @description Execution detail found */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -1334,6 +1505,15 @@ export interface operations {
|
||||
"*/*": components["schemas"]["ExecutionDetail"];
|
||||
};
|
||||
};
|
||||
/** @description Execution not found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ExecutionDetail"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
getProcessorSnapshot: {
|
||||
@@ -1348,7 +1528,7 @@ export interface operations {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
/** @description Snapshot data */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -1359,6 +1539,17 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Snapshot not found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
renderDiagram: {
|
||||
@@ -1378,7 +1569,8 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": Record<string, never>;
|
||||
"image/svg+xml": string;
|
||||
"application/json": components["schemas"]["DiagramLayout"];
|
||||
};
|
||||
};
|
||||
/** @description Diagram not found */
|
||||
@@ -1401,13 +1593,31 @@ export interface operations {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
/** @description OIDC configuration */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": Record<string, never>;
|
||||
"*/*": components["schemas"]["OidcPublicConfigResponse"];
|
||||
};
|
||||
};
|
||||
/** @description OIDC not configured or disabled */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["OidcPublicConfigResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Failed to retrieve OIDC provider metadata */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1429,7 +1639,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["AgentInstanceResponse"][];
|
||||
};
|
||||
};
|
||||
/** @description Invalid status filter */
|
||||
@@ -1438,7 +1648,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": string;
|
||||
"*/*": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
17
ui/src/api/types.ts
Normal file
17
ui/src/api/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { components } from './schema';
|
||||
|
||||
export type ExecutionSummary = components['schemas']['ExecutionSummary'];
|
||||
export type SearchRequest = components['schemas']['SearchRequest'];
|
||||
export type ExecutionDetail = components['schemas']['ExecutionDetail'];
|
||||
export type ExecutionStats = components['schemas']['ExecutionStats'];
|
||||
export type StatsTimeseries = components['schemas']['StatsTimeseries'];
|
||||
export type TimeseriesBucket = components['schemas']['TimeseriesBucket'];
|
||||
export type UserInfo = components['schemas']['UserInfo'];
|
||||
export type ProcessorNode = components['schemas']['ProcessorNode'];
|
||||
export type AgentInstance = components['schemas']['AgentInstanceResponse'];
|
||||
export type OidcAdminConfigResponse = components['schemas']['OidcAdminConfigResponse'];
|
||||
export type OidcAdminConfigRequest = components['schemas']['OidcAdminConfigRequest'];
|
||||
export type OidcTestResult = components['schemas']['OidcTestResult'];
|
||||
export type OidcPublicConfigResponse = components['schemas']['OidcPublicConfigResponse'];
|
||||
export type AuthTokenResponse = components['schemas']['AuthTokenResponse'];
|
||||
export type ErrorResponse = components['schemas']['ErrorResponse'];
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FormEvent, useEffect, useState } from 'react';
|
||||
import { Navigate } from 'react-router';
|
||||
import { useAuthStore } from './auth-store';
|
||||
import { config } from '../config';
|
||||
import { api } from '../api/client';
|
||||
import styles from './LoginPage.module.css';
|
||||
|
||||
interface OidcInfo {
|
||||
@@ -17,9 +17,8 @@ export function LoginPage() {
|
||||
const [oidcLoading, setOidcLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${config.apiBaseUrl}/auth/oidc/config`)
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
.then((data) => {
|
||||
api.GET('/auth/oidc/config')
|
||||
.then(({ data }) => {
|
||||
if (data?.authorizationEndpoint && data?.clientId) {
|
||||
setOidc({ clientId: data.clientId, authorizationEndpoint: data.authorizationEndpoint });
|
||||
if (data.endSessionEndpoint) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import { config } from '../config';
|
||||
import { api } from '../api/client';
|
||||
|
||||
interface AuthState {
|
||||
accessToken: string | null;
|
||||
@@ -58,16 +58,13 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
login: async (username, password) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const res = await fetch(`${config.apiBaseUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
const { data, error } = await api.POST('/auth/login', {
|
||||
body: { username, password },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.message || 'Invalid credentials');
|
||||
if (error || !data) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
const { accessToken, refreshToken } = await res.json();
|
||||
const { accessToken, refreshToken } = data;
|
||||
localStorage.removeItem('cameleer-oidc-end-session');
|
||||
persistTokens(accessToken, refreshToken, username);
|
||||
set({
|
||||
@@ -89,16 +86,13 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
loginWithOidcCode: async (code, redirectUri) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const res = await fetch(`${config.apiBaseUrl}/auth/oidc/callback`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code, redirectUri }),
|
||||
const { data, error } = await api.POST('/auth/oidc/callback', {
|
||||
body: { code, redirectUri },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.message || 'OIDC login failed');
|
||||
if (error || !data) {
|
||||
throw new Error('OIDC login failed');
|
||||
}
|
||||
const { accessToken, refreshToken } = await res.json();
|
||||
const { accessToken, refreshToken } = data;
|
||||
const payload = JSON.parse(atob(accessToken.split('.')[1]));
|
||||
const username = payload.sub ?? 'oidc-user';
|
||||
persistTokens(accessToken, refreshToken, username);
|
||||
@@ -122,13 +116,10 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
const { refreshToken } = get();
|
||||
if (!refreshToken) return false;
|
||||
try {
|
||||
const res = await fetch(`${config.apiBaseUrl}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
const { data, error } = await api.POST('/auth/refresh', {
|
||||
body: { refreshToken },
|
||||
});
|
||||
if (!res.ok) return false;
|
||||
const data = await res.json();
|
||||
if (error || !data) return false;
|
||||
const username = get().username ?? '';
|
||||
persistTokens(data.accessToken, data.refreshToken, username);
|
||||
set({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ExecutionSummary, AgentInstance } from '../../api/schema-types';
|
||||
import type { ExecutionSummary, AgentInstance } from '../../api/types';
|
||||
import type { PaletteResult, RouteInfo } from './use-palette-search';
|
||||
import { highlightMatch, formatRelativeTime } from './utils';
|
||||
import { AppBadge } from '../shared/AppBadge';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../../api/client';
|
||||
import type { ExecutionSummary, AgentInstance } from '../../api/schema-types';
|
||||
import type { ExecutionSummary, AgentInstance } from '../../api/types';
|
||||
import { useCommandPalette, type PaletteScope } from './use-command-palette';
|
||||
import { useDebouncedValue } from './utils';
|
||||
|
||||
@@ -51,7 +51,7 @@ export function usePaletteSearch() {
|
||||
},
|
||||
});
|
||||
if (error) throw new Error('Search failed');
|
||||
return data as unknown as { data: ExecutionSummary[]; total: number };
|
||||
return data!;
|
||||
},
|
||||
enabled: isOpen && isExecutionScope(scope),
|
||||
placeholderData: (prev) => prev,
|
||||
@@ -64,7 +64,7 @@ export function usePaletteSearch() {
|
||||
params: { query: {} },
|
||||
});
|
||||
if (error) throw new Error('Failed to load agents');
|
||||
return data as unknown as AgentInstance[];
|
||||
return data!;
|
||||
},
|
||||
enabled: isOpen && (isApplicationScope(scope) || isRouteScope(scope)),
|
||||
staleTime: 30_000,
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
useSaveOidcConfig,
|
||||
useTestOidcConnection,
|
||||
useDeleteOidcConfig,
|
||||
type OidcConfigRequest,
|
||||
} from '../../api/queries/oidc-admin';
|
||||
import type { OidcAdminConfigRequest } from '../../api/types';
|
||||
import styles from './OidcAdminPage.module.css';
|
||||
|
||||
interface FormData {
|
||||
@@ -87,7 +87,7 @@ function OidcAdminForm() {
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const payload: OidcConfigRequest = {
|
||||
const payload: OidcAdminConfigRequest = {
|
||||
...form,
|
||||
clientSecret: secretTouched ? form.clientSecret : '********',
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useProcessorSnapshot } from '../../api/queries/executions';
|
||||
import type { ExecutionSummary } from '../../api/schema-types';
|
||||
import type { ExecutionSummary } from '../../api/types';
|
||||
import styles from './ExchangeDetail.module.css';
|
||||
|
||||
interface ExchangeDetailProps {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useExecutionDetail } from '../../api/queries/executions';
|
||||
import type { ProcessorNode as ProcessorNodeType } from '../../api/schema-types';
|
||||
import type { ProcessorNode as ProcessorNodeType } from '../../api/types';
|
||||
import styles from './ProcessorTree.module.css';
|
||||
|
||||
const ICON_MAP: Record<string, { label: string; className: string }> = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import type { ExecutionSummary } from '../../api/schema-types';
|
||||
import type { ExecutionSummary } from '../../api/types';
|
||||
import { StatusPill } from '../../components/shared/StatusPill';
|
||||
import { DurationBar } from '../../components/shared/DurationBar';
|
||||
import { AppBadge } from '../../components/shared/AppBadge';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ScopeTabs } from '../../components/command-palette/ScopeTabs';
|
||||
import { ResultsList } from '../../components/command-palette/ResultsList';
|
||||
import { PaletteFooter } from '../../components/command-palette/PaletteFooter';
|
||||
import { FilterChip } from '../../components/shared/FilterChip';
|
||||
import type { ExecutionSummary, AgentInstance } from '../../api/schema-types';
|
||||
import type { ExecutionSummary, AgentInstance } from '../../api/types';
|
||||
import styles from './SearchFilters.module.css';
|
||||
|
||||
export function SearchFilters() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import type { SearchRequest } from '../../api/schema-types';
|
||||
import type { SearchRequest } from '../../api/types';
|
||||
|
||||
function todayMidnight(): string {
|
||||
const d = new Date();
|
||||
|
||||
Reference in New Issue
Block a user