Skip to content
Merged

Dev #181

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
245423a
Implement SABnzbd service definition
WildEchoWanderer Nov 18, 2025
981b341
feat: basic CRUD operations
mayanayza Nov 18, 2025
f90c1bc
Update backend/src/server/services/definitions/sabnzbd.rs
vhsdream Nov 19, 2025
7d82c9e
Update backend/src/server/services/definitions/sabnzbd.rs
vhsdream Nov 19, 2025
0a74e90
Update backend/src/server/services/definitions/sabnzbd.rs
vhsdream Nov 19, 2025
8b3e751
Wiring frontend to topology backend
mayanayza Nov 19, 2025
dc3024d
Wiring state management to frontend
mayanayza Nov 19, 2025
dd3ab17
feat: topology state management done
mayanayza Nov 19, 2025
bf0b9da
Only use http endpoint for NextCloud service detection
vhsdream Nov 20, 2025
152562d
Change SABnzbd port to default 8080
vhsdream Nov 20, 2025
b5aa2d3
Add sabnzbd module to definitions
vhsdream Nov 20, 2025
339e3d2
feat: topology saving working end to end
mayanayza Nov 21, 2025
c16667e
Merge pull request #171 from vhsdream/feat/improve-detection
mayanayza Nov 21, 2025
dd2cdc5
Merge pull request #158 from WildEchoWanderer/main
mayanayza Nov 21, 2025
17138fe
Merge pull request #173 from mayanayza/feat/save-topology
mayanayza Nov 21, 2025
4cca5cf
fix: missing service defs and more tests
mayanayza Nov 21, 2025
55dc66a
feat: hide click to copy button in non secure contexts
mayanayza Nov 18, 2025
c517997
Update release workflow to use target commitish
mayanayza Nov 18, 2025
9760f34
Fixed login page refresh and github stars load failures
mayanayza Nov 19, 2025
6ca7fb8
fix: better determination of server URL in frontend to fix reverse pr…
mayanayza Nov 18, 2025
3c9dccf
fix: better determination of server URL in frontend to fix reverse pr…
mayanayza Nov 18, 2025
5b28ebd
chore: update test fixtures for release v0.10.1
github-actions[bot] Nov 19, 2025
e50310e
fix: remove http from legacy server target URL parsing
mayanayza Nov 19, 2025
4e22d8f
chore: update test fixtures for release v0.10.1
github-actions[bot] Nov 19, 2025
9029f6e
fix: brower cache headers expire/removed
mayanayza Nov 19, 2025
affc8e5
feat: bulk delete
mayanayza Nov 21, 2025
87b46a9
Merge pull request #175 from mayanayza/fix/missing-service-defs
mayanayza Nov 21, 2025
9176260
feat: savable topology works end to end
mayanayza Nov 21, 2025
2802648
Merge branch 'dev' into feat/bulk-actions
mayanayza Nov 21, 2025
0ffd359
Merge pull request #176 from mayanayza/feat/bulk-actions
mayanayza Nov 21, 2025
d7fe490
Merge pull request #177 from mayanayza/feat/save-topology
mayanayza Nov 21, 2025
2f50168
add connection info to server
mayanayza Nov 22, 2025
f56cbeb
Merge pull request #179 from mayanayza/feat/save-topology
mayanayza Nov 22, 2025
cd598d4
chore: update test fixtures for release v0.10.2
github-actions[bot] Nov 22, 2025
478978e
fix linting
mayanayza Nov 22, 2025
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
25 changes: 25 additions & 0 deletions backend/Cargo.lock

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

2 changes: 1 addition & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ argon2 = "0.5.3"
password-hash = "0.5.0"
lazy_static = "1.5.0"
rand_core = "0.9.3"
axum-extra = {version = "0.10.3", features = ["cookie"]}
axum-extra = { version = "0.10.3", features = ["cookie", "typed-header"] }
time = "0.3.44"
tower-sessions-sqlx-store = { version = "0.15", features = ["postgres"] }
secrecy = "0.10.3"
Expand Down
36 changes: 36 additions & 0 deletions backend/migrations/20251118225043_save-topology.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
CREATE TABLE topologies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
network_id UUID NOT NULL REFERENCES networks(id) ON DELETE CASCADE,
name TEXT NOT NULL,
edges JSONB NOT NULL,
nodes JSONB NOT NULL,
options JSONB NOT NULL,
hosts JSONB NOT NULL,
subnets JSONB NOT NULL,
services JSONB NOT NULL,
groups JSONB NOT NULL,
is_stale BOOLEAN,
last_refreshed TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_locked BOOLEAN,
locked_at TIMESTAMPTZ,
locked_by UUID,
removed_hosts UUID[],
removed_services UUID[],
removed_subnets UUID[],
removed_groups UUID[],
parent_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_topologies_network ON topologies(network_id);

-- Migration to change hosts.services from JSONB to UUID[]
-- Converts JSONB array to UUID array, handles NULL and non-array cases

ALTER TABLE hosts
ALTER COLUMN services TYPE UUID[]
USING CASE
WHEN services IS NULL THEN NULL
ELSE translate(services::text, '[]"', '{}')::UUID[]
END;
32 changes: 22 additions & 10 deletions backend/src/bin/server.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::{sync::Arc, time::Duration};
use std::{net::SocketAddr, sync::Arc, time::Duration};

use axum::{
Extension, Router,
http::{HeaderValue, Method},
};
use clap::Parser;
use netvisor::server::{
auth::middleware::AuthenticatedEntity,
billing::types::base::{BillingPlan, BillingRate, Price},
config::{AppState, CliArgs, ServerConfig},
organizations::r#impl::base::{Organization, OrganizationBase},
Expand Down Expand Up @@ -273,7 +274,12 @@ async fn main() -> anyhow::Result<()> {

// Spawn server in background
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
});

// Start cron for discovery scheduler
Expand Down Expand Up @@ -312,17 +318,23 @@ async fn main() -> anyhow::Result<()> {
// First load - populate user and org
if all_users.is_empty() {
let organization = organization_service
.create(Organization::new(OrganizationBase {
stripe_customer_id: None,
plan: None,
plan_status: None,
name: "My Organization".to_string(),
is_onboarded: false,
}))
.create(
Organization::new(OrganizationBase {
stripe_customer_id: None,
plan: None,
plan_status: None,
name: "My Organization".to_string(),
is_onboarded: false,
}),
AuthenticatedEntity::System,
)
.await?;

user_service
.create_user(User::new(UserBase::new_seed(organization.id)))
.create(
User::new(UserBase::new_seed(organization.id)),
AuthenticatedEntity::System,
)
.await?;
} else {
tracing::debug!("Server already has data, skipping seed data");
Expand Down
10 changes: 5 additions & 5 deletions backend/src/daemon/discovery/service/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ impl DiscoveryRunner<NetworkScanDiscovery> {
total_ips = %total_ips,
scanned = %scanned,
discovered = %successful_discoveries.len(),
"📊 Scan complete"
"Scan complete"
);

Ok(successful_discoveries)
Expand Down Expand Up @@ -356,10 +356,10 @@ impl DiscoveryRunner<NetworkScanDiscovery> {
Ok((open_ports, endpoint_responses)) => {
if !open_ports.is_empty() || !endpoint_responses.is_empty() {
tracing::info!(
"Processing host {} with {} open ports and {} endpoint responses",
ip,
open_ports.len(),
endpoint_responses.len()
ip = %ip,
open_port_count = %open_ports.len(),
endpoint_response_count = %endpoint_responses.len(),
"Processing host",
);

// Check cancellation before processing
Expand Down
2 changes: 1 addition & 1 deletion backend/src/daemon/discovery/types/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
#[derive(Debug, Clone, Serialize, Deserialize, Copy, PartialEq, Eq, Hash)]
pub enum DiscoveryPhase {
Pending, // Initial state, set by server; all subsequent states until Finished are set by Daemon
Starting,
Expand Down
20 changes: 9 additions & 11 deletions backend/src/daemon/utils/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,22 +226,20 @@ pub trait DaemonUtils {
} else {
// Use automatic
tracing::info!(
"Using automatic concurrent_scans={} with port_batch={} per host \
(FD limit: {}, available: {}, FDs per host: {})",
optimal_concurrent,
port_batch_bounded,
fd_limit,
available,
fds_per_host
concurrent_scans = %optimal_concurrent,
port_batch = %port_batch_bounded,
fd_limit = %fd_limit,
fd_available = %available,
fds_per_host = %fds_per_host,
"Using automatic concurrent_scans",
);
optimal_concurrent
};

if result == 1 {
if result < 5 {
tracing::warn!(
"Very low concurrency (1 host). File descriptor limit is {}. \
Consider increasing for better performance.",
fd_limit
fd_limit = %fd_limit,
"Very low concurrency. Consider increasing for better performance.",
);
}

Expand Down
88 changes: 40 additions & 48 deletions backend/src/server/api_keys/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use crate::server::{
auth::middleware::RequireMember,
config::AppState,
shared::{
handlers::traits::{CrudHandlers, delete_handler, get_all_handler, get_by_id_handler},
handlers::traits::{
CrudHandlers, bulk_delete_handler, delete_handler, get_all_handler, get_by_id_handler,
},
services::traits::CrudService,
types::api::{ApiError, ApiResponse, ApiResult},
},
Expand All @@ -24,6 +26,7 @@ pub fn create_router() -> Router<Arc<AppState>> {
.route("/{id}", put(update_handler))
.route("/{id}", delete(delete_handler::<ApiKey>))
.route("/{id}", get(get_by_id_handler::<ApiKey>))
.route("/bulk-delete", post(bulk_delete_handler::<ApiKey>))
}

pub async fn create_handler(
Expand All @@ -39,21 +42,17 @@ pub async fn create_handler(
);

let service = ApiKey::get_service(&state);
let api_key = service.create(api_key).await.map_err(|e| {
tracing::error!(
error = %e,
user_id = %user.user_id,
"Failed to create API key"
);
ApiError::internal_error(&e.to_string())
})?;

tracing::info!(
api_key_id = %api_key.id,
api_key_name = %api_key.base.name,
user_id = %user.user_id,
"API key created via API (key shown to user)"
);
let api_key = service
.create(api_key, user.clone().into())
.await
.map_err(|e| {
tracing::error!(
error = %e,
user_id = %user.user_id,
"Failed to create API key"
);
ApiError::internal_error(&e.to_string())
})?;

Ok(Json(ApiResponse::success(ApiKeyResponse {
key: api_key.base.key.clone(),
Expand All @@ -66,28 +65,25 @@ pub async fn rotate_key_handler(
RequireMember(user): RequireMember,
Path(api_key_id): Path<Uuid>,
) -> ApiResult<Json<ApiResponse<String>>> {
tracing::info!(
tracing::debug!(
api_key_id = %api_key_id,
user_id = %user.user_id,
"API key rotation request received"
);

let service = ApiKey::get_service(&state);
let key = service.rotate_key(api_key_id).await.map_err(|e| {
tracing::error!(
api_key_id = %api_key_id,
user_id = %user.user_id,
error = %e,
"Failed to rotate API key"
);
ApiError::internal_error(&e.to_string())
})?;

tracing::info!(
api_key_id = %api_key_id,
user_id = %user.user_id,
"API key rotated via API (new key shown to user)"
);
let key = service
.rotate_key(api_key_id, user.clone().into())
.await
.map_err(|e| {
tracing::error!(
api_key_id = %api_key_id,
user_id = %user.user_id,
error = %e,
"Failed to rotate API key"
);
ApiError::internal_error(&e.to_string())
})?;

Ok(Json(ApiResponse::success(key)))
}
Expand Down Expand Up @@ -131,22 +127,18 @@ pub async fn update_handler(
// Preserve the key - don't allow it to be changed via update
request.base.key = existing.base.key;

let updated = service.update(&mut request).await.map_err(|e| {
tracing::error!(
api_key_id = %id,
user_id = %user.user_id,
error = %e,
"Failed to update API key"
);
ApiError::internal_error(&e.to_string())
})?;

tracing::info!(
api_key_id = %id,
api_key_name = %updated.base.name,
user_id = %user.user_id,
"API key updated via API"
);
let updated = service
.update(&mut request, user.clone().into())
.await
.map_err(|e| {
tracing::error!(
api_key_id = %id,
user_id = %user.user_id,
error = %e,
"Failed to update API key"
);
ApiError::internal_error(&e.to_string())
})?;

Ok(Json(ApiResponse::success(updated)))
}
12 changes: 10 additions & 2 deletions backend/src/server/api_keys/impl/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize, Serializer};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
use crate::server::shared::entities::ChangeTriggersTopologyStaleness;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ApiKeyBase {
#[serde(serialize_with = "serialize_api_key_status")]
pub key: String,
Expand All @@ -22,7 +24,7 @@ where
serializer.serialize_str("***REDACTED***")
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ApiKey {
pub id: Uuid,
pub updated_at: DateTime<Utc>,
Expand All @@ -36,3 +38,9 @@ impl Display for ApiKey {
write!(f, "{}: {}", self.base.name, self.id)
}
}

impl ChangeTriggersTopologyStaleness<ApiKey> for ApiKey {
fn triggers_staleness(&self, _other: Option<ApiKey>) -> bool {
false
}
}
Loading
Loading