feat: stub master file structures

This commit is contained in:
GW_MC
2026-03-03 04:31:46 +00:00
parent 7d9285ba44
commit 8f213c19c8
56 changed files with 2494 additions and 0 deletions

View File

@@ -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

View File

@@ -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<String>,
pub role: String,
pub exp: usize,
pub iat: usize,
}
/// Auth state
#[derive(Clone)]
pub struct AuthState {
settings: Arc<Settings>,
}
impl AuthState {
pub fn new(settings: Arc<Settings>) -> 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<String, jsonwebtoken::errors::Error> {
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<Claims, jsonwebtoken::errors::Error> {
decode::<Claims>(
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<AuthState>,
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::<Claims>()
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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<Settings>,
}
/// 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<serde_json::Value> {
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,
}

View File

@@ -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<AgentResponse>),
)
)]
pub async fn list(Path(ws_id): Path<Uuid>) -> Json<Vec<AgentResponse>> {
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<Uuid>) -> Json<AgentResponse> {
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<Uuid>,
Json(req): Json<CreateAgentTokenRequest>,
) -> Json<CreateAgentTokenResponse> {
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<Uuid>) -> &'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<Uuid>) -> &'static str {
r#"{"success": true}"#
}

View File

@@ -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<CertificateResponse>),
)
)]
pub async fn list(Path(ws_id): Path<Uuid>) -> Json<Vec<CertificateResponse>> {
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<Uuid>) -> Json<CertificateResponse> {
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<Uuid>,
Json(req): Json<CreateCertificateRequest>,
) -> Json<CertificateResponse> {
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<Uuid>) -> &'static str {
"{}"
}

View File

@@ -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<Uuid>) -> Json<MetricsResponse> {
// TODO: Implement
Json(MetricsResponse {
requests_total: 0,
requests_per_second: 0.0,
})
}
/// Get agent metrics
pub async fn get_agent(Path(id): Path<Uuid>) -> Json<MetricsResponse> {
// TODO: Implement
Json(MetricsResponse {
requests_total: 0,
requests_per_second: 0.0,
})
}

View File

@@ -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;

View File

@@ -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<OrganizationResponse>),
)
)]
pub async fn list() -> Json<Vec<OrganizationResponse>> {
// 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<Uuid>) -> Json<OrganizationResponse> {
// 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<CreateOrganizationRequest>) -> Json<OrganizationResponse> {
// 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<Uuid>,
Json(req): Json<CreateOrganizationRequest>,
) -> Json<OrganizationResponse> {
// 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<Uuid>) -> &'static str {
// TODO: Implement
"{}"
}

View File

@@ -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<UpstreamResponse>),
)
)]
pub async fn list(Path(ws_id): Path<Uuid>) -> Json<Vec<UpstreamResponse>> {
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<Uuid>) -> Json<UpstreamResponse> {
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<Uuid>,
Json(req): Json<CreateUpstreamRequest>,
) -> Json<UpstreamResponse> {
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<Uuid>,
Json(req): Json<CreateUpstreamRequest>,
) -> Json<UpstreamResponse> {
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<Uuid>) -> &'static str {
"{}"
}

View File

@@ -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<VirtualHostResponse>),
)
)]
pub async fn list(Path(ws_id): Path<Uuid>) -> Json<Vec<VirtualHostResponse>> {
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<Uuid>) -> Json<VirtualHostResponse> {
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<Uuid>,
Json(req): Json<CreateVirtualHostRequest>,
) -> Json<VirtualHostResponse> {
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<Uuid>,
Json(req): Json<CreateVirtualHostRequest>,
) -> Json<VirtualHostResponse> {
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<Uuid>) -> &'static str {
"{}"
}

View File

@@ -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<WorkspaceResponse>),
)
)]
pub async fn list(Path(org_id): Path<Uuid>) -> Json<Vec<WorkspaceResponse>> {
// 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<Uuid>) -> Json<WorkspaceResponse> {
// 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<Uuid>,
Json(req): Json<CreateWorkspaceRequest>,
) -> Json<WorkspaceResponse> {
// 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<Uuid>,
Json(req): Json<CreateWorkspaceRequest>,
) -> Json<WorkspaceResponse> {
// 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<Uuid>) -> &'static str {
// TODO: Implement
"{}"
}

View File

@@ -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
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,5 @@
//! Configuration management
pub mod settings;
pub use settings::Settings;

View File

@@ -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<Self, ConfigError> {
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()
}
}

View File

@@ -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<Self, DbErr> {
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
}
}

View File

@@ -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<String>,
pub version: Option<String>,
pub state: String,
pub deployment_mode: Option<String>,
pub last_seen_at: Option<DateTimeWithTimeZone>,
pub capabilities: Option<Json>,
pub labels: Option<Json>,
pub token_hash: Option<String>,
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<super::workspaces::Entity> for Entity {
fn to() -> RelationDef {
Relation::Workspaces.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -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<String>,
pub status: Option<String>,
pub issued_at: Option<DateTimeWithTimeZone>,
pub expires_at: Option<DateTimeWithTimeZone>,
pub auto_renew: bool,
#[sea_orm(column_type = "Text", nullable)]
pub certificate_pem: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
pub private_key_pem: Option<String>,
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<super::workspaces::Entity> for Entity {
fn to() -> RelationDef {
Relation::Workspaces.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -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;

View File

@@ -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<Json>,
}
#[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<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
}
impl Related<super::workspaces::Entity> for Entity {
fn to() -> RelationDef {
Relation::Workspaces.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -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;

View File

@@ -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<Json>,
pub health_check: Option<Json>,
pub keepalive_connections: Option<i32>,
pub keepalive_timeout: Option<i32>,
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<super::workspaces::Entity> for Entity {
fn to() -> RelationDef {
Relation::Workspaces.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -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<String>,
pub role: String,
pub organization_id: Option<Uuid>,
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<super::organizations::Entity> for Entity {
fn to() -> RelationDef {
Relation::Organizations.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -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<Uuid>,
pub locations: Option<Json>,
pub http2_enabled: bool,
pub http3_enabled: bool,
pub gzip_enabled: bool,
pub target_agents: Option<Json>,
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<super::workspaces::Entity> for Entity {
fn to() -> RelationDef {
Relation::Workspaces.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -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<super::agents::Entity> for Entity {
fn to() -> RelationDef {
Relation::Agents.def()
}
}
impl Related<super::certificates::Entity> for Entity {
fn to() -> RelationDef {
Relation::Certificates.def()
}
}
impl Related<super::organizations::Entity> for Entity {
fn to() -> RelationDef {
Relation::Organizations.def()
}
}
impl Related<super::upstreams::Entity> for Entity {
fn to() -> RelationDef {
Relation::Upstreams.def()
}
}
impl Related<super::virtual_hosts::Entity> for Entity {
fn to() -> RelationDef {
Relation::VirtualHosts.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,8 @@
//! Database layer
pub mod connection;
pub mod entities;
pub mod repositories;
pub use connection::Database;
pub use migration::Migrator;

View File

@@ -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
}
}

View File

@@ -0,0 +1,5 @@
//! Repository implementations
pub mod agent_repository;
pub mod organization_repository;
pub mod workspace_repository;

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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<String>,
hostname: impl Into<String>,
) -> Self {
Self {
id: Uuid::new_v4(),
workspace_id,
name: name.into(),
hostname: hostname.into(),
}
}
}

View File

@@ -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,
}

View File

@@ -0,0 +1,5 @@
//! Domain entities
pub mod agent;
pub mod config;
pub mod organization;

View File

@@ -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<String>, slug: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
name: name.into(),
slug: slug.into(),
}
}
}

View File

@@ -0,0 +1,40 @@
//! Event bus implementation
use tokio::sync::broadcast;
/// Event bus for internal communication
pub struct EventBus {
sender: broadcast::Sender<Event>,
}
/// 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<Event> {
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()
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,4 @@
//! Event bus
pub mod bus;
pub mod handlers;

View File

@@ -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<RegistrationResponse, Status> {
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<Box<dyn Stream<Item = Result<MasterMessage, Status>> + Send>>;
/// Bidirectional streaming RPC
async fn stream(
&self,
request: Request<Streaming<AgentMessage>>,
) -> Result<Response<Self::StreamStream>, 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<HealthReport>,
) -> Result<Response<Ack>, 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<MetricsBatch>,
) -> Result<Response<Ack>, 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(),
}))
}
}

View File

@@ -0,0 +1,9 @@
//! gRPC interceptors
use tonic::{Request, Status};
/// Authentication interceptor
pub fn auth_interceptor(req: Request<()>) -> Result<Request<()>, Status> {
// TODO: Implement authentication
Ok(req)
}

View File

@@ -0,0 +1,5 @@
//! gRPC service
pub mod agent_service;
pub mod interceptor;
pub mod server;

View File

@@ -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<dyn std::error::Error>> {
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(())
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,5 @@
//! External integrations
pub mod acme;
pub mod notifier;
pub mod storage;

View File

@@ -0,0 +1,11 @@
//! Notification system
/// Notifier for sending alerts
pub struct Notifier;
impl Notifier {
/// Create a new notifier
pub fn new() -> Self {
Self
}
}

View File

@@ -0,0 +1,11 @@
//! Object storage integration
/// Storage client
pub struct StorageClient;
impl StorageClient {
/// Create a new storage client
pub fn new() -> Self {
Self
}
}

View File

@@ -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<dyn std::error::Error>> {
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(())
}

View File

@@ -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<dyn std::error::Error>> {
// 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(())
}

View File

@@ -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<Uuid, String> {
// 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<String, String> {
// TODO: Implement proper token generation
Ok(format!("nxmesh_agent_token_{}", agent_id))
}
}

View File

@@ -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<String, String> {
// TODO: Implement
tracing::info!("Authenticating user: {}", email);
Ok("jwt_token_placeholder".to_string())
}
/// Validate token
pub async fn validate_token(&self, token: &str) -> Result<bool, String> {
// TODO: Implement
tracing::info!("Validating token: {}", token);
Ok(true)
}
}

View File

@@ -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<Uuid, String> {
// TODO: Implement
let id = Uuid::new_v4();
tracing::info!(
"Requesting certificate for {} in workspace {} -> {}",
domain,
workspace_id,
id
);
Ok(id)
}
}

View File

@@ -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(())
}
}

View File

@@ -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;

View File

@@ -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<Uuid, String> {
// TODO: Implement
let id = Uuid::new_v4();
tracing::info!("Creating organization: {} ({}) -> {}", name, slug, id);
Ok(id)
}
}

View File

@@ -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<Uuid, String> {
// TODO: Implement
let id = Uuid::new_v4();
tracing::info!(
"Creating workspace: {} ({}) in org {} -> {}",
name,
slug,
org_id,
id
);
Ok(id)
}
}