diff --git a/src/api/editor.rs b/src/api/editor.rs deleted file mode 100644 index f4085ce6a..000000000 --- a/src/api/editor.rs +++ /dev/null @@ -1,209 +0,0 @@ -use axum::{ - extract::{Path, State}, - http::StatusCode, - response::Json, -}; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -use crate::core::shared::state::AppState; - -#[derive(Debug, Serialize, Deserialize)] -pub struct FileContent { - pub content: String, - pub language: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FileResponse { - pub success: bool, - pub content: Option, - pub language: Option, - pub error: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SaveResponse { - pub success: bool, - pub message: Option, - pub error: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FileInfo { - pub name: String, - pub path: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FileListResponse { - pub success: bool, - pub files: Vec, - pub error: Option, -} - -pub async fn get_file( - Path(file_path): Path, - State(_state): State>, -) -> Result, (StatusCode, Json)> { - let decoded_path = urlencoding::decode(&file_path) - .map(|s| s.to_string()) - .unwrap_or(file_path); - - let language = detect_language(&decoded_path); - - match std::fs::read_to_string(&decoded_path) { - Ok(content) => Ok(Json(FileResponse { - success: true, - content: Some(content), - language: Some(language), - error: None, - })), - Err(e) => { - log::warn!("Failed to read file {}: {}", decoded_path, e); - Ok(Json(FileResponse { - success: false, - content: Some(String::new()), - language: Some(language), - error: Some(format!("File not found: {}", decoded_path)), - })) - } - } -} - -pub async fn save_file( - Path(file_path): Path, - State(_state): State>, - Json(payload): Json, -) -> Result, (StatusCode, Json)> { - let decoded_path = urlencoding::decode(&file_path) - .map(|s| s.to_string()) - .unwrap_or(file_path); - - if let Some(parent) = std::path::Path::new(&decoded_path).parent() { - if !parent.exists() { - if let Err(e) = std::fs::create_dir_all(parent) { - log::error!("Failed to create directories for {}: {}", decoded_path, e); - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(SaveResponse { - success: false, - message: None, - error: Some(format!("Failed to create directories: {}", e)), - }), - )); - } - } - } - - match std::fs::write(&decoded_path, &payload.content) { - Ok(_) => { - log::info!("Successfully saved file: {}", decoded_path); - Ok(Json(SaveResponse { - success: true, - message: Some(format!("File saved: {}", decoded_path)), - error: None, - })) - } - Err(e) => { - log::error!("Failed to save file {}: {}", decoded_path, e); - Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(SaveResponse { - success: false, - message: None, - error: Some(format!("Failed to save file: {}", e)), - }), - )) - } - } -} - -pub async fn list_files( - State(_state): State>, -) -> Result, (StatusCode, Json)> { - let mut files = Vec::new(); - - let common_paths = vec![ - "index.html", - "styles.css", - "app.js", - "main.js", - "package.json", - "README.md", - ]; - - for path in common_paths { - if std::path::Path::new(path).exists() { - files.push(FileInfo { - name: path.to_string(), - path: path.to_string(), - }); - } - } - - if let Ok(entries) = std::fs::read_dir(".") { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_file() { - if let Some(name) = path.file_name().and_then(|n| n.to_str()) { - let ext = std::path::Path::new(name) - .extension() - .and_then(|e| e.to_str()) - .unwrap_or(""); - - if matches!( - ext, - "html" | "css" | "js" | "json" | "ts" | "bas" | "py" | "rs" | "md" - ) && !files.iter().any(|f| f.path == name) - { - files.push(FileInfo { - name: name.to_string(), - path: name.to_string(), - }); - } - } - } - } - } - - Ok(Json(FileListResponse { - success: true, - files, - error: None, - })) -} - -fn detect_language(file_path: &str) -> String { - let ext = std::path::Path::new(file_path) - .extension() - .and_then(|e| e.to_str()) - .unwrap_or("") - .to_lowercase(); - - match ext.as_str() { - "html" | "htm" => "html", - "css" => "css", - "js" => "javascript", - "json" => "json", - "ts" => "typescript", - "bas" => "basic", - "py" => "python", - "rs" => "rust", - "md" => "markdown", - "xml" => "xml", - "yaml" | "yml" => "yaml", - "sql" => "sql", - _ => "plaintext", - } - .to_string() -} - -pub fn configure_editor_routes() -> axum::Router> { - use axum::routing::{get, post}; - - axum::Router::new() - .route("/api/editor/file/:file_path", get(get_file)) - .route("/api/editor/file/:file_path", post(save_file)) - .route("/api/editor/files", get(list_files)) -} diff --git a/src/deployment/mod.rs b/src/deployment/mod.rs index 00f418b38..219a2cdd8 100644 --- a/src/deployment/mod.rs +++ b/src/deployment/mod.rs @@ -1,7 +1,16 @@ pub mod forgejo; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use std::sync::Arc; + +use crate::core::shared::state::AppState; // Re-export types from forgejo module pub use forgejo::{AppType, BuildConfig, ForgejoClient, ForgejoError, ForgejoRepo}; @@ -200,3 +209,226 @@ impl GeneratedApp { self.add_file(path, content.into_bytes()); } } + +// ============================================================================= +// API Types and Handlers +// ============================================================================= + +#[derive(Debug, Deserialize)] +pub struct DeploymentRequest { + pub app_name: String, + pub target: String, + pub environment: String, + pub manifest: serde_json::Value, +} + +#[derive(Debug, Serialize)] +pub struct DeploymentResponse { + pub success: bool, + pub url: Option, + pub deployment_type: Option, + pub status: Option, + pub error: Option, +} + +#[derive(Debug, Serialize)] +pub struct DeploymentTargetsResponse { + pub targets: Vec, +} + +#[derive(Debug, Serialize)] +pub struct DeploymentTargetInfo { + pub id: String, + pub name: String, + pub description: String, + pub features: Vec, +} + +/// Configure deployment routes +pub fn configure_deployment_routes() -> axum::Router> { + axum::Router::new() + .route("/api/deployment/targets", axum::routing::get(get_deployment_targets)) + .route("/api/deployment/deploy", axum::routing::post(deploy_app)) +} + +/// Get available deployment targets +pub async fn get_deployment_targets( + State(_state): State>, +) -> Result, DeploymentApiError> { + let targets = vec![ + DeploymentTargetInfo { + id: "internal".to_string(), + name: "GB Platform".to_string(), + description: "Deploy internally to General Bots platform".to_string(), + features: vec![ + "Instant deployment".to_string(), + "Shared resources".to_string(), + "Auto-scaling".to_string(), + "Built-in monitoring".to_string(), + "Zero configuration".to_string(), + ], + }, + DeploymentTargetInfo { + id: "external".to_string(), + name: "Forgejo ALM".to_string(), + description: "Deploy to external Git repository with CI/CD".to_string(), + features: vec![ + "Git-based deployment".to_string(), + "Custom domains".to_string(), + "CI/CD pipelines".to_string(), + "Version control".to_string(), + "Team collaboration".to_string(), + ], + }, + ]; + + Ok(Json(DeploymentTargetsResponse { targets })) +} + +/// Deploy an application +pub async fn deploy_app( + State(state): State>, + Json(request): Json, +) -> Result, DeploymentApiError> { + log::info!( + "Deployment request received: app={}, target={}, env={}", + request.app_name, + request.target, + request.environment + ); + + // Parse deployment target + let target = match request.target.as_str() { + "internal" => { + let route = request.manifest + .get("route") + .and_then(|v| v.as_str()) + .unwrap_or(&request.app_name) + .to_string(); + + let shared_resources = request.manifest + .get("shared_resources") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + + DeploymentTarget::Internal { + route, + shared_resources, + } + } + "external" => { + let repo_url = request.manifest + .get("repo_url") + .and_then(|v| v.as_str()) + .ok_or_else(|| DeploymentApiError::ValidationError("repo_url is required for external deployment".to_string()))? + .to_string(); + + let custom_domain = request.manifest + .get("custom_domain") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let ci_cd_enabled = request.manifest + .get("ci_cd_enabled") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + + DeploymentTarget::External { + repo_url, + custom_domain, + ci_cd_enabled, + } + } + _ => { + return Err(DeploymentApiError::ValidationError(format!( + "Unknown deployment target: {}", + request.target + ))); + } + }; + + // Parse environment + let environment = match request.environment.as_str() { + "development" => DeploymentEnvironment::Development, + "staging" => DeploymentEnvironment::Staging, + "production" => DeploymentEnvironment::Production, + _ => DeploymentEnvironment::Development, + }; + + // Create deployment configuration + let config = DeploymentConfig { + app_name: request.app_name.clone(), + target, + environment, + }; + + // Get Forgejo configuration from environment + let forgejo_url = std::env::var("FORGEJO_URL") + .unwrap_or_else(|_| "https://alm.pragmatismo.com.br".to_string()); + + let forgejo_token = std::env::var("FORGEJO_TOKEN").ok(); + + // Create deployment router + let internal_base_path = std::path::PathBuf::from("/opt/gbo/data/apps"); + let router = DeploymentRouter::new(forgejo_url, forgejo_token, internal_base_path); + + // Create a placeholder generated app + // In real implementation, this would come from the orchestrator + let generated_app = GeneratedApp::new( + config.app_name.clone(), + "Generated application".to_string(), + ); + + // Execute deployment + let result = router.deploy(config, generated_app).await + .map_err(|e| DeploymentApiError::DeploymentFailed(e.to_string()))?; + + log::info!( + "Deployment successful: url={}, type={}, status={:?}", + result.url, + result.deployment_type, + result.status + ); + + Ok(Json(DeploymentResponse { + success: true, + url: Some(result.url), + deployment_type: Some(result.deployment_type), + status: Some(format!("{:?}", result.status)), + error: None, + })) +} + +#[derive(Debug)] +pub enum DeploymentApiError { + ValidationError(String), + DeploymentFailed(String), + InternalError(String), +} + +impl IntoResponse for DeploymentApiError { + fn into_response(self) -> Response { + use crate::security::error_sanitizer::log_and_sanitize; + + let (status, message) = match self { + DeploymentApiError::ValidationError(msg) => { + (StatusCode::BAD_REQUEST, msg) + } + DeploymentApiError::DeploymentFailed(msg) => { + let sanitized = log_and_sanitize(&msg, "deployment", None); + (StatusCode::INTERNAL_SERVER_ERROR, sanitized) + } + DeploymentApiError::InternalError(msg) => { + let sanitized = log_and_sanitize(&msg, "deployment", None); + (StatusCode::INTERNAL_SERVER_ERROR, sanitized) + } + }; + + let body = Json(serde_json::json!({ + "success": false, + "error": message, + })); + + (status, body).into_response() + } +} diff --git a/src/main_module/server.rs b/src/main_module/server.rs index 7215ca4ed..5685aaecc 100644 --- a/src/main_module/server.rs +++ b/src/main_module/server.rs @@ -373,6 +373,9 @@ pub async fn run_axum_server( api_router = api_router.merge(crate::core::oauth::routes::configure()); + // Deployment routes for VibeCode platform + api_router = api_router.merge(crate::deployment::configure_deployment_routes()); + let site_path = app_state .config .as_ref()