Skip to main content
Glama
routes.rs25.1 kB
use std::time::Duration; use axum::{ Router, extract::State, http::{ Request, StatusCode, }, middleware::{ self, Next, }, response::{ IntoResponse, Json, Response, }, routing::get, }; use hyper::{ Method, header, }; use serde_json::{ Value, json, }; use si_data_nats::NatsError; use si_data_pg::PgError; use telemetry::prelude::*; use thiserror::Error; use tower_http::{ compression::CompressionLayer, cors::{ AllowOrigin, CorsLayer, }, }; use utoipa::{ OpenApi, ToSchema, }; use crate::{ ServerError, app_state::{ AppState, ApplicationRuntimeMode, }, service::{ v1, workspace_management, }, }; const MAINTENANCE_MODE_MESSAGE: &str = concat!( " SI is currently in maintenance mode. ", "Please refresh & try again later. ", "Reach out to support@systeminit.com ", "or in the SI Discord for more information if this problem persists", ); async fn app_state_middeware<B>( State(state): State<AppState>, request: Request<B>, next: Next<B>, ) -> Response { match *state.application_runtime_mode.read().await { ApplicationRuntimeMode::Maintenance => { (StatusCode::SERVICE_UNAVAILABLE, MAINTENANCE_MODE_MESSAGE).into_response() } ApplicationRuntimeMode::Running => next.run(request).await, } } #[derive(OpenApi)] #[openapi( paths( system_status_route, crate::service::whoami::whoami, ), components( schemas( SystemStatusResponse, ErrorResponse, ErrorDetail, crate::service::whoami::WhoamiResponse, crate::service::v1::common::ApiSuccess<String>, crate::service::v1::common::ApiError, ) ), tags( (name = "root", description = "Root API endpoints"), (name = "whoami", description = "User identity endpoints"), (name = "change_sets", description = "Change Set management endpoints"), (name = "components", description = "Components management endpoints"), (name = "schemas", description = "Schemas management endpoints"), (name = "actions", description = "Actions management endpoints"), (name = "secrets", description = "Secrets management endpoints"), (name = "funcs", description = "Functions management endpoints"), (name = "management_funcs", description = "Management functions endpoints"), (name = "debug_funcs", description = "Debug function endpoints"), (name = "workspace_management", description = "Workspace management endpoints"), ), info( title = "System Initiative API", description = "The API Server for interacting with a System Initiative workspace", version = "1.0.0" ), servers( (url = "/", description = "This API Server") ) )] struct ApiDoc; #[derive(serde::Serialize, ToSchema)] struct SystemStatusResponse { #[schema(example = "Available at /swagger-ui")] #[serde(rename = "API Documentation")] api_documentation: String, } #[derive(serde::Serialize, ToSchema)] struct ErrorResponse { error: ErrorDetail, } #[derive(serde::Serialize, ToSchema)] struct ErrorDetail { message: String, code: i32, status_code: u16, } pub async fn openapi_handler() -> Result<Json<utoipa::openapi::OpenApi>, (StatusCode, String)> { let mut openapi = ApiDoc::openapi(); let v1_openapi = v1::get_openapi(); let workspace_management_openapi = workspace_management::get_openapi(); for (path, path_item) in v1_openapi.paths.paths { openapi.paths.paths.insert(path, path_item); } if let Some(openapi_components) = openapi.components.as_mut() { if let Some(v1_components) = v1_openapi.components { for (name, schema) in v1_components.schemas { openapi_components.schemas.insert(name, schema); } } } for (path, path_item) in workspace_management_openapi.paths.paths { openapi.paths.paths.insert(path, path_item); } if let Some(openapi_components) = openapi.components.as_mut() { if let Some(components) = workspace_management_openapi.components { for (name, schema) in components.schemas { openapi_components.schemas.insert(name, schema); } } } Ok(Json(openapi)) } #[allow(clippy::too_many_arguments)] pub fn routes(state: AppState) -> Router { async fn openapi_handler_with_globals() -> Result<Json<serde_json::Value>, (StatusCode, String)> { let original_response = openapi_handler().await?; let mut spec = serde_json::to_value(original_response.0).map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, format!("JSON conversion error: {e}"), ) })?; if let Some(components) = spec.get_mut("components") { if let Some(comp_obj) = components.as_object_mut() { if !comp_obj.contains_key("parameters") { comp_obj.insert("parameters".to_string(), json!({})); } if let Some(parameters) = comp_obj.get_mut("parameters") { if let Some(params_obj) = parameters.as_object_mut() { params_obj.insert( "WorkspaceId".to_string(), json!({ "name": "workspace_id", "in": "query", "description": "Workspace ID - can be set globally in the UI", "required": false, "schema": { "type": "string", "format": "uuid" } }), ); params_obj.insert( "ChangeSetId".to_string(), json!({ "name": "change_set_id", "in": "query", "description": "Change Set ID - can be set globally in the UI", "required": false, "schema": { "type": "string", "format": "uuid" } }), ); } } } } Ok(Json(spec)) } Router::new() .nest("/whoami", crate::service::whoami::routes()) .nest( "/management", crate::service::workspace_management::routes(state.clone()), ) .nest("/v1", crate::service::v1::routes(state.clone())) .route("/openapi.json", get(openapi_handler_with_globals)) // Add a route for Swagger UI HTML .route("/swagger-ui", get(serve_swagger_ui)) .layer(CompressionLayer::new()) .layer( // CORS configuration for public API CorsLayer::new() // Allow any origin since this is a public API .allow_origin(AllowOrigin::any()) // Allow common headers needed for API operations .allow_headers([ header::ACCEPT, header::ACCEPT_LANGUAGE, header::AUTHORIZATION, header::CONTENT_TYPE, header::ORIGIN, header::CACHE_CONTROL, header::CONTENT_LANGUAGE, header::PRAGMA, ]) // Expose headers that clients might need to access .expose_headers([header::CONTENT_LENGTH, header::CONTENT_TYPE]) // Allow standard methods .allow_methods([ Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS, Method::PATCH, ]) // Cache preflight requests for 1 hour (3600 seconds) .max_age(Duration::from_secs(3600)), ) .layer(middleware::from_fn_with_state( state.clone(), app_state_middeware, )) .nest( "/", Router::new().route("/", get(system_status_route).layer(CorsLayer::permissive())), ) .with_state(state) } #[utoipa::path( get, path = "/", tag = "root", responses( (status = 200, description = "System status information", body = SystemStatusResponse), (status = 401, description = "Unauthorized - Invalid or missing token"), (status = 503, description = "Service in maintenance mode") ) )] async fn system_status_route() -> Json<Value> { Json(json!({ "API Documentation": "Available at /swagger-ui" })) } async fn serve_swagger_ui() -> impl IntoResponse { const SWAGGER_UI_HTML: &str = r#"<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Luminork API Documentation</title> <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.1.0/swagger-ui.css"> <link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@5.1.0/favicon-32x32.png" sizes="32x32"> <link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@5.1.0/favicon-16x16.png" sizes="16x16"> <style> html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; } *, *:before, *:after { box-sizing: inherit; } body { margin: 0; background: #fafafa; } .topbar { display: none; } </style> </head> <body> <div id="swagger-ui"></div> <script src="https://unpkg.com/swagger-ui-dist@5.1.0/swagger-ui-bundle.js" charset="UTF-8"></script> <script src="https://unpkg.com/swagger-ui-dist@5.1.0/swagger-ui-standalone-preset.js" charset="UTF-8"></script> <script> window.onload = function() { // Create a plugin to intercept executions and add global parameters const GlobalParametersPlugin = function() { return { statePlugins: { spec: { wrapActions: { // Intercept the execute request action executeRequest: (oriAction) => (req) => { try { // Get the global parameters from localStorage const workspaceId = localStorage.getItem('swagger_ui_workspace_id'); const changeSetId = localStorage.getItem('swagger_ui_change_set_id'); // Clone the request object let requestWithParams = {...req}; // Check if the request has parameters if (!requestWithParams.parameters) { requestWithParams.parameters = {}; } // Add workspace_id if it's not already set and we have a global value if (workspaceId && !requestWithParams.parameters.workspace_id) { requestWithParams.parameters.workspace_id = workspaceId; } // Add change_set_id if it's not already set and we have a global value if (changeSetId && !requestWithParams.parameters.change_set_id) { requestWithParams.parameters.change_set_id = changeSetId; } return oriAction(requestWithParams); } catch (error) { console.error("Error in executeRequest interceptor:", error); return oriAction(req); } } } } } }; }; const ui = SwaggerUIBundle({ url: "/openapi.json", dom_id: '#swagger-ui', deepLinking: true, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl, GlobalParametersPlugin ], layout: "StandaloneLayout", defaultModelsExpandDepth: 3, defaultModelExpandDepth: 3, docExpansion: "list", tryItOutEnabled: true, displayRequestDuration: true, filter: true, persistAuthorization: true, // Remember auth token between page refreshes // Define security scheme for JWT authentication (since OpenAPI doesn't support it) requestInterceptor: function(request) { const authToken = localStorage.getItem('swagger_ui_token'); if (authToken) { request.headers.Authorization = "Bearer " + authToken; } return request; }, syntaxHighlight: { activate: true, theme: "agate" }, // Add a custom authorization UI onComplete: function() { // Get saved values from localStorage const savedToken = localStorage.getItem('swagger_ui_token') || ''; const savedWorkspaceId = localStorage.getItem('swagger_ui_workspace_id') || ''; const savedChangeSetId = localStorage.getItem('swagger_ui_change_set_id') || ''; // Create UI elements const authContainer = document.createElement('div'); authContainer.id = 'swagger-ui-auth'; authContainer.style.cssText = 'margin:20px 0; padding:20px; background:#fafafa; border:1px solid #ddd;'; // Add a title for the global parameters section const globalTitle = document.createElement('h3'); globalTitle.textContent = 'Global Parameters'; globalTitle.style.margin = '0 0 15px 0'; globalTitle.style.borderBottom = '1px solid #ddd'; globalTitle.style.paddingBottom = '10px'; const globalDescription = document.createElement('p'); globalDescription.textContent = 'Set global parameters that will be automatically applied to all API requests.'; globalDescription.style.margin = '0 0 20px 0'; globalDescription.style.color = '#666'; // Function to create form groups for consistency const createFormGroup = (title, inputId, placeholder, savedValue, description) => { const group = document.createElement('div'); group.style.marginBottom = '15px'; const label = document.createElement('label'); label.textContent = title; label.style.display = 'block'; label.style.fontWeight = 'bold'; label.style.marginBottom = '5px'; const input = document.createElement('input'); input.type = 'text'; input.id = inputId; input.placeholder = placeholder; input.value = savedValue; input.style.cssText = 'width:100%; padding:8px; border:1px solid #ddd;'; group.appendChild(label); if (description) { const desc = document.createElement('p'); desc.textContent = description; desc.style.margin = '0 0 5px 0'; desc.style.fontSize = '12px'; desc.style.color = '#666'; group.appendChild(desc); } group.appendChild(input); return { group, input }; }; // Create form elements const { group: tokenGroup, input: tokenInput } = createFormGroup( 'JWT Authorization Token', 'jwt-token-input', 'Enter JWT token', savedToken, 'Required: Authorization token will be added as a Bearer token in the Authorization header' ); const { group: workspaceGroup, input: workspaceInput } = createFormGroup( 'Workspace ID', 'workspace-id-input', 'Enter workspace ID (optional)', savedWorkspaceId, 'Optional: Will be added to requests that require a workspace_id parameter' ); const { group: changeSetGroup, input: changeSetInput } = createFormGroup( 'Change Set ID', 'change-set-id-input', 'Enter change set ID (optional)', savedChangeSetId, 'Optional: Will be added to requests that require a change_set_id parameter' ); const buttonContainer = document.createElement('div'); buttonContainer.style.marginTop = '10px'; const authButton = document.createElement('button'); authButton.textContent = 'Save Settings'; authButton.style.cssText = 'background:#4990e2; color:white; border:none; padding:8px 16px; margin-right:10px; cursor:pointer;'; const clearButton = document.createElement('button'); clearButton.textContent = 'Clear All'; clearButton.style.cssText = 'background:#e57373; color:white; border:none; padding:8px 16px; cursor:pointer;'; // Event handlers // Create a notification div for status messages const notificationDiv = document.createElement('div'); notificationDiv.id = 'swagger-notification'; notificationDiv.style.cssText = 'margin-top:10px; padding:8px; border-radius:4px; display:none;'; // Function to show notifications without using alert() const showNotification = (message, isSuccess = true) => { notificationDiv.textContent = message; notificationDiv.style.display = 'block'; notificationDiv.style.backgroundColor = isSuccess ? '#dff0d8' : '#f2dede'; notificationDiv.style.color = isSuccess ? '#3c763d' : '#a94442'; // Hide after 3 seconds setTimeout(() => { notificationDiv.style.display = 'none'; }, 3000); }; authButton.onclick = function() { // Clear any existing notifications notificationDiv.style.display = 'none'; const token = tokenInput.value.trim(); const workspaceId = workspaceInput.value.trim(); const changeSetId = changeSetInput.value.trim(); // Validate auth token is required if (!token) { showNotification('⚠️ Authorization token is required.', false); return; } // Save all values to localStorage localStorage.setItem('swagger_ui_token', token); localStorage.setItem('swagger_ui_workspace_id', workspaceId); localStorage.setItem('swagger_ui_change_set_id', changeSetId); showNotification('Settings saved. Your values will be applied to all requests.'); // Apply the authorization token setTimeout(() => { window.ui.preauthorizeApiKey('Bearer', token); }, 100); }; clearButton.onclick = function() { // Clear any existing notifications notificationDiv.style.display = 'none'; // Clear input fields tokenInput.value = ''; workspaceInput.value = ''; changeSetInput.value = ''; // Clear localStorage localStorage.removeItem('swagger_ui_token'); localStorage.removeItem('swagger_ui_workspace_id'); localStorage.removeItem('swagger_ui_change_set_id'); showNotification('All settings cleared.'); // Remove authorization setTimeout(() => { window.ui.preauthorizeApiKey('Bearer', ''); }, 100); }; // Add a reload the page button to force refresh the parameters const reloadButton = document.createElement('button'); reloadButton.textContent = 'Apply & Reload'; reloadButton.style.cssText = 'background:#4CAF50; color:white; border:none; padding:8px 16px; margin-right:10px; cursor:pointer;'; reloadButton.onclick = function() { // Clear any existing notifications notificationDiv.style.display = 'none'; // Save settings const token = tokenInput.value.trim(); const workspaceId = workspaceInput.value.trim(); const changeSetId = changeSetInput.value.trim(); // Validate auth token is required if (!token) { showNotification('⚠️ Authorization token is required.', false); return; } localStorage.setItem('swagger_ui_token', token); localStorage.setItem('swagger_ui_workspace_id', workspaceId); localStorage.setItem('swagger_ui_change_set_id', changeSetId); // Show notification and reload showNotification('Settings saved. Reloading page to apply changes...'); setTimeout(() => { window.location.reload(); }, 1000); }; // Assemble the DOM buttonContainer.appendChild(authButton); buttonContainer.appendChild(clearButton); buttonContainer.appendChild(reloadButton); authContainer.appendChild(globalTitle); authContainer.appendChild(globalDescription); authContainer.appendChild(tokenGroup); authContainer.appendChild(workspaceGroup); authContainer.appendChild(changeSetGroup); authContainer.appendChild(buttonContainer); authContainer.appendChild(notificationDiv); // Add a status indicator that shows when global params are active const statusDiv = document.createElement('div'); statusDiv.style.cssText = 'margin-top:15px; padding:8px; border-left:4px solid #4990e2; background:#f8f8f8;'; statusDiv.innerHTML = '<strong>Status:</strong> <span id="global-params-status">Loading...</span>'; authContainer.appendChild(statusDiv); // Simple status update function const updateStatus = () => { const hasWorkspace = localStorage.getItem('swagger_ui_workspace_id'); const hasChangeSet = localStorage.getItem('swagger_ui_change_set_id'); const hasToken = localStorage.getItem('swagger_ui_token'); const statusSpan = document.getElementById('global-params-status'); if (statusSpan) { const items = []; if (hasToken) items.push('Auth Token'); if (hasWorkspace) items.push('Workspace ID'); if (hasChangeSet) items.push('Change Set ID'); statusSpan.textContent = items.length > 0 ? `Using global ${items.join(', ')}` : 'No global parameters set'; statusSpan.style.color = items.length > 0 ? '#3c763d' : '#777'; } }; // Update status on load and when values change updateStatus(); authButton.addEventListener('click', updateStatus); clearButton.addEventListener('click', updateStatus); // Insert at the top of the Swagger UI const swaggerUIContainer = document.getElementById('swagger-ui'); swaggerUIContainer.insertBefore(authContainer, swaggerUIContainer.firstChild); } }); window.ui = ui; }; </script> </body> </html>"#; ( StatusCode::OK, [("Content-Type", "text/html")], SWAGGER_UI_HTML, ) } #[allow(clippy::large_enum_variant)] #[remain::sorted] #[derive(Debug, Error)] pub enum AppError { #[error("nats error: {0}")] Nats(#[from] NatsError), #[error("pg error: {0}")] Pg(#[from] PgError), #[error("server error: {0}")] Server(#[from] ServerError), } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, error_message) = (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()); let body = Json(serde_json::json!({ "error": { "message": error_message, "code": 42, "statusCode": status.as_u16(), }, })); error!(si.error.message = error_message); (status, body).into_response() } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server