Files
claudetools/projects/msp-tools/guru-rmm/server/src/db/clients.rs
Mike Swanson 6c316aa701 Add VPN configuration tools and agent documentation
Created comprehensive VPN setup tooling for Peaceful Spirit L2TP/IPsec connection
and enhanced agent documentation framework.

VPN Configuration (PST-NW-VPN):
- Setup-PST-L2TP-VPN.ps1: Automated L2TP/IPsec setup with split-tunnel and DNS
- Connect-PST-VPN.ps1: Connection helper with PPP adapter detection, DNS (192.168.0.2), and route config (192.168.0.0/24)
- Connect-PST-VPN-Standalone.ps1: Self-contained connection script for remote deployment
- Fix-PST-VPN-Auth.ps1: Authentication troubleshooting for CHAP/MSChapv2
- Diagnose-VPN-Interface.ps1: Comprehensive VPN interface and routing diagnostic
- Quick-Test-VPN.ps1: Fast connectivity verification (DNS/router/routes)
- Add-PST-VPN-Route-Manual.ps1: Manual route configuration helper
- vpn-connect.bat, vpn-disconnect.bat: Simple batch file shortcuts
- OpenVPN config files (Windows-compatible, abandoned for L2TP)

Key VPN Implementation Details:
- L2TP creates PPP adapter with connection name as interface description
- UniFi auto-configures DNS (192.168.0.2) but requires manual route to 192.168.0.0/24
- Split-tunnel enabled (only remote traffic through VPN)
- All-user connection for pre-login auto-connect via scheduled task
- Authentication: CHAP + MSChapv2 for UniFi compatibility

Agent Documentation:
- AGENT_QUICK_REFERENCE.md: Quick reference for all specialized agents
- documentation-squire.md: Documentation and task management specialist agent
- Updated all agent markdown files with standardized formatting

Project Organization:
- Moved conversation logs to dedicated directories (guru-connect-conversation-logs, guru-rmm-conversation-logs)
- Cleaned up old session JSONL files from projects/msp-tools/
- Added guru-connect infrastructure (agent, dashboard, proto, scripts, .gitea workflows)
- Added guru-rmm server components and deployment configs

Technical Notes:
- VPN IP pool: 192.168.4.x (client gets 192.168.4.6)
- Remote network: 192.168.0.0/24 (router at 192.168.0.10)
- PSK: rrClvnmUeXEFo90Ol+z7tfsAZHeSK6w7
- Credentials: pst-admin / 24Hearts$

Files: 15 VPN scripts, 2 agent docs, conversation log reorganization,
guru-connect/guru-rmm infrastructure additions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 11:51:47 -07:00

158 lines
4.0 KiB
Rust

//! Client (organization) database operations
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use uuid::Uuid;
/// Client record from database
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct Client {
pub id: Uuid,
pub name: String,
pub code: Option<String>,
pub notes: Option<String>,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
/// Client response for API
#[derive(Debug, Clone, Serialize)]
pub struct ClientResponse {
pub id: Uuid,
pub name: String,
pub code: Option<String>,
pub notes: Option<String>,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub site_count: Option<i64>,
pub agent_count: Option<i64>,
}
impl From<Client> for ClientResponse {
fn from(c: Client) -> Self {
ClientResponse {
id: c.id,
name: c.name,
code: c.code,
notes: c.notes,
is_active: c.is_active,
created_at: c.created_at,
site_count: None,
agent_count: None,
}
}
}
/// Data for creating a new client
#[derive(Debug, Deserialize)]
pub struct CreateClient {
pub name: String,
pub code: Option<String>,
pub notes: Option<String>,
}
/// Data for updating a client
#[derive(Debug, Deserialize)]
pub struct UpdateClient {
pub name: Option<String>,
pub code: Option<String>,
pub notes: Option<String>,
pub is_active: Option<bool>,
}
/// Create a new client
pub async fn create_client(pool: &PgPool, client: CreateClient) -> Result<Client, sqlx::Error> {
sqlx::query_as::<_, Client>(
r#"
INSERT INTO clients (name, code, notes)
VALUES ($1, $2, $3)
RETURNING *
"#,
)
.bind(&client.name)
.bind(&client.code)
.bind(&client.notes)
.fetch_one(pool)
.await
}
/// Get a client by ID
pub async fn get_client_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Client>, sqlx::Error> {
sqlx::query_as::<_, Client>("SELECT * FROM clients WHERE id = $1")
.bind(id)
.fetch_optional(pool)
.await
}
/// Get all clients
pub async fn get_all_clients(pool: &PgPool) -> Result<Vec<Client>, sqlx::Error> {
sqlx::query_as::<_, Client>("SELECT * FROM clients ORDER BY name")
.fetch_all(pool)
.await
}
/// Get all clients with counts
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct ClientWithCounts {
pub id: Uuid,
pub name: String,
pub code: Option<String>,
pub notes: Option<String>,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub site_count: i64,
pub agent_count: i64,
}
pub async fn get_all_clients_with_counts(pool: &PgPool) -> Result<Vec<ClientWithCounts>, sqlx::Error> {
sqlx::query_as::<_, ClientWithCounts>(
r#"
SELECT
c.*,
COALESCE((SELECT COUNT(*) FROM sites WHERE client_id = c.id), 0) as site_count,
COALESCE((SELECT COUNT(*) FROM agents a JOIN sites s ON a.site_id = s.id WHERE s.client_id = c.id), 0) as agent_count
FROM clients c
ORDER BY c.name
"#,
)
.fetch_all(pool)
.await
}
/// Update a client
pub async fn update_client(
pool: &PgPool,
id: Uuid,
update: UpdateClient,
) -> Result<Option<Client>, sqlx::Error> {
sqlx::query_as::<_, Client>(
r#"
UPDATE clients
SET name = COALESCE($1, name),
code = COALESCE($2, code),
notes = COALESCE($3, notes),
is_active = COALESCE($4, is_active)
WHERE id = $5
RETURNING *
"#,
)
.bind(&update.name)
.bind(&update.code)
.bind(&update.notes)
.bind(&update.is_active)
.bind(id)
.fetch_optional(pool)
.await
}
/// Delete a client
pub async fn delete_client(pool: &PgPool, id: Uuid) -> Result<bool, sqlx::Error> {
let result = sqlx::query("DELETE FROM clients WHERE id = $1")
.bind(id)
.execute(pool)
.await?;
Ok(result.rows_affected() > 0)
}