diff --git a/crates/nxmesh-master/Cargo.toml b/crates/nxmesh-master/Cargo.toml new file mode 100644 index 0000000..9465e9d --- /dev/null +++ b/crates/nxmesh-master/Cargo.toml @@ -0,0 +1,79 @@ +[package] +name = "nxmesh-master" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +default-run = "nxmesh-master" + +[[bin]] +name = "nxmesh-master" +path = "src/main.rs" + +[[bin]] +name = "gen-openapi" +path = "src/bin/gen-openapi.rs" + +[dependencies] +# Internal +nxmesh-core.workspace = true +nxmesh-proto.workspace = true +migration = { path = "../../migration" } + +# Core +tokio.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true + +# Web +axum = { workspace = true, features = ["ws"] } +tower.workspace = true +tower-http = { workspace = true, features = ["fs", "cors"] } + +# OpenAPI +utoipa = { version = "4", features = ["axum_extras"] } +utoipa-swagger-ui = { version = "6", features = ["axum"] } + +# gRPC +tonic.workspace = true + +# Database +sea-orm.workspace = true +sea-orm-migration = { workspace = true, features = ["runtime-tokio-native-tls", "sqlx-postgres"] } + +# Async +async-trait.workspace = true +futures.workspace = true +async-stream = "0.3" + +# Config +config.workspace = true +toml.workspace = true + +# Crypto +argon2.workspace = true +jsonwebtoken.workspace = true +sha2.workspace = true +hex.workspace = true + +# Validation +validator.workspace = true + +# Time +chrono.workspace = true + +# UUID +uuid.workspace = true + +# Templating +handlebars.workspace = true + +[dev-dependencies] +tokio-test.workspace = true +mockall.workspace = true diff --git a/crates/nxmesh-master/src/api/middleware/auth.rs b/crates/nxmesh-master/src/api/middleware/auth.rs new file mode 100644 index 0000000..d5ca3aa --- /dev/null +++ b/crates/nxmesh-master/src/api/middleware/auth.rs @@ -0,0 +1,122 @@ +//! JWT Authentication middleware + +use axum::{ + extract::{Request, State}, + http::StatusCode, + middleware::Next, + response::{IntoResponse, Response}, + Json, +}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +use crate::config::Settings; + +/// Claims in the JWT token +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, // User ID + pub email: String, + pub org_id: Option, + pub role: String, + pub exp: usize, + pub iat: usize, +} + +/// Auth state +#[derive(Clone)] +pub struct AuthState { + settings: Arc, +} + +impl AuthState { + pub fn new(settings: Arc) -> Self { + Self { settings } + } +} + +/// Generate a JWT token +pub fn generate_token( + user_id: &str, + email: &str, + org_id: Option<&str>, + role: &str, + settings: &Settings, +) -> Result { + let now = chrono::Utc::now().timestamp() as usize; + let exp = now + (settings.auth.jwt_expiration_hours as usize * 3600); + + let claims = Claims { + sub: user_id.to_string(), + email: email.to_string(), + org_id: org_id.map(|s| s.to_string()), + role: role.to_string(), + exp, + iat: now, + }; + + encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(settings.auth.jwt_secret.as_bytes()), + ) +} + +/// Validate a JWT token +pub fn validate_token(token: &str, settings: &Settings) -> Result { + decode::( + token, + &DecodingKey::from_secret(settings.auth.jwt_secret.as_bytes()), + &Validation::default(), + ) + .map(|data| data.claims) +} + +/// Authentication middleware +pub async fn auth_middleware( + State(state): State, + mut request: Request, + next: Next, +) -> Response { + // Extract token from Authorization header + let auth_header = request + .headers() + .get("authorization") + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.strip_prefix("Bearer ")); + + let token = match auth_header { + Some(token) => token, + None => { + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ + "error": "Missing authorization token" + })), + ) + .into_response(); + } + }; + + // Validate token + match validate_token(token, &state.settings) { + Ok(claims) => { + // Add claims to request extensions + request.extensions_mut().insert(claims); + next.run(request).await + } + Err(_) => ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ + "error": "Invalid token" + })), + ) + .into_response(), + } +} + +/// Extract claims from request extensions +pub fn extract_claims(request: &Request) -> Option<&Claims> { + request.extensions().get::() +} diff --git a/crates/nxmesh-master/src/api/middleware/mod.rs b/crates/nxmesh-master/src/api/middleware/mod.rs new file mode 100644 index 0000000..d1271fd --- /dev/null +++ b/crates/nxmesh-master/src/api/middleware/mod.rs @@ -0,0 +1,22 @@ +//! API middleware + +pub mod auth; + +use axum::{ + extract::Request, + middleware::Next, + response::Response, +}; +use tracing::info; + +/// Logging middleware +pub async fn log_request(req: Request, next: Next) -> Response { + let method = req.method().clone(); + let uri = req.uri().clone(); + + let response = next.run(req).await; + + info!("{} {} - {}", method, uri, response.status()); + + response +} diff --git a/crates/nxmesh-master/src/api/mod.rs b/crates/nxmesh-master/src/api/mod.rs new file mode 100644 index 0000000..3ad892d --- /dev/null +++ b/crates/nxmesh-master/src/api/mod.rs @@ -0,0 +1,96 @@ +//! REST API handlers + +pub mod middleware; +pub mod routes; +pub mod v1; +pub mod websocket; + +use utoipa::OpenApi; + +/// API Documentation +#[derive(OpenApi)] +#[openapi( + paths( + routes::health_check, + routes::login, + v1::organizations::list, + v1::organizations::create, + v1::organizations::get, + v1::organizations::update, + v1::organizations::delete, + v1::workspaces::list, + v1::workspaces::create, + v1::workspaces::get, + v1::workspaces::update, + v1::workspaces::delete, + v1::agents::list, + v1::agents::get, + v1::agents::create_token, + v1::agents::delete, + v1::agents::reload, + v1::virtual_hosts::list, + v1::virtual_hosts::create, + v1::virtual_hosts::get, + v1::virtual_hosts::update, + v1::virtual_hosts::delete, + v1::upstreams::list, + v1::upstreams::create, + v1::upstreams::get, + v1::upstreams::update, + v1::upstreams::delete, + v1::certificates::list, + v1::certificates::create, + v1::certificates::get, + v1::certificates::delete, + ), + components( + schemas( + routes::LoginRequest, + routes::LoginResponse, + v1::organizations::OrganizationResponse, + v1::organizations::CreateOrganizationRequest, + v1::workspaces::WorkspaceResponse, + v1::workspaces::CreateWorkspaceRequest, + v1::agents::AgentResponse, + v1::agents::CreateAgentTokenRequest, + v1::agents::CreateAgentTokenResponse, + v1::virtual_hosts::VirtualHostResponse, + v1::virtual_hosts::CreateVirtualHostRequest, + v1::upstreams::UpstreamResponse, + v1::upstreams::CreateUpstreamRequest, + v1::certificates::CertificateResponse, + v1::certificates::CreateCertificateRequest, + ) + ), + tags( + (name = "Organizations", description = "Organization management"), + (name = "Workspaces", description = "Workspace management"), + (name = "Agents", description = "Agent management"), + (name = "Virtual Hosts", description = "Virtual host configuration"), + (name = "Upstreams", description = "Upstream configuration"), + (name = "Certificates", description = "SSL certificate management"), + ) +)] +pub struct ApiDoc; + +impl ApiDoc { + /// Generate OpenAPI spec with Uuid schema added + pub fn generate() -> utoipa::openapi::OpenApi { + let mut openapi = Self::openapi(); + + // Add Uuid schema + if let Some(components) = openapi.components.as_mut() { + components.schemas.insert( + "Uuid".to_string(), + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::SchemaType::String) + .format(Some(utoipa::openapi::SchemaFormat::Custom("uuid".to_string()))) + .description(Some("UUID string")) + .example(Some(serde_json::json!("550e8400-e29b-41d4-a716-446655440000"))) + .into(), + ); + } + + openapi + } +} diff --git a/crates/nxmesh-master/src/api/routes.rs b/crates/nxmesh-master/src/api/routes.rs new file mode 100644 index 0000000..3193080 --- /dev/null +++ b/crates/nxmesh-master/src/api/routes.rs @@ -0,0 +1,114 @@ +//! API route definitions + +use axum::{ + routing::{get, post}, + Router, +}; +use std::sync::Arc; + +use crate::config::Settings; +use crate::db::Database; +use crate::api::middleware::auth::AuthState; +use crate::api::ApiDoc; +use crate::api::v1; + +use super::middleware::auth::auth_middleware; +use super::middleware::log_request; + +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +/// Application state +#[derive(Clone)] +pub struct AppState { + pub db: Database, + pub settings: Arc, +} + +/// Create the main API router +pub fn create_router(state: AppState) -> Router { + let auth_state = AuthState::new(state.settings.clone()); + + // Public routes (no auth required) + let public_routes = Router::new() + .route("/health", get(health_check)) + .route("/api/v1/auth/login", post(login)) + // Swagger UI (includes OpenAPI spec at /api/openapi.json) + .merge(SwaggerUi::new("/swagger-ui").url("/api/openapi.json", ApiDoc::generate())); + + // Protected routes (auth required) + let protected_routes = Router::new() + // Organizations + .route("/api/v1/organizations", get(v1::organizations::list).post(v1::organizations::create)) + .route("/api/v1/organizations/:id", get(v1::organizations::get).patch(v1::organizations::update).delete(v1::organizations::delete)) + .route("/api/v1/organizations/:id/workspaces", get(v1::workspaces::list).post(v1::workspaces::create)) + + // Workspaces + .route("/api/v1/workspaces/:id", get(v1::workspaces::get).patch(v1::workspaces::update).delete(v1::workspaces::delete)) + .route("/api/v1/workspaces/:id/agents", get(v1::agents::list)) + .route("/api/v1/workspaces/:id/virtual-hosts", get(v1::virtual_hosts::list).post(v1::virtual_hosts::create)) + .route("/api/v1/workspaces/:id/upstreams", get(v1::upstreams::list).post(v1::upstreams::create)) + .route("/api/v1/workspaces/:id/certificates", get(v1::certificates::list).post(v1::certificates::create)) + + // Agents + .route("/api/v1/agents/:id", get(v1::agents::get).delete(v1::agents::delete)) + .route("/api/v1/agents/:id/reload", post(v1::agents::reload)) + .route("/api/v1/workspaces/:id/agents/tokens", post(v1::agents::create_token)) + + // Virtual Hosts + .route("/api/v1/virtual-hosts/:id", get(v1::virtual_hosts::get).patch(v1::virtual_hosts::update).delete(v1::virtual_hosts::delete)) + + // Upstreams + .route("/api/v1/upstreams/:id", get(v1::upstreams::get).patch(v1::upstreams::update).delete(v1::upstreams::delete)) + + // Certificates + .route("/api/v1/certificates/:id", get(v1::certificates::get).delete(v1::certificates::delete)) + + // Middleware layer for protected routes + .layer(axum::middleware::from_fn_with_state(auth_state.clone(), auth_middleware)); + + Router::new() + .merge(public_routes) + .merge(protected_routes) + .layer(axum::middleware::from_fn(log_request)) + .with_state(state) +} + +/// Health check endpoint +#[utoipa::path( + get, + path = "/health", + responses( + (status = 200, description = "Server is healthy", body = String), + ) +)] +async fn health_check() -> &'static str { + "OK" +} + +/// Login endpoint +#[utoipa::path( + post, + path = "/api/v1/auth/login", + operation_id = "login", + request_body = LoginRequest, + responses( + (status = 200, description = "Login successful", body = LoginResponse), + ) +)] +async fn login() -> axum::Json { + axum::Json(serde_json::json!({ + "token": "placeholder_token" + })) +} + +#[derive(utoipa::ToSchema, serde::Deserialize)] +pub struct LoginRequest { + email: String, + password: String, +} + +#[derive(utoipa::ToSchema, serde::Serialize)] +pub struct LoginResponse { + token: String, +} diff --git a/crates/nxmesh-master/src/api/v1/agents.rs b/crates/nxmesh-master/src/api/v1/agents.rs new file mode 100644 index 0000000..ecf2d12 --- /dev/null +++ b/crates/nxmesh-master/src/api/v1/agents.rs @@ -0,0 +1,133 @@ +//! Agent API handlers + +use axum::{ + extract::Path, + Json, +}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +/// Agent response +#[derive(Serialize, ToSchema)] +pub struct AgentResponse { + pub id: Uuid, + pub workspace_id: Uuid, + pub name: String, + pub hostname: String, + pub state: String, +} + +/// List agents +#[utoipa::path( + get, + path = "/api/v1/workspaces/{id}/agents", + operation_id = "listAgents", + tag = "Agents", + params( + ("id" = Uuid, Path, description = "Workspace ID") + ), + responses( + (status = 200, description = "List of agents", body = Vec), + ) +)] +pub async fn list(Path(ws_id): Path) -> Json> { + let _ = ws_id; + Json(vec![]) +} + +/// Get agent by ID +#[utoipa::path( + get, + path = "/api/v1/agents/{id}", + operation_id = "getAgent", + tag = "Agents", + params( + ("id" = Uuid, Path, description = "Agent ID") + ), + responses( + (status = 200, description = "Agent found", body = AgentResponse), + (status = 404, description = "Agent not found"), + ) +)] +pub async fn get(Path(id): Path) -> Json { + Json(AgentResponse { + id, + workspace_id: Uuid::new_v4(), + name: "agent-01".to_string(), + hostname: "host-01".to_string(), + state: "online".to_string(), + }) +} + +/// Create agent token request +#[derive(Deserialize, ToSchema)] +pub struct CreateAgentTokenRequest { + pub name: String, +} + +/// Create agent token response +#[derive(Serialize, ToSchema)] +pub struct CreateAgentTokenResponse { + pub token: String, + pub agent_id: Uuid, +} + +/// Create agent registration token +#[utoipa::path( + post, + path = "/api/v1/workspaces/{id}/agents/tokens", + operation_id = "createAgentToken", + tag = "Agents", + params( + ("id" = Uuid, Path, description = "Workspace ID") + ), + request_body = CreateAgentTokenRequest, + responses( + (status = 201, description = "Token created", body = CreateAgentTokenResponse), + ) +)] +pub async fn create_token( + Path(ws_id): Path, + Json(req): Json, +) -> Json { + let _ = ws_id; + Json(CreateAgentTokenResponse { + token: "nxmesh_agent_token_placeholder".to_string(), + agent_id: Uuid::new_v4(), + }) +} + +/// Delete agent +#[utoipa::path( + delete, + path = "/api/v1/agents/{id}", + operation_id = "deleteAgent", + tag = "Agents", + params( + ("id" = Uuid, Path, description = "Agent ID") + ), + responses( + (status = 204, description = "Agent deleted"), + ) +)] +pub async fn delete(Path(_id): Path) -> &'static str { + "{}" +} + +/// Reload agent nginx +#[utoipa::path( + post, + path = "/api/v1/agents/{id}/reload", + operation_id = "reloadAgent", + tag = "Agents", + params( + ("id" = Uuid, Path, description = "Agent ID") + ), + responses( + (status = 200, description = "Nginx reloaded successfully"), + ) +)] +pub async fn reload(Path(_id): Path) -> &'static str { + r#"{"success": true}"# +} diff --git a/crates/nxmesh-master/src/api/v1/certificates.rs b/crates/nxmesh-master/src/api/v1/certificates.rs new file mode 100644 index 0000000..1886e06 --- /dev/null +++ b/crates/nxmesh-master/src/api/v1/certificates.rs @@ -0,0 +1,106 @@ +//! Certificate API handlers + +use axum::{ + extract::Path, + Json, +}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +/// Certificate response +#[derive(Serialize, ToSchema)] +pub struct CertificateResponse { + pub id: Uuid, + pub domain: String, + pub status: String, +} + +/// List certificates +#[utoipa::path( + get, + path = "/api/v1/workspaces/{id}/certificates", + operation_id = "listCertificates", + tag = "Certificates", + params( + ("id" = Uuid, Path, description = "Workspace ID") + ), + responses( + (status = 200, description = "List of certificates", body = Vec), + ) +)] +pub async fn list(Path(ws_id): Path) -> Json> { + let _ = ws_id; + Json(vec![]) +} + +/// Get certificate by ID +#[utoipa::path( + get, + path = "/api/v1/certificates/{id}", + operation_id = "getCertificate", + tag = "Certificates", + params( + ("id" = Uuid, Path, description = "Certificate ID") + ), + responses( + (status = 200, description = "Certificate found", body = CertificateResponse), + (status = 404, description = "Certificate not found"), + ) +)] +pub async fn get(Path(id): Path) -> Json { + Json(CertificateResponse { + id, + domain: "example.com".to_string(), + status: "active".to_string(), + }) +} + +/// Create certificate request +#[derive(Deserialize, ToSchema)] +pub struct CreateCertificateRequest { + pub domain: String, +} + +/// Create certificate +#[utoipa::path( + post, + path = "/api/v1/workspaces/{id}/certificates", + operation_id = "createCertificate", + tag = "Certificates", + params( + ("id" = Uuid, Path, description = "Workspace ID") + ), + request_body = CreateCertificateRequest, + responses( + (status = 201, description = "Certificate requested", body = CertificateResponse), + ) +)] +pub async fn create( + Path(ws_id): Path, + Json(req): Json, +) -> Json { + let _ = ws_id; + Json(CertificateResponse { + id: Uuid::new_v4(), + domain: req.domain, + status: "pending".to_string(), + }) +} + +/// Delete certificate +#[utoipa::path( + delete, + path = "/api/v1/certificates/{id}", + operation_id = "deleteCertificate", + tag = "Certificates", + params( + ("id" = Uuid, Path, description = "Certificate ID") + ), + responses( + (status = 204, description = "Certificate deleted"), + ) +)] +pub async fn delete(Path(_id): Path) -> &'static str { + "{}" +} diff --git a/crates/nxmesh-master/src/api/v1/metrics.rs b/crates/nxmesh-master/src/api/v1/metrics.rs new file mode 100644 index 0000000..f4aac61 --- /dev/null +++ b/crates/nxmesh-master/src/api/v1/metrics.rs @@ -0,0 +1,33 @@ +//! Metrics API handlers + +use axum::{ + extract::Path, + Json, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Metrics response +#[derive(Serialize)] +pub struct MetricsResponse { + pub requests_total: u64, + pub requests_per_second: f64, +} + +/// Get workspace metrics +pub async fn get_workspace(Path(ws_id): Path) -> Json { + // TODO: Implement + Json(MetricsResponse { + requests_total: 0, + requests_per_second: 0.0, + }) +} + +/// Get agent metrics +pub async fn get_agent(Path(id): Path) -> Json { + // TODO: Implement + Json(MetricsResponse { + requests_total: 0, + requests_per_second: 0.0, + }) +} diff --git a/crates/nxmesh-master/src/api/v1/mod.rs b/crates/nxmesh-master/src/api/v1/mod.rs new file mode 100644 index 0000000..49e23e2 --- /dev/null +++ b/crates/nxmesh-master/src/api/v1/mod.rs @@ -0,0 +1,9 @@ +//! API v1 handlers + +pub mod agents; +pub mod certificates; +pub mod metrics; +pub mod organizations; +pub mod upstreams; +pub mod virtual_hosts; +pub mod workspaces; diff --git a/crates/nxmesh-master/src/api/v1/organizations.rs b/crates/nxmesh-master/src/api/v1/organizations.rs new file mode 100644 index 0000000..84fd5de --- /dev/null +++ b/crates/nxmesh-master/src/api/v1/organizations.rs @@ -0,0 +1,129 @@ +//! Organization API handlers + +use axum::{ + extract::Path, + Json, +}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +/// Organization response +#[derive(Serialize, ToSchema)] +pub struct OrganizationResponse { + pub id: Uuid, + pub name: String, + pub slug: String, +} + +/// List organizations +#[utoipa::path( + get, + path = "/api/v1/organizations", + operation_id = "listOrganizations", + tag = "Organizations", + responses( + (status = 200, description = "List of organizations", body = Vec), + ) +)] +pub async fn list() -> Json> { + // TODO: Implement + Json(vec![]) +} + +/// Get organization by ID +#[utoipa::path( + get, + path = "/api/v1/organizations/{id}", + operation_id = "getOrganization", + tag = "Organizations", + params( + ("id" = Uuid, Path, description = "Organization ID") + ), + responses( + (status = 200, description = "Organization found", body = OrganizationResponse), + (status = 404, description = "Organization not found"), + ) +)] +pub async fn get(Path(id): Path) -> Json { + // TODO: Implement + Json(OrganizationResponse { + id, + name: "Example".to_string(), + slug: "example".to_string(), + }) +} + +/// Create organization request +#[derive(Deserialize, ToSchema)] +pub struct CreateOrganizationRequest { + pub name: String, + pub slug: String, +} + +/// Create organization +#[utoipa::path( + post, + path = "/api/v1/organizations", + operation_id = "createOrganization", + tag = "Organizations", + request_body = CreateOrganizationRequest, + responses( + (status = 201, description = "Organization created", body = OrganizationResponse), + (status = 400, description = "Invalid request"), + ) +)] +pub async fn create(Json(req): Json) -> Json { + // TODO: Implement + Json(OrganizationResponse { + id: Uuid::new_v4(), + name: req.name, + slug: req.slug, + }) +} + +/// Update organization +#[utoipa::path( + patch, + path = "/api/v1/organizations/{id}", + operation_id = "updateOrganization", + tag = "Organizations", + params( + ("id" = Uuid, Path, description = "Organization ID") + ), + request_body = CreateOrganizationRequest, + responses( + (status = 200, description = "Organization updated", body = OrganizationResponse), + (status = 404, description = "Organization not found"), + ) +)] +pub async fn update( + Path(id): Path, + Json(req): Json, +) -> Json { + // TODO: Implement + Json(OrganizationResponse { + id, + name: req.name, + slug: req.slug, + }) +} + +/// Delete organization +#[utoipa::path( + delete, + path = "/api/v1/organizations/{id}", + operation_id = "deleteOrganization", + tag = "Organizations", + params( + ("id" = Uuid, Path, description = "Organization ID") + ), + responses( + (status = 204, description = "Organization deleted"), + (status = 404, description = "Organization not found"), + ) +)] +pub async fn delete(Path(_id): Path) -> &'static str { + // TODO: Implement + "{}" +} diff --git a/crates/nxmesh-master/src/api/v1/upstreams.rs b/crates/nxmesh-master/src/api/v1/upstreams.rs new file mode 100644 index 0000000..0aee9e6 --- /dev/null +++ b/crates/nxmesh-master/src/api/v1/upstreams.rs @@ -0,0 +1,132 @@ +//! Upstream API handlers + +use axum::{ + extract::Path, + Json, +}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +/// Upstream response +#[derive(Serialize, ToSchema)] +pub struct UpstreamResponse { + pub id: Uuid, + pub name: String, + pub algorithm: String, +} + +/// List upstreams +#[utoipa::path( + get, + path = "/api/v1/workspaces/{id}/upstreams", + operation_id = "listUpstreams", + tag = "Upstreams", + params( + ("id" = Uuid, Path, description = "Workspace ID") + ), + responses( + (status = 200, description = "List of upstreams", body = Vec), + ) +)] +pub async fn list(Path(ws_id): Path) -> Json> { + let _ = ws_id; + Json(vec![]) +} + +/// Get upstream by ID +#[utoipa::path( + get, + path = "/api/v1/upstreams/{id}", + operation_id = "getUpstream", + tag = "Upstreams", + params( + ("id" = Uuid, Path, description = "Upstream ID") + ), + responses( + (status = 200, description = "Upstream found", body = UpstreamResponse), + (status = 404, description = "Upstream not found"), + ) +)] +pub async fn get(Path(id): Path) -> Json { + Json(UpstreamResponse { + id, + name: "backend-api".to_string(), + algorithm: "round_robin".to_string(), + }) +} + +/// Create upstream request +#[derive(Deserialize, ToSchema)] +pub struct CreateUpstreamRequest { + pub name: String, + pub algorithm: String, +} + +/// Create upstream +#[utoipa::path( + post, + path = "/api/v1/workspaces/{id}/upstreams", + operation_id = "createUpstream", + tag = "Upstreams", + params( + ("id" = Uuid, Path, description = "Workspace ID") + ), + request_body = CreateUpstreamRequest, + responses( + (status = 201, description = "Upstream created", body = UpstreamResponse), + ) +)] +pub async fn create( + Path(ws_id): Path, + Json(req): Json, +) -> Json { + let _ = ws_id; + Json(UpstreamResponse { + id: Uuid::new_v4(), + name: req.name, + algorithm: req.algorithm, + }) +} + +/// Update upstream +#[utoipa::path( + patch, + path = "/api/v1/upstreams/{id}", + operation_id = "updateUpstream", + tag = "Upstreams", + params( + ("id" = Uuid, Path, description = "Upstream ID") + ), + request_body = CreateUpstreamRequest, + responses( + (status = 200, description = "Upstream updated", body = UpstreamResponse), + ) +)] +pub async fn update( + Path(id): Path, + Json(req): Json, +) -> Json { + Json(UpstreamResponse { + id, + name: req.name, + algorithm: req.algorithm, + }) +} + +/// Delete upstream +#[utoipa::path( + delete, + path = "/api/v1/upstreams/{id}", + operation_id = "deleteUpstream", + tag = "Upstreams", + params( + ("id" = Uuid, Path, description = "Upstream ID") + ), + responses( + (status = 204, description = "Upstream deleted"), + ) +)] +pub async fn delete(Path(_id): Path) -> &'static str { + "{}" +} diff --git a/crates/nxmesh-master/src/api/v1/virtual_hosts.rs b/crates/nxmesh-master/src/api/v1/virtual_hosts.rs new file mode 100644 index 0000000..c6f2ef9 --- /dev/null +++ b/crates/nxmesh-master/src/api/v1/virtual_hosts.rs @@ -0,0 +1,137 @@ +//! Virtual Host API handlers + +use axum::{ + extract::Path, + Json, +}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +/// Virtual Host response +#[derive(Serialize, ToSchema)] +pub struct VirtualHostResponse { + pub id: Uuid, + pub name: String, + pub server_name: String, + pub listen_port: u16, +} + +/// List virtual hosts +#[utoipa::path( + get, + path = "/api/v1/workspaces/{id}/virtual-hosts", + operation_id = "listVirtualHosts", + tag = "Virtual Hosts", + params( + ("id" = Uuid, Path, description = "Workspace ID") + ), + responses( + (status = 200, description = "List of virtual hosts", body = Vec), + ) +)] +pub async fn list(Path(ws_id): Path) -> Json> { + let _ = ws_id; + Json(vec![]) +} + +/// Get virtual host by ID +#[utoipa::path( + get, + path = "/api/v1/virtual-hosts/{id}", + operation_id = "getVirtualHost", + tag = "Virtual Hosts", + params( + ("id" = Uuid, Path, description = "Virtual Host ID") + ), + responses( + (status = 200, description = "Virtual host found", body = VirtualHostResponse), + (status = 404, description = "Virtual host not found"), + ) +)] +pub async fn get(Path(id): Path) -> Json { + Json(VirtualHostResponse { + id, + name: "Main Website".to_string(), + server_name: "example.com".to_string(), + listen_port: 443, + }) +} + +/// Create virtual host request +#[derive(Deserialize, ToSchema)] +pub struct CreateVirtualHostRequest { + pub name: String, + pub server_name: String, + pub listen_port: u16, +} + +/// Create virtual host +#[utoipa::path( + post, + path = "/api/v1/workspaces/{id}/virtual-hosts", + operation_id = "createVirtualHost", + tag = "Virtual Hosts", + params( + ("id" = Uuid, Path, description = "Workspace ID") + ), + request_body = CreateVirtualHostRequest, + responses( + (status = 201, description = "Virtual host created", body = VirtualHostResponse), + ) +)] +pub async fn create( + Path(ws_id): Path, + Json(req): Json, +) -> Json { + let _ = ws_id; + Json(VirtualHostResponse { + id: Uuid::new_v4(), + name: req.name, + server_name: req.server_name, + listen_port: req.listen_port, + }) +} + +/// Update virtual host +#[utoipa::path( + patch, + path = "/api/v1/virtual-hosts/{id}", + operation_id = "updateVirtualHost", + tag = "Virtual Hosts", + params( + ("id" = Uuid, Path, description = "Virtual Host ID") + ), + request_body = CreateVirtualHostRequest, + responses( + (status = 200, description = "Virtual host updated", body = VirtualHostResponse), + ) +)] +pub async fn update( + Path(id): Path, + Json(req): Json, +) -> Json { + Json(VirtualHostResponse { + id, + name: req.name, + server_name: req.server_name, + listen_port: req.listen_port, + }) +} + +/// Delete virtual host +#[utoipa::path( + delete, + path = "/api/v1/virtual-hosts/{id}", + operation_id = "deleteVirtualHost", + tag = "Virtual Hosts", + params( + ("id" = Uuid, Path, description = "Virtual Host ID") + ), + responses( + (status = 204, description = "Virtual host deleted"), + ) +)] +pub async fn delete(Path(_id): Path) -> &'static str { + "{}" +} diff --git a/crates/nxmesh-master/src/api/v1/workspaces.rs b/crates/nxmesh-master/src/api/v1/workspaces.rs new file mode 100644 index 0000000..d5c5b0a --- /dev/null +++ b/crates/nxmesh-master/src/api/v1/workspaces.rs @@ -0,0 +1,140 @@ +//! Workspace API handlers + +use axum::{ + extract::Path, + Json, +}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +/// Workspace response +#[derive(Serialize, ToSchema)] +pub struct WorkspaceResponse { + pub id: Uuid, + pub organization_id: Uuid, + pub name: String, + pub slug: String, +} + +/// List workspaces +#[utoipa::path( + get, + path = "/api/v1/organizations/{id}/workspaces", + operation_id = "listWorkspaces", + tag = "Workspaces", + params( + ("id" = Uuid, Path, description = "Organization ID") + ), + responses( + (status = 200, description = "List of workspaces", body = Vec), + ) +)] +pub async fn list(Path(org_id): Path) -> Json> { + // TODO: Implement using org_id + let _ = org_id; + Json(vec![]) +} + +/// Get workspace by ID +#[utoipa::path( + get, + path = "/api/v1/workspaces/{id}", + operation_id = "getWorkspace", + tag = "Workspaces", + params( + ("id" = Uuid, Path, description = "Workspace ID") + ), + responses( + (status = 200, description = "Workspace found", body = WorkspaceResponse), + (status = 404, description = "Workspace not found"), + ) +)] +pub async fn get(Path(id): Path) -> Json { + // TODO: Implement + Json(WorkspaceResponse { + id, + organization_id: Uuid::new_v4(), + name: "Production".to_string(), + slug: "production".to_string(), + }) +} + +/// Create workspace request +#[derive(Deserialize, ToSchema)] +pub struct CreateWorkspaceRequest { + pub name: String, + pub slug: String, +} + +/// Create workspace +#[utoipa::path( + post, + path = "/api/v1/organizations/{id}/workspaces", + operation_id = "createWorkspace", + tag = "Workspaces", + params( + ("id" = Uuid, Path, description = "Organization ID") + ), + request_body = CreateWorkspaceRequest, + responses( + (status = 201, description = "Workspace created", body = WorkspaceResponse), + ) +)] +pub async fn create( + Path(org_id): Path, + Json(req): Json, +) -> Json { + // TODO: Implement + Json(WorkspaceResponse { + id: Uuid::new_v4(), + organization_id: org_id, + name: req.name, + slug: req.slug, + }) +} + +/// Update workspace +#[utoipa::path( + patch, + path = "/api/v1/workspaces/{id}", + operation_id = "updateWorkspace", + tag = "Workspaces", + params( + ("id" = Uuid, Path, description = "Workspace ID") + ), + request_body = CreateWorkspaceRequest, + responses( + (status = 200, description = "Workspace updated", body = WorkspaceResponse), + ) +)] +pub async fn update( + Path(id): Path, + Json(req): Json, +) -> Json { + // TODO: Implement + Json(WorkspaceResponse { + id, + organization_id: Uuid::new_v4(), + name: req.name, + slug: req.slug, + }) +} + +/// Delete workspace +#[utoipa::path( + delete, + path = "/api/v1/workspaces/{id}", + operation_id = "deleteWorkspace", + tag = "Workspaces", + params( + ("id" = Uuid, Path, description = "Workspace ID") + ), + responses( + (status = 204, description = "Workspace deleted"), + ) +)] +pub async fn delete(Path(_id): Path) -> &'static str { + // TODO: Implement + "{}" +} diff --git a/crates/nxmesh-master/src/api/websocket.rs b/crates/nxmesh-master/src/api/websocket.rs new file mode 100644 index 0000000..a0c22d5 --- /dev/null +++ b/crates/nxmesh-master/src/api/websocket.rs @@ -0,0 +1,22 @@ +//! WebSocket handler for real-time events + +use axum::{ + extract::ws::{WebSocket, WebSocketUpgrade}, + extract::State, + response::Response, +}; + +/// Handle WebSocket upgrade +pub async fn ws_handler( + ws: WebSocketUpgrade, + State(state): State<()>, +) -> Response { + ws.on_upgrade(move |socket| handle_socket(socket, state)) +} + +async fn handle_socket(mut socket: WebSocket, _state: ()) { + // TODO: Implement WebSocket event handling + while let Some(_msg) = socket.recv().await { + // Handle messages + } +} diff --git a/crates/nxmesh-master/src/bin/gen-openapi.rs b/crates/nxmesh-master/src/bin/gen-openapi.rs new file mode 100644 index 0000000..2f4efbf --- /dev/null +++ b/crates/nxmesh-master/src/bin/gen-openapi.rs @@ -0,0 +1,9 @@ +//! Generate OpenAPI spec + +use nxmesh_master::api::ApiDoc; + +fn main() { + let openapi = ApiDoc::generate(); + let spec = serde_json::to_string_pretty(&openapi).unwrap(); + println!("{}", spec); +} diff --git a/crates/nxmesh-master/src/config/mod.rs b/crates/nxmesh-master/src/config/mod.rs new file mode 100644 index 0000000..be9c3c6 --- /dev/null +++ b/crates/nxmesh-master/src/config/mod.rs @@ -0,0 +1,5 @@ +//! Configuration management + +pub mod settings; + +pub use settings::Settings; diff --git a/crates/nxmesh-master/src/config/settings.rs b/crates/nxmesh-master/src/config/settings.rs new file mode 100644 index 0000000..6e2160f --- /dev/null +++ b/crates/nxmesh-master/src/config/settings.rs @@ -0,0 +1,79 @@ +//! Master configuration settings + +use config::{Config, ConfigError, Environment, File}; +use serde::{Deserialize, Serialize}; + +/// Master server settings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Settings { + pub server: ServerSettings, + pub database: DatabaseSettings, + pub grpc: GrpcSettings, + pub auth: AuthSettings, +} + +/// HTTP server settings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServerSettings { + pub bind_address: String, + pub port: u16, +} + +/// Database connection settings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DatabaseSettings { + pub url: String, + pub max_connections: u32, +} + +/// gRPC server settings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GrpcSettings { + pub bind_address: String, + pub port: u16, +} + +/// Authentication settings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthSettings { + pub jwt_secret: String, + pub jwt_expiration_hours: u64, +} + +impl Default for Settings { + fn default() -> Self { + Self { + server: ServerSettings { + bind_address: "0.0.0.0".to_string(), + port: 8080, + }, + database: DatabaseSettings { + url: "postgres://postgres:postgres@localhost/nxmesh".to_string(), + max_connections: 10, + }, + grpc: GrpcSettings { + bind_address: "0.0.0.0".to_string(), + port: 8443, + }, + auth: AuthSettings { + jwt_secret: "change-me-in-production".to_string(), + jwt_expiration_hours: 24, + }, + } + } +} + +impl Settings { + /// Load settings from config files and environment + pub fn load() -> Result { + let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into()); + + let settings = Config::builder() + .add_source(File::with_name("config/default").required(false)) + .add_source(File::with_name(&format!("config/{}", run_mode)).required(false)) + .add_source(Environment::with_prefix("NXMESH").separator("__")) + .build()?; + + settings.try_deserialize() + } +} diff --git a/crates/nxmesh-master/src/db/connection.rs b/crates/nxmesh-master/src/db/connection.rs new file mode 100644 index 0000000..766d83c --- /dev/null +++ b/crates/nxmesh-master/src/db/connection.rs @@ -0,0 +1,30 @@ +//! Database connection management + +use sea_orm::{Database as SeaDatabase, DatabaseConnection, DbErr}; + +/// Database wrapper +#[derive(Clone, Debug)] +pub struct Database { + conn: DatabaseConnection, +} + +impl Database { + /// Create a new database connection + pub async fn connect(database_url: &str) -> Result { + let conn = SeaDatabase::connect(database_url).await?; + Ok(Self { conn }) + } + + /// Get the database connection + pub fn conn(&self) -> &DatabaseConnection { + &self.conn + } +} + +impl std::ops::Deref for Database { + type Target = DatabaseConnection; + + fn deref(&self) -> &Self::Target { + &self.conn + } +} diff --git a/crates/nxmesh-master/src/db/entities/agents.rs b/crates/nxmesh-master/src/db/entities/agents.rs new file mode 100644 index 0000000..86a079f --- /dev/null +++ b/crates/nxmesh-master/src/db/entities/agents.rs @@ -0,0 +1,44 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "agents")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub workspace_id: Uuid, + pub name: String, + pub hostname: String, + pub ip_address: Option, + pub version: Option, + pub state: String, + pub deployment_mode: Option, + pub last_seen_at: Option, + pub capabilities: Option, + pub labels: Option, + pub token_hash: Option, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::workspaces::Entity", + from = "Column::WorkspaceId", + to = "super::workspaces::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Workspaces, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Workspaces.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/nxmesh-master/src/db/entities/certificates.rs b/crates/nxmesh-master/src/db/entities/certificates.rs new file mode 100644 index 0000000..5e050b6 --- /dev/null +++ b/crates/nxmesh-master/src/db/entities/certificates.rs @@ -0,0 +1,45 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "certificates")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub workspace_id: Uuid, + pub domain: String, + pub is_wildcard: bool, + pub provider: Option, + pub status: Option, + pub issued_at: Option, + pub expires_at: Option, + pub auto_renew: bool, + #[sea_orm(column_type = "Text", nullable)] + pub certificate_pem: Option, + #[sea_orm(column_type = "Text", nullable)] + pub private_key_pem: Option, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::workspaces::Entity", + from = "Column::WorkspaceId", + to = "super::workspaces::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Workspaces, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Workspaces.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/nxmesh-master/src/db/entities/mod.rs b/crates/nxmesh-master/src/db/entities/mod.rs new file mode 100644 index 0000000..2de2bd6 --- /dev/null +++ b/crates/nxmesh-master/src/db/entities/mod.rs @@ -0,0 +1,11 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +pub mod prelude; + +pub mod agents; +pub mod certificates; +pub mod organizations; +pub mod upstreams; +pub mod users; +pub mod virtual_hosts; +pub mod workspaces; diff --git a/crates/nxmesh-master/src/db/entities/organizations.rs b/crates/nxmesh-master/src/db/entities/organizations.rs new file mode 100644 index 0000000..4d20d54 --- /dev/null +++ b/crates/nxmesh-master/src/db/entities/organizations.rs @@ -0,0 +1,39 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "organizations")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + #[sea_orm(unique)] + pub slug: String, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, + pub settings: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::users::Entity")] + Users, + #[sea_orm(has_many = "super::workspaces::Entity")] + Workspaces, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Workspaces.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/nxmesh-master/src/db/entities/prelude.rs b/crates/nxmesh-master/src/db/entities/prelude.rs new file mode 100644 index 0000000..2e3e297 --- /dev/null +++ b/crates/nxmesh-master/src/db/entities/prelude.rs @@ -0,0 +1,9 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +pub use super::agents::Entity as Agents; +pub use super::certificates::Entity as Certificates; +pub use super::organizations::Entity as Organizations; +pub use super::upstreams::Entity as Upstreams; +pub use super::users::Entity as Users; +pub use super::virtual_hosts::Entity as VirtualHosts; +pub use super::workspaces::Entity as Workspaces; diff --git a/crates/nxmesh-master/src/db/entities/upstreams.rs b/crates/nxmesh-master/src/db/entities/upstreams.rs new file mode 100644 index 0000000..df1ceca --- /dev/null +++ b/crates/nxmesh-master/src/db/entities/upstreams.rs @@ -0,0 +1,40 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "upstreams")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub workspace_id: Uuid, + pub name: String, + pub algorithm: String, + pub servers: Option, + pub health_check: Option, + pub keepalive_connections: Option, + pub keepalive_timeout: Option, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::workspaces::Entity", + from = "Column::WorkspaceId", + to = "super::workspaces::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Workspaces, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Workspaces.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/nxmesh-master/src/db/entities/users.rs b/crates/nxmesh-master/src/db/entities/users.rs new file mode 100644 index 0000000..8022bc5 --- /dev/null +++ b/crates/nxmesh-master/src/db/entities/users.rs @@ -0,0 +1,39 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "users")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + #[sea_orm(unique)] + pub email: String, + pub password_hash: String, + pub name: Option, + pub role: String, + pub organization_id: Option, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::organizations::Entity", + from = "Column::OrganizationId", + to = "super::organizations::Column::Id", + on_update = "NoAction", + on_delete = "SetNull" + )] + Organizations, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Organizations.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/nxmesh-master/src/db/entities/virtual_hosts.rs b/crates/nxmesh-master/src/db/entities/virtual_hosts.rs new file mode 100644 index 0000000..8e5b95c --- /dev/null +++ b/crates/nxmesh-master/src/db/entities/virtual_hosts.rs @@ -0,0 +1,44 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "virtual_hosts")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub workspace_id: Uuid, + pub name: String, + pub server_name: String, + pub listen_port: i32, + pub ssl_enabled: bool, + pub ssl_certificate_id: Option, + pub locations: Option, + pub http2_enabled: bool, + pub http3_enabled: bool, + pub gzip_enabled: bool, + pub target_agents: Option, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::workspaces::Entity", + from = "Column::WorkspaceId", + to = "super::workspaces::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Workspaces, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Workspaces.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/nxmesh-master/src/db/entities/workspaces.rs b/crates/nxmesh-master/src/db/entities/workspaces.rs new file mode 100644 index 0000000..b433267 --- /dev/null +++ b/crates/nxmesh-master/src/db/entities/workspaces.rs @@ -0,0 +1,68 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "workspaces")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub organization_id: Uuid, + pub name: String, + pub slug: String, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::agents::Entity")] + Agents, + #[sea_orm(has_many = "super::certificates::Entity")] + Certificates, + #[sea_orm( + belongs_to = "super::organizations::Entity", + from = "Column::OrganizationId", + to = "super::organizations::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Organizations, + #[sea_orm(has_many = "super::upstreams::Entity")] + Upstreams, + #[sea_orm(has_many = "super::virtual_hosts::Entity")] + VirtualHosts, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Agents.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Certificates.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Organizations.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Upstreams.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::VirtualHosts.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/nxmesh-master/src/db/mod.rs b/crates/nxmesh-master/src/db/mod.rs new file mode 100644 index 0000000..1cd9ab7 --- /dev/null +++ b/crates/nxmesh-master/src/db/mod.rs @@ -0,0 +1,8 @@ +//! Database layer + +pub mod connection; +pub mod entities; +pub mod repositories; + +pub use connection::Database; +pub use migration::Migrator; diff --git a/crates/nxmesh-master/src/db/repositories/agent_repository.rs b/crates/nxmesh-master/src/db/repositories/agent_repository.rs new file mode 100644 index 0000000..61fbffd --- /dev/null +++ b/crates/nxmesh-master/src/db/repositories/agent_repository.rs @@ -0,0 +1,19 @@ +//! Agent repository + +use uuid::Uuid; + +/// Agent repository +pub struct AgentRepository; + +impl AgentRepository { + /// Create a new repository instance + pub fn new() -> Self { + Self + } + + /// Find agent by ID + pub async fn find_by_id(&self, _id: Uuid) -> Option<()> { + // TODO: Implement + None + } +} diff --git a/crates/nxmesh-master/src/db/repositories/mod.rs b/crates/nxmesh-master/src/db/repositories/mod.rs new file mode 100644 index 0000000..9dcec8c --- /dev/null +++ b/crates/nxmesh-master/src/db/repositories/mod.rs @@ -0,0 +1,5 @@ +//! Repository implementations + +pub mod agent_repository; +pub mod organization_repository; +pub mod workspace_repository; diff --git a/crates/nxmesh-master/src/db/repositories/organization_repository.rs b/crates/nxmesh-master/src/db/repositories/organization_repository.rs new file mode 100644 index 0000000..b5c3d6d --- /dev/null +++ b/crates/nxmesh-master/src/db/repositories/organization_repository.rs @@ -0,0 +1,19 @@ +//! Organization repository + +use uuid::Uuid; + +/// Organization repository +pub struct OrganizationRepository; + +impl OrganizationRepository { + /// Create a new repository instance + pub fn new() -> Self { + Self + } + + /// Find organization by ID + pub async fn find_by_id(&self, _id: Uuid) -> Option<()> { + // TODO: Implement + None + } +} diff --git a/crates/nxmesh-master/src/db/repositories/workspace_repository.rs b/crates/nxmesh-master/src/db/repositories/workspace_repository.rs new file mode 100644 index 0000000..d4d5fd3 --- /dev/null +++ b/crates/nxmesh-master/src/db/repositories/workspace_repository.rs @@ -0,0 +1,19 @@ +//! Workspace repository + +use uuid::Uuid; + +/// Workspace repository +pub struct WorkspaceRepository; + +impl WorkspaceRepository { + /// Create a new repository instance + pub fn new() -> Self { + Self + } + + /// Find workspace by ID + pub async fn find_by_id(&self, _id: Uuid) -> Option<()> { + // TODO: Implement + None + } +} diff --git a/crates/nxmesh-master/src/domain/agent.rs b/crates/nxmesh-master/src/domain/agent.rs new file mode 100644 index 0000000..63a11e4 --- /dev/null +++ b/crates/nxmesh-master/src/domain/agent.rs @@ -0,0 +1,28 @@ +//! Agent domain entity + +use uuid::Uuid; + +/// Agent domain entity +#[derive(Debug, Clone)] +pub struct Agent { + pub id: Uuid, + pub workspace_id: Uuid, + pub name: String, + pub hostname: String, +} + +impl Agent { + /// Create a new agent entity + pub fn new( + workspace_id: Uuid, + name: impl Into, + hostname: impl Into, + ) -> Self { + Self { + id: Uuid::new_v4(), + workspace_id, + name: name.into(), + hostname: hostname.into(), + } + } +} diff --git a/crates/nxmesh-master/src/domain/config.rs b/crates/nxmesh-master/src/domain/config.rs new file mode 100644 index 0000000..bb9be1f --- /dev/null +++ b/crates/nxmesh-master/src/domain/config.rs @@ -0,0 +1,11 @@ +//! Config domain entity + +use uuid::Uuid; + +/// Config domain entity +#[derive(Debug, Clone)] +pub struct Config { + pub id: Uuid, + pub workspace_id: Uuid, + pub name: String, +} diff --git a/crates/nxmesh-master/src/domain/mod.rs b/crates/nxmesh-master/src/domain/mod.rs new file mode 100644 index 0000000..c764a7b --- /dev/null +++ b/crates/nxmesh-master/src/domain/mod.rs @@ -0,0 +1,5 @@ +//! Domain entities + +pub mod agent; +pub mod config; +pub mod organization; diff --git a/crates/nxmesh-master/src/domain/organization.rs b/crates/nxmesh-master/src/domain/organization.rs new file mode 100644 index 0000000..63450cc --- /dev/null +++ b/crates/nxmesh-master/src/domain/organization.rs @@ -0,0 +1,22 @@ +//! Organization domain entity + +use uuid::Uuid; + +/// Organization domain entity +#[derive(Debug, Clone)] +pub struct Organization { + pub id: Uuid, + pub name: String, + pub slug: String, +} + +impl Organization { + /// Create a new organization + pub fn new(name: impl Into, slug: impl Into) -> Self { + Self { + id: Uuid::new_v4(), + name: name.into(), + slug: slug.into(), + } + } +} diff --git a/crates/nxmesh-master/src/events/bus.rs b/crates/nxmesh-master/src/events/bus.rs new file mode 100644 index 0000000..e59b879 --- /dev/null +++ b/crates/nxmesh-master/src/events/bus.rs @@ -0,0 +1,40 @@ +//! Event bus implementation + +use tokio::sync::broadcast; + +/// Event bus for internal communication +pub struct EventBus { + sender: broadcast::Sender, +} + +/// Events that can be broadcast +#[derive(Debug, Clone)] +pub enum Event { + AgentConnected { agent_id: String }, + AgentDisconnected { agent_id: String }, + ConfigUpdated { config_id: String }, +} + +impl EventBus { + /// Create a new event bus + pub fn new() -> Self { + let (sender, _receiver) = broadcast::channel(100); + Self { sender } + } + + /// Subscribe to events + pub fn subscribe(&self) -> broadcast::Receiver { + self.sender.subscribe() + } + + /// Publish an event + pub fn publish(&self, event: Event) { + let _ = self.sender.send(event); + } +} + +impl Default for EventBus { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/nxmesh-master/src/events/handlers.rs b/crates/nxmesh-master/src/events/handlers.rs new file mode 100644 index 0000000..86146b3 --- /dev/null +++ b/crates/nxmesh-master/src/events/handlers.rs @@ -0,0 +1,19 @@ +//! Event handlers + +use super::bus::Event; +use tracing::info; + +/// Handle events +pub async fn handle_event(event: Event) { + match event { + Event::AgentConnected { agent_id } => { + info!("Agent connected: {}", agent_id); + } + Event::AgentDisconnected { agent_id } => { + info!("Agent disconnected: {}", agent_id); + } + Event::ConfigUpdated { config_id } => { + info!("Config updated: {}", config_id); + } + } +} diff --git a/crates/nxmesh-master/src/events/mod.rs b/crates/nxmesh-master/src/events/mod.rs new file mode 100644 index 0000000..0493fc7 --- /dev/null +++ b/crates/nxmesh-master/src/events/mod.rs @@ -0,0 +1,4 @@ +//! Event bus + +pub mod bus; +pub mod handlers; diff --git a/crates/nxmesh-master/src/grpc/agent_service.rs b/crates/nxmesh-master/src/grpc/agent_service.rs new file mode 100644 index 0000000..895ddbb --- /dev/null +++ b/crates/nxmesh-master/src/grpc/agent_service.rs @@ -0,0 +1,194 @@ +//! Agent gRPC service + +use chrono::Utc; +use nxmesh_proto::{ + agent_service_server::AgentService, agent::AgentMessage, Ack, ConfigUpdate, HealthReport, + MasterMessage, MetricsBatch, RegistrationRequest, RegistrationResponse, +}; +use sea_orm::ActiveModelTrait; +use sea_orm::Set; +use tonic::{Request, Response, Status, Streaming}; +use futures::Stream; +use std::pin::Pin; +use tracing::{error, info, warn}; +use uuid::Uuid; + +use crate::db::entities::agent; +use crate::db::Database; + +/// Agent service implementation +#[derive(Debug)] +pub struct AgentServiceImpl { + db: Database, +} + +impl AgentServiceImpl { + pub fn new(db: Database) -> Self { + Self { db } + } + + async fn handle_registration( + &self, + request: RegistrationRequest, + ) -> Result { + info!("Agent registration request: hostname={}", request.hostname); + + // TODO: Validate token properly + // For now, create a new agent record + let agent_id = Uuid::new_v4(); + + let now = Utc::now(); + let agent = agent::ActiveModel { + id: Set(agent_id), + workspace_id: Set(Uuid::nil()), // TODO: Get from token + name: Set(request.hostname.clone()), + hostname: Set(request.hostname), + ip_address: Set(Some(request.ip_address)), + version: Set(Some(request.version)), + state: Set("online".to_string()), + deployment_mode: Set(Some(format!("{:?}", request.deployment_mode))), + last_seen_at: Set(Some(now.into())), + capabilities: Set(Some(serde_json::json!(request.capabilities))), + labels: Set(Some(serde_json::json!(request.labels))), + token_hash: Set(None), + created_at: Set(now.into()), + updated_at: Set(now.into()), + }; + + match agent.insert(self.db.conn()).await { + Ok(_) => { + info!("Agent registered successfully: {}", agent_id); + Ok(RegistrationResponse { + agent_id: agent_id.to_string(), + success: true, + error_message: String::new(), + heartbeat_interval_seconds: 30, + }) + } + Err(e) => { + error!("Failed to register agent: {}", e); + Err(Status::internal(format!("Database error: {}", e))) + } + } + } + + async fn handle_health_report(&self, report: HealthReport, agent_id: &str) -> Result<(), Status> { + let agent_uuid = Uuid::parse_str(agent_id) + .map_err(|_| Status::invalid_argument("Invalid agent ID"))?; + + // Update agent's last_seen_at + let now = Utc::now(); + let agent = agent::ActiveModel { + id: Set(agent_uuid), + last_seen_at: Set(Some(now.into())), + ..Default::default() + }; + + if let Err(e) = agent.update(self.db.conn()).await { + warn!("Failed to update agent last_seen: {}", e); + } + + // TODO: Store health report in time-series database + if let Some(nginx) = report.nginx { + info!( + "Health report from {}: nginx_running={}", + agent_id, nginx.is_running + ); + } + + Ok(()) + } +} + +#[tonic::async_trait] +impl AgentService for AgentServiceImpl { + type StreamStream = Pin> + Send>>; + + /// Bidirectional streaming RPC + async fn stream( + &self, + request: Request>, + ) -> Result, Status> { + let mut stream = request.into_inner(); + let db = self.db.clone(); + + let output_stream = async_stream::try_stream! { + while let Some(result) = stream.message().await? { + let msg = result; + + // Handle different message types via payload + if let Some(payload) = msg.payload { + use nxmesh_proto::agent_message::Payload; + match payload { + Payload::Registration(_reg) => { + info!("Received registration in stream from agent {}", msg.agent_id); + } + Payload::Health(health) => { + if let Err(e) = Self::handle_health_report(&Self { db: db.clone() }, health, &msg.agent_id).await { + warn!("Failed to handle health report: {}", e); + } + } + Payload::Metrics(_metrics) => { + info!("Received metrics from agent {}", msg.agent_id); + } + Payload::ConfigStatus(_status) => { + info!("Received config status from agent {}", msg.agent_id); + } + Payload::Logs(_logs) => { + info!("Received logs from agent {}", msg.agent_id); + } + Payload::Event(_event) => { + info!("Received event from agent {}", msg.agent_id); + } + } + } + + // Echo back a response + yield MasterMessage { + timestamp: Utc::now().timestamp(), + ..Default::default() + }; + } + }; + + Ok(Response::new(Box::pin(output_stream))) + } + + /// Report health status + async fn report_health( + &self, + request: Request, + ) -> Result, Status> { + // Extract agent ID from metadata before consuming request + let agent_id = request.metadata() + .get("x-agent-id") + .and_then(|v| v.to_str().ok()) + .unwrap_or("unknown") + .to_string(); + + let report = request.into_inner(); + + self.handle_health_report(report, &agent_id).await?; + + Ok(Response::new(Ack { + message_id: "health".to_string(), + success: true, + error_message: String::new(), + })) + } + + /// Report metrics + async fn report_metrics( + &self, + request: Request, + ) -> Result, Status> { + let metrics = request.into_inner(); + info!("Metrics batch received with {} metrics", metrics.metrics.len()); + + Ok(Response::new(Ack { + message_id: "metrics".to_string(), + success: true, + error_message: String::new(), + })) + } +} diff --git a/crates/nxmesh-master/src/grpc/interceptor.rs b/crates/nxmesh-master/src/grpc/interceptor.rs new file mode 100644 index 0000000..c025a2f --- /dev/null +++ b/crates/nxmesh-master/src/grpc/interceptor.rs @@ -0,0 +1,9 @@ +//! gRPC interceptors + +use tonic::{Request, Status}; + +/// Authentication interceptor +pub fn auth_interceptor(req: Request<()>) -> Result, Status> { + // TODO: Implement authentication + Ok(req) +} diff --git a/crates/nxmesh-master/src/grpc/mod.rs b/crates/nxmesh-master/src/grpc/mod.rs new file mode 100644 index 0000000..30fc2b2 --- /dev/null +++ b/crates/nxmesh-master/src/grpc/mod.rs @@ -0,0 +1,5 @@ +//! gRPC service + +pub mod agent_service; +pub mod interceptor; +pub mod server; diff --git a/crates/nxmesh-master/src/grpc/server.rs b/crates/nxmesh-master/src/grpc/server.rs new file mode 100644 index 0000000..8dc61f2 --- /dev/null +++ b/crates/nxmesh-master/src/grpc/server.rs @@ -0,0 +1,23 @@ +//! gRPC server + +use tonic::transport::Server; + +use crate::db::Database; + +use super::agent_service::AgentServiceImpl; + +/// Start the gRPC server +pub async fn start(bind_address: &str, db: Database) -> Result<(), Box> { + let addr = bind_address.parse()?; + + let agent_service = AgentServiceImpl::new(db); + + Server::builder() + .add_service(nxmesh_proto::agent_service_server::AgentServiceServer::new( + agent_service, + )) + .serve(addr) + .await?; + + Ok(()) +} diff --git a/crates/nxmesh-master/src/infrastructure/acme/mod.rs b/crates/nxmesh-master/src/infrastructure/acme/mod.rs new file mode 100644 index 0000000..97e4451 --- /dev/null +++ b/crates/nxmesh-master/src/infrastructure/acme/mod.rs @@ -0,0 +1,11 @@ +//! ACME/Let's Encrypt integration + +/// ACME client for certificate management +pub struct AcmeClient; + +impl AcmeClient { + /// Create a new ACME client + pub fn new() -> Self { + Self + } +} diff --git a/crates/nxmesh-master/src/infrastructure/mod.rs b/crates/nxmesh-master/src/infrastructure/mod.rs new file mode 100644 index 0000000..1393cca --- /dev/null +++ b/crates/nxmesh-master/src/infrastructure/mod.rs @@ -0,0 +1,5 @@ +//! External integrations + +pub mod acme; +pub mod notifier; +pub mod storage; diff --git a/crates/nxmesh-master/src/infrastructure/notifier/mod.rs b/crates/nxmesh-master/src/infrastructure/notifier/mod.rs new file mode 100644 index 0000000..5f41302 --- /dev/null +++ b/crates/nxmesh-master/src/infrastructure/notifier/mod.rs @@ -0,0 +1,11 @@ +//! Notification system + +/// Notifier for sending alerts +pub struct Notifier; + +impl Notifier { + /// Create a new notifier + pub fn new() -> Self { + Self + } +} diff --git a/crates/nxmesh-master/src/infrastructure/storage/mod.rs b/crates/nxmesh-master/src/infrastructure/storage/mod.rs new file mode 100644 index 0000000..1418448 --- /dev/null +++ b/crates/nxmesh-master/src/infrastructure/storage/mod.rs @@ -0,0 +1,11 @@ +//! Object storage integration + +/// Storage client +pub struct StorageClient; + +impl StorageClient { + /// Create a new storage client + pub fn new() -> Self { + Self + } +} diff --git a/crates/nxmesh-master/src/lib.rs b/crates/nxmesh-master/src/lib.rs new file mode 100644 index 0000000..435c078 --- /dev/null +++ b/crates/nxmesh-master/src/lib.rs @@ -0,0 +1,85 @@ +//! NxMesh Master Library +//! +//! This crate implements the control plane for NxMesh. + +pub mod api; +pub mod config; +pub mod db; +pub mod domain; +pub mod events; +pub mod grpc; +pub mod infrastructure; +pub mod services; + +use std::sync::Arc; + +use config::Settings; +use db::{Database, Migrator}; +use sea_orm_migration::MigratorTrait; +use tokio::net::TcpListener; +use tracing::{error, info}; + +/// Start the master server +pub async fn start(settings: Settings) -> Result<(), Box> { + let settings = Arc::new(settings); + + info!("Connecting to database..."); + let db = Database::connect(&settings.database.url).await.map_err(|e| { + error!("Failed to connect to database: {}", e); + e + })?; + + info!("Running database migrations..."); + Migrator::up(db.conn(), None).await.map_err(|e| { + error!("Failed to run migrations: {}", e); + e + })?; + info!("Database migrations complete"); + + // Create application state + let app_state = api::routes::AppState { + db: db.clone(), + settings: settings.clone(), + }; + + // Create router + let app = api::routes::create_router(app_state); + + // Start HTTP server + let http_addr = format!("{}:{}", settings.server.bind_address, settings.server.port); + info!("Starting HTTP server on {}", http_addr); + + let http_listener = TcpListener::bind(&http_addr).await?; + + // Start gRPC server in a separate task + let grpc_settings = settings.clone(); + let grpc_db = db.clone(); + let grpc_handle = tokio::spawn(async move { + let grpc_addr = format!("{}:{}", grpc_settings.grpc.bind_address, grpc_settings.grpc.port); + info!("Starting gRPC server on {}", grpc_addr); + + if let Err(e) = grpc::server::start(&grpc_addr, grpc_db).await { + error!("gRPC server error: {}", e); + } + }); + + // Run HTTP server + info!("Master server ready!"); + + tokio::select! { + result = axum::serve(http_listener, app) => { + if let Err(e) = result { + error!("HTTP server error: {}", e); + } + } + _ = tokio::signal::ctrl_c() => { + info!("Shutdown signal received"); + } + } + + // Cancel gRPC server + grpc_handle.abort(); + + info!("Master server shutdown complete"); + Ok(()) +} diff --git a/crates/nxmesh-master/src/main.rs b/crates/nxmesh-master/src/main.rs new file mode 100644 index 0000000..84d2bd4 --- /dev/null +++ b/crates/nxmesh-master/src/main.rs @@ -0,0 +1,35 @@ +//! NxMesh Master - Control Plane +//! +//! The master is the central control plane that manages agents and distributes +//! nginx configurations across the cluster. + +use tracing::{info, error}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + info!("Starting NxMesh Master v{}", env!("CARGO_PKG_VERSION")); + + // Load configuration + let config = match nxmesh_master::config::Settings::load() { + Ok(cfg) => cfg, + Err(e) => { + error!("Failed to load configuration: {}", e); + std::process::exit(1); + } + }; + + info!("Configuration loaded successfully"); + + // Start the master + if let Err(e) = nxmesh_master::start(config).await { + error!("Master error: {}", e); + std::process::exit(1); + } + + Ok(()) +} diff --git a/crates/nxmesh-master/src/services/agent_service.rs b/crates/nxmesh-master/src/services/agent_service.rs new file mode 100644 index 0000000..3fdadb4 --- /dev/null +++ b/crates/nxmesh-master/src/services/agent_service.rs @@ -0,0 +1,38 @@ +//! Agent service + +use uuid::Uuid; + +/// Agent service +pub struct AgentService; + +impl AgentService { + /// Create a new service instance + pub fn new() -> Self { + Self + } + + /// Register agent + pub async fn register( + &self, + workspace_id: Uuid, + name: &str, + hostname: &str, + ) -> Result { + // TODO: Implement + let id = Uuid::new_v4(); + tracing::info!( + "Registering agent: {} ({}) in workspace {} -> {}", + name, + hostname, + workspace_id, + id + ); + Ok(id) + } + + /// Generate registration token + pub async fn generate_token(&self, agent_id: Uuid) -> Result { + // TODO: Implement proper token generation + Ok(format!("nxmesh_agent_token_{}", agent_id)) + } +} diff --git a/crates/nxmesh-master/src/services/auth_service.rs b/crates/nxmesh-master/src/services/auth_service.rs new file mode 100644 index 0000000..c25c61a --- /dev/null +++ b/crates/nxmesh-master/src/services/auth_service.rs @@ -0,0 +1,25 @@ +//! Authentication service + +/// Auth service +pub struct AuthService; + +impl AuthService { + /// Create a new service instance + pub fn new() -> Self { + Self + } + + /// Authenticate user + pub async fn authenticate(&self, email: &str, password: &str) -> Result { + // TODO: Implement + tracing::info!("Authenticating user: {}", email); + Ok("jwt_token_placeholder".to_string()) + } + + /// Validate token + pub async fn validate_token(&self, token: &str) -> Result { + // TODO: Implement + tracing::info!("Validating token: {}", token); + Ok(true) + } +} diff --git a/crates/nxmesh-master/src/services/certificate_service.rs b/crates/nxmesh-master/src/services/certificate_service.rs new file mode 100644 index 0000000..33521ed --- /dev/null +++ b/crates/nxmesh-master/src/services/certificate_service.rs @@ -0,0 +1,26 @@ +//! Certificate service + +use uuid::Uuid; + +/// Certificate service +pub struct CertificateService; + +impl CertificateService { + /// Create a new service instance + pub fn new() -> Self { + Self + } + + /// Request certificate + pub async fn request(&self, workspace_id: Uuid, domain: &str) -> Result { + // TODO: Implement + let id = Uuid::new_v4(); + tracing::info!( + "Requesting certificate for {} in workspace {} -> {}", + domain, + workspace_id, + id + ); + Ok(id) + } +} diff --git a/crates/nxmesh-master/src/services/config_service.rs b/crates/nxmesh-master/src/services/config_service.rs new file mode 100644 index 0000000..af6e175 --- /dev/null +++ b/crates/nxmesh-master/src/services/config_service.rs @@ -0,0 +1,20 @@ +//! Config service + +use uuid::Uuid; + +/// Config service +pub struct ConfigService; + +impl ConfigService { + /// Create a new service instance + pub fn new() -> Self { + Self + } + + /// Apply configuration + pub async fn apply(&self, workspace_id: Uuid) -> Result<(), String> { + // TODO: Implement + tracing::info!("Applying configuration for workspace {}", workspace_id); + Ok(()) + } +} diff --git a/crates/nxmesh-master/src/services/mod.rs b/crates/nxmesh-master/src/services/mod.rs new file mode 100644 index 0000000..cf357a4 --- /dev/null +++ b/crates/nxmesh-master/src/services/mod.rs @@ -0,0 +1,8 @@ +//! Business logic services + +pub mod agent_service; +pub mod auth_service; +pub mod certificate_service; +pub mod config_service; +pub mod organization_service; +pub mod workspace_service; diff --git a/crates/nxmesh-master/src/services/organization_service.rs b/crates/nxmesh-master/src/services/organization_service.rs new file mode 100644 index 0000000..3be9205 --- /dev/null +++ b/crates/nxmesh-master/src/services/organization_service.rs @@ -0,0 +1,21 @@ +//! Organization service + +use uuid::Uuid; + +/// Organization service +pub struct OrganizationService; + +impl OrganizationService { + /// Create a new service instance + pub fn new() -> Self { + Self + } + + /// Create organization + pub async fn create(&self, name: &str, slug: &str) -> Result { + // TODO: Implement + let id = Uuid::new_v4(); + tracing::info!("Creating organization: {} ({}) -> {}", name, slug, id); + Ok(id) + } +} diff --git a/crates/nxmesh-master/src/services/workspace_service.rs b/crates/nxmesh-master/src/services/workspace_service.rs new file mode 100644 index 0000000..8cdadaa --- /dev/null +++ b/crates/nxmesh-master/src/services/workspace_service.rs @@ -0,0 +1,32 @@ +//! Workspace service + +use uuid::Uuid; + +/// Workspace service +pub struct WorkspaceService; + +impl WorkspaceService { + /// Create a new service instance + pub fn new() -> Self { + Self + } + + /// Create workspace + pub async fn create( + &self, + org_id: Uuid, + name: &str, + slug: &str, + ) -> Result { + // TODO: Implement + let id = Uuid::new_v4(); + tracing::info!( + "Creating workspace: {} ({}) in org {} -> {}", + name, + slug, + org_id, + id + ); + Ok(id) + } +}