feat: stub master file structures
This commit is contained in:
79
crates/nxmesh-master/Cargo.toml
Normal file
79
crates/nxmesh-master/Cargo.toml
Normal 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
|
||||||
122
crates/nxmesh-master/src/api/middleware/auth.rs
Normal file
122
crates/nxmesh-master/src/api/middleware/auth.rs
Normal 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>()
|
||||||
|
}
|
||||||
22
crates/nxmesh-master/src/api/middleware/mod.rs
Normal file
22
crates/nxmesh-master/src/api/middleware/mod.rs
Normal 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
|
||||||
|
}
|
||||||
96
crates/nxmesh-master/src/api/mod.rs
Normal file
96
crates/nxmesh-master/src/api/mod.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
114
crates/nxmesh-master/src/api/routes.rs
Normal file
114
crates/nxmesh-master/src/api/routes.rs
Normal 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,
|
||||||
|
}
|
||||||
133
crates/nxmesh-master/src/api/v1/agents.rs
Normal file
133
crates/nxmesh-master/src/api/v1/agents.rs
Normal 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}"#
|
||||||
|
}
|
||||||
106
crates/nxmesh-master/src/api/v1/certificates.rs
Normal file
106
crates/nxmesh-master/src/api/v1/certificates.rs
Normal 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 {
|
||||||
|
"{}"
|
||||||
|
}
|
||||||
33
crates/nxmesh-master/src/api/v1/metrics.rs
Normal file
33
crates/nxmesh-master/src/api/v1/metrics.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
9
crates/nxmesh-master/src/api/v1/mod.rs
Normal file
9
crates/nxmesh-master/src/api/v1/mod.rs
Normal 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;
|
||||||
129
crates/nxmesh-master/src/api/v1/organizations.rs
Normal file
129
crates/nxmesh-master/src/api/v1/organizations.rs
Normal 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
|
||||||
|
"{}"
|
||||||
|
}
|
||||||
132
crates/nxmesh-master/src/api/v1/upstreams.rs
Normal file
132
crates/nxmesh-master/src/api/v1/upstreams.rs
Normal 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 {
|
||||||
|
"{}"
|
||||||
|
}
|
||||||
137
crates/nxmesh-master/src/api/v1/virtual_hosts.rs
Normal file
137
crates/nxmesh-master/src/api/v1/virtual_hosts.rs
Normal 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 {
|
||||||
|
"{}"
|
||||||
|
}
|
||||||
140
crates/nxmesh-master/src/api/v1/workspaces.rs
Normal file
140
crates/nxmesh-master/src/api/v1/workspaces.rs
Normal 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
|
||||||
|
"{}"
|
||||||
|
}
|
||||||
22
crates/nxmesh-master/src/api/websocket.rs
Normal file
22
crates/nxmesh-master/src/api/websocket.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
9
crates/nxmesh-master/src/bin/gen-openapi.rs
Normal file
9
crates/nxmesh-master/src/bin/gen-openapi.rs
Normal 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);
|
||||||
|
}
|
||||||
5
crates/nxmesh-master/src/config/mod.rs
Normal file
5
crates/nxmesh-master/src/config/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//! Configuration management
|
||||||
|
|
||||||
|
pub mod settings;
|
||||||
|
|
||||||
|
pub use settings::Settings;
|
||||||
79
crates/nxmesh-master/src/config/settings.rs
Normal file
79
crates/nxmesh-master/src/config/settings.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
30
crates/nxmesh-master/src/db/connection.rs
Normal file
30
crates/nxmesh-master/src/db/connection.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
44
crates/nxmesh-master/src/db/entities/agents.rs
Normal file
44
crates/nxmesh-master/src/db/entities/agents.rs
Normal 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 {}
|
||||||
45
crates/nxmesh-master/src/db/entities/certificates.rs
Normal file
45
crates/nxmesh-master/src/db/entities/certificates.rs
Normal 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 {}
|
||||||
11
crates/nxmesh-master/src/db/entities/mod.rs
Normal file
11
crates/nxmesh-master/src/db/entities/mod.rs
Normal 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;
|
||||||
39
crates/nxmesh-master/src/db/entities/organizations.rs
Normal file
39
crates/nxmesh-master/src/db/entities/organizations.rs
Normal 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 {}
|
||||||
9
crates/nxmesh-master/src/db/entities/prelude.rs
Normal file
9
crates/nxmesh-master/src/db/entities/prelude.rs
Normal 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;
|
||||||
40
crates/nxmesh-master/src/db/entities/upstreams.rs
Normal file
40
crates/nxmesh-master/src/db/entities/upstreams.rs
Normal 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 {}
|
||||||
39
crates/nxmesh-master/src/db/entities/users.rs
Normal file
39
crates/nxmesh-master/src/db/entities/users.rs
Normal 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 {}
|
||||||
44
crates/nxmesh-master/src/db/entities/virtual_hosts.rs
Normal file
44
crates/nxmesh-master/src/db/entities/virtual_hosts.rs
Normal 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 {}
|
||||||
68
crates/nxmesh-master/src/db/entities/workspaces.rs
Normal file
68
crates/nxmesh-master/src/db/entities/workspaces.rs
Normal 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 {}
|
||||||
8
crates/nxmesh-master/src/db/mod.rs
Normal file
8
crates/nxmesh-master/src/db/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//! Database layer
|
||||||
|
|
||||||
|
pub mod connection;
|
||||||
|
pub mod entities;
|
||||||
|
pub mod repositories;
|
||||||
|
|
||||||
|
pub use connection::Database;
|
||||||
|
pub use migration::Migrator;
|
||||||
19
crates/nxmesh-master/src/db/repositories/agent_repository.rs
Normal file
19
crates/nxmesh-master/src/db/repositories/agent_repository.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
5
crates/nxmesh-master/src/db/repositories/mod.rs
Normal file
5
crates/nxmesh-master/src/db/repositories/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//! Repository implementations
|
||||||
|
|
||||||
|
pub mod agent_repository;
|
||||||
|
pub mod organization_repository;
|
||||||
|
pub mod workspace_repository;
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
28
crates/nxmesh-master/src/domain/agent.rs
Normal file
28
crates/nxmesh-master/src/domain/agent.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
crates/nxmesh-master/src/domain/config.rs
Normal file
11
crates/nxmesh-master/src/domain/config.rs
Normal 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,
|
||||||
|
}
|
||||||
5
crates/nxmesh-master/src/domain/mod.rs
Normal file
5
crates/nxmesh-master/src/domain/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//! Domain entities
|
||||||
|
|
||||||
|
pub mod agent;
|
||||||
|
pub mod config;
|
||||||
|
pub mod organization;
|
||||||
22
crates/nxmesh-master/src/domain/organization.rs
Normal file
22
crates/nxmesh-master/src/domain/organization.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
crates/nxmesh-master/src/events/bus.rs
Normal file
40
crates/nxmesh-master/src/events/bus.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
19
crates/nxmesh-master/src/events/handlers.rs
Normal file
19
crates/nxmesh-master/src/events/handlers.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
crates/nxmesh-master/src/events/mod.rs
Normal file
4
crates/nxmesh-master/src/events/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
//! Event bus
|
||||||
|
|
||||||
|
pub mod bus;
|
||||||
|
pub mod handlers;
|
||||||
194
crates/nxmesh-master/src/grpc/agent_service.rs
Normal file
194
crates/nxmesh-master/src/grpc/agent_service.rs
Normal 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(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
9
crates/nxmesh-master/src/grpc/interceptor.rs
Normal file
9
crates/nxmesh-master/src/grpc/interceptor.rs
Normal 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)
|
||||||
|
}
|
||||||
5
crates/nxmesh-master/src/grpc/mod.rs
Normal file
5
crates/nxmesh-master/src/grpc/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//! gRPC service
|
||||||
|
|
||||||
|
pub mod agent_service;
|
||||||
|
pub mod interceptor;
|
||||||
|
pub mod server;
|
||||||
23
crates/nxmesh-master/src/grpc/server.rs
Normal file
23
crates/nxmesh-master/src/grpc/server.rs
Normal 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(())
|
||||||
|
}
|
||||||
11
crates/nxmesh-master/src/infrastructure/acme/mod.rs
Normal file
11
crates/nxmesh-master/src/infrastructure/acme/mod.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
5
crates/nxmesh-master/src/infrastructure/mod.rs
Normal file
5
crates/nxmesh-master/src/infrastructure/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//! External integrations
|
||||||
|
|
||||||
|
pub mod acme;
|
||||||
|
pub mod notifier;
|
||||||
|
pub mod storage;
|
||||||
11
crates/nxmesh-master/src/infrastructure/notifier/mod.rs
Normal file
11
crates/nxmesh-master/src/infrastructure/notifier/mod.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
11
crates/nxmesh-master/src/infrastructure/storage/mod.rs
Normal file
11
crates/nxmesh-master/src/infrastructure/storage/mod.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
85
crates/nxmesh-master/src/lib.rs
Normal file
85
crates/nxmesh-master/src/lib.rs
Normal 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(())
|
||||||
|
}
|
||||||
35
crates/nxmesh-master/src/main.rs
Normal file
35
crates/nxmesh-master/src/main.rs
Normal 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(())
|
||||||
|
}
|
||||||
38
crates/nxmesh-master/src/services/agent_service.rs
Normal file
38
crates/nxmesh-master/src/services/agent_service.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
25
crates/nxmesh-master/src/services/auth_service.rs
Normal file
25
crates/nxmesh-master/src/services/auth_service.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
crates/nxmesh-master/src/services/certificate_service.rs
Normal file
26
crates/nxmesh-master/src/services/certificate_service.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
crates/nxmesh-master/src/services/config_service.rs
Normal file
20
crates/nxmesh-master/src/services/config_service.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
8
crates/nxmesh-master/src/services/mod.rs
Normal file
8
crates/nxmesh-master/src/services/mod.rs
Normal 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;
|
||||||
21
crates/nxmesh-master/src/services/organization_service.rs
Normal file
21
crates/nxmesh-master/src/services/organization_service.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
32
crates/nxmesh-master/src/services/workspace_service.rs
Normal file
32
crates/nxmesh-master/src/services/workspace_service.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user