Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ Thumbs.db
/target

# Environment
.env
.env
oidc.toml
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
help:
@echo "NetVisor Development Commands"
@echo ""
@echo " make fresh-db - Clean and set up a new database"
@echo " make setup-db - Set up database"
@echo " make clean-db - Clean up database"
@echo " make clean-daemon - Remove daemon config file"
Expand All @@ -22,6 +23,10 @@ help:
@echo " make install-dev-mac - Install development dependencies on macOS"
@echo " make install-dev-linux - Install development dependencies on Linux"

fresh-db:
make clean-db
make setup-db

setup-db:
@echo "Setting up PostgreSQL..."
@docker run -d \
Expand Down Expand Up @@ -97,6 +102,9 @@ lint:
@echo "Linting UI..."
cd ui && npm run lint && npm run format -- --check && npm run check

stripe-webhook:
stripe listen --forward-to http://localhost:60072/api/billing/webhooks

clean:
make clean-db
docker compose down -v
Expand Down
35 changes: 35 additions & 0 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ config = "0.14"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
dotenv = "0.15"
figment = { version = "0.10", features = ["json", "env"] }
figment = { version = "0.10", features = ["json", "env", "toml"] }

# === CLI ===
clap = { version = "4.0", features = ["derive"] }
Expand Down Expand Up @@ -158,6 +158,8 @@ serde_with = "3.15.1"
lettre = { version = "0.11.19", default-features = false, features = ["smtp-transport", "builder", "tokio1", "tokio1-rustls", "ring", "webpki-roots"] }
html2text = "0.16.4"
json_value_merge = "2.0.1"
bad_email = "0.1.1"
axum-client-ip = "1.1.3"

# === Platform-specific Dependencies ===
[target.'cfg(target_os = "linux")'.dependencies]
Expand Down
10 changes: 10 additions & 0 deletions backend/migrations/20251128035448_org-onboarding-status.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Add migration script here
ALTER TABLE organizations ADD COLUMN onboarding JSONB DEFAULT '[]';

-- Set onboarding for existing organizations where is_onboarded is true
UPDATE organizations
SET onboarding = '["OrgCreated", "OnboardingModalCompleted"]'::JSONB
WHERE is_onboarded = true;

-- Drop the old is_onboarded column
ALTER TABLE organizations DROP COLUMN is_onboarded;
129 changes: 13 additions & 116 deletions backend/src/bin/server.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use std::{net::SocketAddr, sync::Arc, time::Duration};
use std::{net::SocketAddr, str::FromStr, sync::Arc, time::Duration};

use axum::{
Extension, Router,
http::{HeaderValue, Method},
};
use axum_client_ip::ClientIpSource;
use clap::Parser;
use netvisor::server::{
auth::middleware::AuthenticatedEntity,
billing::types::base::{BillingPlan, BillingRate, PlanConfig},
config::{AppState, CliArgs, ServerConfig},
config::{AppState, ServerCli, ServerConfig},
organizations::r#impl::base::{Organization, OrganizationBase},
shared::{
handlers::{cache::AppCache, factory::create_router},
Expand All @@ -27,124 +28,17 @@ use tower_http::{
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[derive(Parser)]
#[command(name = "netvisor-server")]
#[command(about = "NetVisor server")]
struct Cli {
/// Override server port
#[arg(long)]
server_port: Option<u16>,

/// Override log level
#[arg(long)]
log_level: Option<String>,

/// Override rust system log level
#[arg(long)]
rust_log: Option<String>,

/// Override database path
#[arg(long)]
database_url: Option<String>,

/// Override integrated daemon url
#[arg(long)]
integrated_daemon_url: Option<String>,

/// Use secure session cookies (if serving UI behind HTTPS)
#[arg(long)]
use_secure_session_cookies: Option<bool>,

/// Enable or disable registration flow
#[arg(long)]
disable_registration: bool,

/// OIDC client ID
#[arg(long)]
oidc_client_id: Option<String>,

/// OIDC client secret
#[arg(long)]
oidc_client_secret: Option<String>,

/// OIDC issuer url
#[arg(long)]
oidc_issuer_url: Option<String>,

/// OIDC issuer url
#[arg(long)]
oidc_provider_name: Option<String>,

/// OIDC redirect url
#[arg(long)]
oidc_redirect_url: Option<String>,

/// OIDC redirect url
#[arg(long)]
stripe_secret: Option<String>,

/// OIDC redirect url
#[arg(long)]
stripe_webhook_secret: Option<String>,

#[arg(long)]
smtp_username: Option<String>,

#[arg(long)]
smtp_password: Option<String>,

/// Email used as to/from in emails send by NetVisor
#[arg(long)]
smtp_email: Option<String>,

#[arg(long)]
smtp_relay: Option<String>,

#[arg(long)]
smtp_port: Option<String>,

/// Server URL used in features like password reset and invite links
#[arg(long)]
public_url: Option<String>,
}

impl From<Cli> for CliArgs {
fn from(cli: Cli) -> Self {
Self {
server_port: cli.server_port,
log_level: cli.log_level,
rust_log: cli.rust_log,
database_url: cli.database_url,
integrated_daemon_url: cli.integrated_daemon_url,
use_secure_session_cookies: cli.use_secure_session_cookies,
disable_registration: cli.disable_registration,
oidc_client_id: cli.oidc_client_id,
oidc_client_secret: cli.oidc_client_secret,
oidc_issuer_url: cli.oidc_issuer_url,
oidc_provider_name: cli.oidc_provider_name,
oidc_redirect_url: cli.oidc_redirect_url,
stripe_secret: cli.stripe_secret,
stripe_webhook_secret: cli.stripe_webhook_secret,
smtp_email: cli.smtp_email,
smtp_password: cli.smtp_password,
smtp_relay: cli.smtp_relay,
smtp_username: cli.smtp_username,
public_url: cli.public_url,
}
}
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let _ = dotenv::dotenv();

let cli = Cli::parse();
let cli_args = CliArgs::from(cli);
let cli = ServerCli::parse();

// Load configuration using figment
let config = ServerConfig::load(cli_args)?;
let config = ServerConfig::load(cli)?;
let listen_addr = format!("0.0.0.0:{}", &config.server_port);
let web_external_path = config.web_external_path.clone();
let client_ip_source = config.client_ip_source.clone();

// Initialize tracing
tracing_subscriber::registry()
Expand Down Expand Up @@ -247,6 +141,10 @@ async fn main() -> anyhow::Result<()> {
CorsLayer::permissive()
};

let client_ip_source = client_ip_source
.map(|s| ClientIpSource::from_str(&s))
.unwrap_or(Ok(ClientIpSource::ConnectInfo))?;

let cache_headers = SetResponseHeaderLayer::if_not_present(
header::CACHE_CONTROL,
HeaderValue::from_static("no-store, no-cache, must-revalidate, private"),
Expand All @@ -260,7 +158,8 @@ async fn main() -> anyhow::Result<()> {
.layer(TraceLayer::new_for_http())
.layer(cors)
.layer(Extension(app_cache))
.layer(cache_headers),
.layer(cache_headers)
.layer(client_ip_source.into_extension()),
);

let listener = tokio::net::TcpListener::bind(&listen_addr).await?;
Expand Down Expand Up @@ -339,7 +238,7 @@ async fn main() -> anyhow::Result<()> {
plan: None,
plan_status: None,
name: "My Organization".to_string(),
is_onboarded: false,
onboarding: vec![],
}),
AuthenticatedEntity::System,
)
Expand All @@ -351,8 +250,6 @@ async fn main() -> anyhow::Result<()> {
AuthenticatedEntity::System,
)
.await?;
} else {
tracing::debug!("Server already has data, skipping seed data");
}

tokio::signal::ctrl_c().await?;
Expand Down
37 changes: 28 additions & 9 deletions backend/src/daemon/runtime/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,20 @@ impl DaemonRuntimeService {
let error_msg = api_response
.error
.unwrap_or_else(|| "Unknown error".to_string());
tracing::warn!(
daemon_id = %daemon_id,
err = %error_msg,
"Failed to check for work"
);

if error_msg.contains("not found") {
tracing::error!(
daemon_id = %daemon_id,
error = %error_msg,
"Failed to check for work - the Daemon ID present in the config on this host could not be found on the server. Please remove the config and install a new daemon."
);
} else {
tracing::error!(
daemon_id = %daemon_id,
err = %error_msg,
"Failed to check for work"
);
}
} else if let Some((payload, cancel_current_session)) = api_response.data {
if !cancel_current_session && payload.is_none() {
tracing::info!(
Expand Down Expand Up @@ -162,10 +171,20 @@ impl DaemonRuntimeService {
let error_msg = api_response
.error
.unwrap_or_else(|| "Unknown error".to_string());
tracing::error!(
error = %error_msg,
"Heartbeat failed - check network connectivity"
);

if error_msg.contains("not found") {
tracing::error!(
error = %error_msg,
daemon_id = %daemon_id,
"Heartbeat failed - the Daemon ID present in the config on this host could not be found on the server. Please remove the config and install a new daemon."
);
} else {
tracing::error!(
error = %error_msg,
daemon_id = %daemon_id,
"Heartbeat failed - check network connectivity"
);
}
}

if let Err(e) = self.config_store.update_heartbeat().await {
Expand Down
Loading